From 9e8e8b4e987f4db1e0c2da75aaf7ed1aab1f9eab Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:04:21 +0000 Subject: [PATCH 01/51] Eng 2431 create computediskaccess adapter (#3829) > [!NOTE] > **Medium Risk** > Adds a new Azure resource adapter plus integration test that provisions and deletes real cloud resources; risk is mainly around discovery correctness and potential test flakiness/cost, with minimal impact on existing adapters. > > **Overview** > Adds first-class discovery support for Azure **Compute Disk Access** resources via a new `NewComputeDiskAccess` wrapper that implements `Get`, `List`, and `ListStream`, and emits linked queries to `ComputeDiskAccessPrivateEndpointConnection` plus related `NetworkPrivateEndpoint` resources. > > Wires the new adapter into Azure manual adapter initialization (`adapters.go`), introduces a thin `DiskAccessesClient` interface + generated mock for testability, expands shared type/model constants to include `ComputeDiskAccessPrivateEndpointConnection`, and adds both unit tests and an end-to-end Azure integration test that provisions a real disk access and validates retrieval/listing/linking. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d849596e95b4332905b1d7a6047f263be3a7e9b5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fda086881bcc54eadf23a7c012fff5a2325b0cac --- auth/auth.go | 448 - auth/auth_client.go | 124 - auth/auth_test.go | 201 - auth/gcpauth.go | 58 - auth/middleware.go | 499 -- auth/middleware_test.go | 792 -- auth/nats.go | 251 - auth/nats_test.go | 423 - auth/tracing.go | 18 - .../adapterhelpers_always_get_source.go | 6 +- .../adapterhelpers_always_get_source_test.go | 6 +- .../adapterhelpers_describe_source.go | 6 +- .../adapterhelpers_describe_source_test.go | 6 +- .../adapterhelpers_get_list_adapter_v2.go | 6 +- ...adapterhelpers_get_list_adapter_v2_test.go | 6 +- .../adapterhelpers_get_list_source.go | 4 +- .../adapterhelpers_get_list_source_test.go | 4 +- .../adapterhelpers_notfound_cache_test.go | 4 +- .../adapters/adapterhelpers_shared_tests.go | 2 +- aws-source/adapters/adapterhelpers_util.go | 4 +- aws-source/adapters/apigateway-api-key.go | 4 +- .../adapters/apigateway-api-key_test.go | 4 +- aws-source/adapters/apigateway-authorizer.go | 4 +- .../adapters/apigateway-authorizer_test.go | 4 +- aws-source/adapters/apigateway-deployment.go | 4 +- .../adapters/apigateway-deployment_test.go | 4 +- aws-source/adapters/apigateway-domain-name.go | 4 +- .../adapters/apigateway-domain-name_test.go | 4 +- aws-source/adapters/apigateway-integration.go | 4 +- .../adapters/apigateway-integration_test.go | 4 +- .../adapters/apigateway-method-response.go | 4 +- .../apigateway-method-response_test.go | 4 +- aws-source/adapters/apigateway-method.go | 4 +- aws-source/adapters/apigateway-method_test.go | 4 +- aws-source/adapters/apigateway-model.go | 4 +- aws-source/adapters/apigateway-model_test.go | 4 +- aws-source/adapters/apigateway-resource.go | 4 +- .../adapters/apigateway-resource_test.go | 2 +- aws-source/adapters/apigateway-rest-api.go | 4 +- .../adapters/apigateway-rest-api_test.go | 4 +- aws-source/adapters/apigateway-stage.go | 4 +- aws-source/adapters/apigateway-stage_test.go | 4 +- .../autoscaling-auto-scaling-group.go | 4 +- .../autoscaling-auto-scaling-group_test.go | 4 +- .../autoscaling-auto-scaling-policy.go | 4 +- .../autoscaling-auto-scaling-policy_test.go | 4 +- .../adapters/cloudfront-cache-policy.go | 4 +- .../adapters/cloudfront-cache-policy_test.go | 2 +- ...cloudfront-continuous-deployment-policy.go | 4 +- ...front-continuous-deployment-policy_test.go | 4 +- .../adapters/cloudfront-distribution.go | 4 +- .../adapters/cloudfront-distribution_test.go | 4 +- aws-source/adapters/cloudfront-function.go | 4 +- .../adapters/cloudfront-function_test.go | 2 +- aws-source/adapters/cloudfront-key-group.go | 4 +- .../adapters/cloudfront-key-group_test.go | 2 +- .../cloudfront-origin-access-control.go | 4 +- .../cloudfront-origin-access-control_test.go | 2 +- .../cloudfront-origin-request-policy.go | 4 +- .../cloudfront-origin-request-policy_test.go | 2 +- .../cloudfront-realtime-log-config.go | 4 +- .../cloudfront-realtime-log-config_test.go | 4 +- .../cloudfront-response-headers-policy.go | 4 +- ...cloudfront-response-headers-policy_test.go | 2 +- .../cloudfront-streaming-distribution.go | 4 +- .../cloudfront-streaming-distribution_test.go | 4 +- aws-source/adapters/cloudwatch-alarm.go | 4 +- aws-source/adapters/cloudwatch-alarm_test.go | 4 +- .../adapters/cloudwatch-instance-metric.go | 4 +- ...dwatch-instance-metric_integration_test.go | 2 +- .../cloudwatch-instance-metric_test.go | 2 +- .../adapters/cloudwatch_metric_links.go | 2 +- .../adapters/directconnect-connection.go | 4 +- .../adapters/directconnect-connection_test.go | 4 +- .../directconnect-customer-metadata.go | 4 +- .../directconnect-customer-metadata_test.go | 2 +- ...ct-connect-gateway-association-proposal.go | 4 +- ...nnect-gateway-association-proposal_test.go | 4 +- ...nect-direct-connect-gateway-association.go | 4 +- ...direct-connect-gateway-association_test.go | 4 +- ...nnect-direct-connect-gateway-attachment.go | 4 +- ...-direct-connect-gateway-attachment_test.go | 4 +- .../directconnect-direct-connect-gateway.go | 4 +- ...rectconnect-direct-connect-gateway_test.go | 4 +- .../directconnect-hosted-connection.go | 4 +- .../directconnect-hosted-connection_test.go | 4 +- .../adapters/directconnect-interconnect.go | 4 +- .../directconnect-interconnect_test.go | 4 +- aws-source/adapters/directconnect-lag.go | 4 +- aws-source/adapters/directconnect-lag_test.go | 4 +- aws-source/adapters/directconnect-location.go | 4 +- .../adapters/directconnect-location_test.go | 2 +- .../directconnect-router-configuration.go | 4 +- ...directconnect-router-configuration_test.go | 4 +- .../adapters/directconnect-virtual-gateway.go | 4 +- .../directconnect-virtual-gateway_test.go | 2 +- .../directconnect-virtual-interface.go | 4 +- .../directconnect-virtual-interface_test.go | 4 +- aws-source/adapters/dynamodb-backup.go | 4 +- aws-source/adapters/dynamodb-backup_test.go | 4 +- aws-source/adapters/dynamodb-table.go | 4 +- aws-source/adapters/dynamodb-table_test.go | 4 +- aws-source/adapters/ec2-address.go | 4 +- aws-source/adapters/ec2-address_test.go | 4 +- .../ec2-capacity-reservation-fleet.go | 4 +- .../ec2-capacity-reservation-fleet_test.go | 2 +- .../adapters/ec2-capacity-reservation.go | 4 +- .../adapters/ec2-capacity-reservation_test.go | 4 +- .../ec2-egress-only-internet-gateway.go | 4 +- .../ec2-egress-only-internet-gateway_test.go | 4 +- .../ec2-iam-instance-profile-association.go | 4 +- ...2-iam-instance-profile-association_test.go | 4 +- aws-source/adapters/ec2-image.go | 4 +- aws-source/adapters/ec2-image_test.go | 2 +- .../adapters/ec2-instance-event-window.go | 4 +- .../ec2-instance-event-window_test.go | 4 +- aws-source/adapters/ec2-instance-status.go | 4 +- .../adapters/ec2-instance-status_test.go | 4 +- aws-source/adapters/ec2-instance.go | 4 +- aws-source/adapters/ec2-instance_test.go | 4 +- aws-source/adapters/ec2-internet-gateway.go | 4 +- .../adapters/ec2-internet-gateway_test.go | 4 +- aws-source/adapters/ec2-key-pair.go | 4 +- aws-source/adapters/ec2-key-pair_test.go | 2 +- .../adapters/ec2-launch-template-version.go | 4 +- .../ec2-launch-template-version_test.go | 4 +- aws-source/adapters/ec2-launch-template.go | 4 +- .../adapters/ec2-launch-template_test.go | 2 +- aws-source/adapters/ec2-nat-gateway.go | 4 +- aws-source/adapters/ec2-nat-gateway_test.go | 4 +- aws-source/adapters/ec2-network-acl.go | 4 +- aws-source/adapters/ec2-network-acl_test.go | 4 +- .../ec2-network-interface-permission.go | 4 +- .../ec2-network-interface-permission_test.go | 4 +- aws-source/adapters/ec2-network-interface.go | 4 +- .../adapters/ec2-network-interface_test.go | 4 +- aws-source/adapters/ec2-placement-group.go | 4 +- .../adapters/ec2-placement-group_test.go | 2 +- aws-source/adapters/ec2-reserved-instance.go | 4 +- .../adapters/ec2-reserved-instance_test.go | 2 +- aws-source/adapters/ec2-route-table.go | 4 +- aws-source/adapters/ec2-route-table_test.go | 4 +- .../adapters/ec2-security-group-rule.go | 4 +- .../adapters/ec2-security-group-rule_test.go | 4 +- aws-source/adapters/ec2-security-group.go | 4 +- .../adapters/ec2-security-group_test.go | 4 +- aws-source/adapters/ec2-snapshot.go | 4 +- aws-source/adapters/ec2-snapshot_test.go | 4 +- aws-source/adapters/ec2-subnet.go | 4 +- aws-source/adapters/ec2-subnet_test.go | 4 +- aws-source/adapters/ec2-volume-status.go | 4 +- aws-source/adapters/ec2-volume-status_test.go | 4 +- aws-source/adapters/ec2-volume.go | 4 +- aws-source/adapters/ec2-volume_test.go | 4 +- aws-source/adapters/ec2-vpc-endpoint.go | 4 +- aws-source/adapters/ec2-vpc-endpoint_test.go | 4 +- .../adapters/ec2-vpc-peering-connection.go | 4 +- .../ec2-vpc-peering-connection_test.go | 4 +- aws-source/adapters/ec2-vpc.go | 4 +- aws-source/adapters/ec2-vpc_test.go | 2 +- aws-source/adapters/ecs-capacity-provider.go | 4 +- .../adapters/ecs-capacity-provider_test.go | 6 +- aws-source/adapters/ecs-cluster.go | 4 +- aws-source/adapters/ecs-cluster_test.go | 4 +- aws-source/adapters/ecs-container-instance.go | 4 +- .../adapters/ecs-container-instance_test.go | 4 +- aws-source/adapters/ecs-service.go | 4 +- aws-source/adapters/ecs-service_test.go | 4 +- aws-source/adapters/ecs-task-definition.go | 4 +- .../adapters/ecs-task-definition_test.go | 4 +- aws-source/adapters/ecs-task.go | 4 +- aws-source/adapters/ecs-task_test.go | 4 +- aws-source/adapters/efs-access-point.go | 4 +- aws-source/adapters/efs-access-point_test.go | 4 +- aws-source/adapters/efs-backup-policy.go | 4 +- aws-source/adapters/efs-file-system.go | 4 +- aws-source/adapters/efs-file-system_test.go | 4 +- aws-source/adapters/efs-mount-target.go | 4 +- aws-source/adapters/efs-mount-target_test.go | 2 +- .../adapters/efs-replication-configuration.go | 4 +- .../efs-replication-configuration_test.go | 2 +- aws-source/adapters/efs.go | 2 +- aws-source/adapters/eks-addon.go | 4 +- aws-source/adapters/eks-addon_test.go | 2 +- aws-source/adapters/eks-cluster.go | 4 +- aws-source/adapters/eks-cluster_test.go | 4 +- aws-source/adapters/eks-fargate-profile.go | 4 +- .../adapters/eks-fargate-profile_test.go | 4 +- aws-source/adapters/eks-nodegroup.go | 4 +- aws-source/adapters/eks-nodegroup_test.go | 4 +- aws-source/adapters/elb-instance-health.go | 4 +- .../adapters/elb-instance-health_test.go | 2 +- aws-source/adapters/elb-load-balancer.go | 4 +- aws-source/adapters/elb-load-balancer_test.go | 2 +- aws-source/adapters/elbv2-listener.go | 4 +- aws-source/adapters/elbv2-listener_test.go | 2 +- aws-source/adapters/elbv2-load-balancer.go | 4 +- .../adapters/elbv2-load-balancer_test.go | 2 +- aws-source/adapters/elbv2-rule.go | 4 +- aws-source/adapters/elbv2-rule_test.go | 6 +- aws-source/adapters/elbv2-target-group.go | 4 +- .../adapters/elbv2-target-group_test.go | 4 +- aws-source/adapters/elbv2-target-health.go | 4 +- .../adapters/elbv2-target-health_test.go | 2 +- aws-source/adapters/elbv2.go | 2 +- aws-source/adapters/elbv2_test.go | 2 +- aws-source/adapters/iam-group.go | 4 +- aws-source/adapters/iam-group_test.go | 2 +- aws-source/adapters/iam-instance-profile.go | 4 +- .../adapters/iam-instance-profile_test.go | 2 +- aws-source/adapters/iam-policy.go | 4 +- aws-source/adapters/iam-policy_test.go | 6 +- aws-source/adapters/iam-role.go | 4 +- aws-source/adapters/iam-role_test.go | 6 +- aws-source/adapters/iam-user.go | 4 +- aws-source/adapters/iam-user_test.go | 6 +- aws-source/adapters/iam.go | 2 +- aws-source/adapters/iam_test.go | 2 +- .../integration/apigateway/apigateway_test.go | 4 +- .../adapters/integration/ec2/instance_test.go | 6 +- .../adapters/integration/kms/kms_test.go | 6 +- .../networkmanager/networkmanager_test.go | 6 +- .../adapters/integration/ssm/main_test.go | 6 +- aws-source/adapters/integration/util.go | 4 +- aws-source/adapters/kms-alias.go | 4 +- aws-source/adapters/kms-alias_test.go | 4 +- aws-source/adapters/kms-custom-key-store.go | 4 +- .../adapters/kms-custom-key-store_test.go | 4 +- aws-source/adapters/kms-grant.go | 4 +- aws-source/adapters/kms-grant_test.go | 4 +- aws-source/adapters/kms-key-policy.go | 4 +- aws-source/adapters/kms-key-policy_test.go | 4 +- aws-source/adapters/kms-key.go | 4 +- aws-source/adapters/kms-key_test.go | 2 +- .../adapters/lambda-event-source-mapping.go | 4 +- .../lambda-event-source-mapping_test.go | 4 +- aws-source/adapters/lambda-function.go | 4 +- aws-source/adapters/lambda-function_test.go | 4 +- aws-source/adapters/lambda-layer-version.go | 4 +- .../adapters/lambda-layer-version_test.go | 4 +- aws-source/adapters/lambda-layer.go | 4 +- aws-source/adapters/lambda-layer_test.go | 4 +- aws-source/adapters/main.go | 2 +- .../network-firewall-firewall-policy.go | 4 +- .../network-firewall-firewall-policy_test.go | 2 +- .../adapters/network-firewall-firewall.go | 4 +- .../network-firewall-firewall_test.go | 2 +- .../adapters/network-firewall-rule-group.go | 4 +- .../network-firewall-rule-group_test.go | 2 +- ...k-firewall-tls-inspection-configuration.go | 4 +- ...ewall-tls-inspection-configuration_test.go | 2 +- aws-source/adapters/networkfirewall.go | 2 +- .../networkmanager-connect-attachment.go | 4 +- .../networkmanager-connect-attachment_test.go | 2 +- ...networkmanager-connect-peer-association.go | 4 +- ...rkmanager-connect-peer-association_test.go | 2 +- .../adapters/networkmanager-connect-peer.go | 4 +- .../networkmanager-connect-peer_test.go | 2 +- .../adapters/networkmanager-connection.go | 4 +- .../networkmanager-connection_test.go | 4 +- .../networkmanager-core-network-policy.go | 4 +- ...networkmanager-core-network-policy_test.go | 2 +- .../adapters/networkmanager-core-network.go | 4 +- .../networkmanager-core-network_test.go | 2 +- aws-source/adapters/networkmanager-device.go | 4 +- .../adapters/networkmanager-device_test.go | 4 +- .../adapters/networkmanager-global-network.go | 4 +- .../networkmanager-global-network_test.go | 2 +- .../networkmanager-link-association.go | 4 +- .../networkmanager-link-association_test.go | 2 +- aws-source/adapters/networkmanager-link.go | 4 +- .../adapters/networkmanager-link_test.go | 4 +- ...rkmanager-network-resource-relationship.go | 4 +- ...ager-network-resource-relationship_test.go | 2 +- ...workmanager-site-to-site-vpn-attachment.go | 4 +- ...anager-site-to-site-vpn-attachment_test.go | 2 +- aws-source/adapters/networkmanager-site.go | 4 +- .../adapters/networkmanager-site_test.go | 4 +- ...ransit-gateway-connect-peer-association.go | 4 +- ...t-gateway-connect-peer-association_test.go | 2 +- .../networkmanager-transit-gateway-peering.go | 4 +- ...orkmanager-transit-gateway-peering_test.go | 2 +- ...orkmanager-transit-gateway-registration.go | 4 +- ...nager-transit-gateway-registration_test.go | 2 +- ...-transit-gateway-route-table-attachment.go | 4 +- ...sit-gateway-route-table-attachment_test.go | 2 +- .../adapters/networkmanager-vpc-attachment.go | 4 +- .../networkmanager-vpc-attachment_test.go | 2 +- .../rds-db-cluster-parameter-group.go | 4 +- .../rds-db-cluster-parameter-group_test.go | 2 +- aws-source/adapters/rds-db-cluster.go | 4 +- aws-source/adapters/rds-db-cluster_test.go | 4 +- aws-source/adapters/rds-db-instance.go | 4 +- aws-source/adapters/rds-db-instance_test.go | 4 +- aws-source/adapters/rds-db-parameter-group.go | 4 +- .../adapters/rds-db-parameter-group_test.go | 2 +- aws-source/adapters/rds-db-subnet-group.go | 4 +- .../adapters/rds-db-subnet-group_test.go | 4 +- aws-source/adapters/rds-option-group.go | 4 +- aws-source/adapters/route53-health-check.go | 4 +- .../adapters/route53-health-check_test.go | 4 +- aws-source/adapters/route53-hosted-zone.go | 4 +- .../adapters/route53-hosted-zone_test.go | 4 +- .../adapters/route53-resource-record-set.go | 4 +- .../route53-resource-record-set_test.go | 4 +- aws-source/adapters/s3.go | 4 +- aws-source/adapters/s3_test.go | 4 +- .../adapters/sns-data-protection-policy.go | 4 +- .../sns-data-protection-policy_test.go | 2 +- aws-source/adapters/sns-endpoint.go | 4 +- aws-source/adapters/sns-endpoint_test.go | 2 +- .../adapters/sns-platform-application.go | 4 +- .../adapters/sns-platform-application_test.go | 2 +- aws-source/adapters/sns-subscription.go | 4 +- aws-source/adapters/sns-subscription_test.go | 2 +- aws-source/adapters/sns-topic.go | 4 +- aws-source/adapters/sns-topic_test.go | 2 +- aws-source/adapters/sqs-queue.go | 4 +- aws-source/adapters/sqs-queue_test.go | 4 +- aws-source/adapters/ssm-parameter.go | 4 +- aws-source/adapters/ssm-parameter_test.go | 4 +- aws-source/build/package/Dockerfile | 2 +- aws-source/cmd/root.go | 6 +- aws-source/proc/proc.go | 4 +- aws-source/proc/proc_test.go | 4 +- cmd/auth_client.go | 8 +- cmd/auth_client_test.go | 4 +- cmd/bookmarks_create_bookmark.go | 2 +- cmd/bookmarks_get_affected_bookmarks.go | 2 +- cmd/bookmarks_get_bookmark.go | 2 +- cmd/changes_end_change.go | 2 +- cmd/changes_get_change.go | 2 +- cmd/changes_get_change_test.go | 2 +- cmd/changes_get_signals.go | 2 +- cmd/changes_list_changes.go | 2 +- cmd/changes_start_change.go | 2 +- cmd/changes_submit_plan.go | 2 +- cmd/changes_submit_signal.go | 2 +- cmd/explore.go | 6 +- cmd/flags.go | 2 +- cmd/flags_test.go | 2 +- cmd/integrations_tfc.go | 2 +- cmd/invites_crud.go | 2 +- cmd/pterm.go | 10 +- cmd/request.go | 4 +- cmd/request_load.go | 6 +- cmd/request_query.go | 6 +- cmd/root.go | 6 +- cmd/root_test.go | 2 +- cmd/snapshots_create.go | 6 +- cmd/snapshots_get_snapshot.go | 2 +- cmd/terraform_apply.go | 2 +- cmd/terraform_plan.go | 2 +- discovery/adapter.go | 205 - discovery/adapter_test.go | 736 -- discovery/adapterhost.go | 203 - discovery/adapterhost_bench_test.go | 822 -- discovery/adapterhost_test.go | 362 - discovery/cmd.go | 326 - discovery/cmd_test.go | 235 - discovery/doc.go | 33 - discovery/engine.go | 898 -- discovery/engine_initerror_test.go | 365 - discovery/engine_test.go | 808 -- discovery/enginerequests.go | 532 -- discovery/enginerequests_test.go | 460 -- discovery/getfindmutex.go | 80 - discovery/getfindmutex_test.go | 194 - discovery/heartbeat.go | 165 - discovery/heartbeat_test.go | 207 - discovery/item_tests.go | 100 - discovery/logs.go | 102 - discovery/logs_test.go | 398 - discovery/main_test.go | 24 - discovery/nats_shared_test.go | 111 - discovery/nats_watcher.go | 126 - discovery/nats_watcher_test.go | 572 -- discovery/nil_publisher.go | 111 - discovery/performance_test.go | 216 - discovery/querytracker.go | 92 - discovery/querytracker_test.go | 299 - discovery/shared_test.go | 275 - discovery/tracing.go | 20 - .../docs/sources/_category_.json | 6 + .../aws/Types/apigateway-domain-name.md | 16 + .../sources/aws/Types/apigateway-resource.md | 17 + .../sources/aws/Types/apigateway-rest-api.md | 27 + .../Types/autoscaling-auto-scaling-group.md | 39 + .../aws/Types/cloudfront-cache-policy.md | 16 + ...cloudfront-continuous-deployment-policy.md | 19 + .../aws/Types/cloudfront-distribution.md | 58 + .../sources/aws/Types/cloudfront-function.md | 16 + .../sources/aws/Types/cloudfront-key-group.md | 17 + .../Types/cloudfront-origin-access-control.md | 17 + .../Types/cloudfront-origin-request-policy.md | 17 + .../Types/cloudfront-realtime-log-config.md | 17 + .../cloudfront-response-headers-policy.md | 17 + .../cloudfront-streaming-distribution.md | 23 + .../sources/aws/Types/cloudwatch-alarm.md | 17 + .../aws/Types/directconnect-connection.md | 30 + .../Types/directconnect-customer-metadata.md | 13 + ...ct-connect-gateway-association-proposal.md | 24 + ...nect-direct-connect-gateway-association.md | 23 + ...nnect-direct-connect-gateway-attachment.md | 23 + .../directconnect-direct-connect-gateway.md | 17 + .../Types/directconnect-hosted-connection.md | 31 + .../aws/Types/directconnect-interconnect.md | 27 + .../sources/aws/Types/directconnect-lag.md | 31 + .../aws/Types/directconnect-location.md | 17 + .../directconnect-router-configuration.md | 23 + .../Types/directconnect-virtual-gateway.md | 14 + .../Types/directconnect-virtual-interface.md | 41 + .../docs/sources/aws/Types/dynamodb-backup.md | 18 + .../docs/sources/aws/Types/dynamodb-table.md | 27 + .../docs/sources/aws/Types/ec2-address.md | 31 + .../Types/ec2-capacity-reservation-fleet.md | 19 + .../aws/Types/ec2-capacity-reservation.md | 27 + .../Types/ec2-egress-only-internet-gateway.md | 23 + .../ec2-iam-instance-profile-association.md | 23 + .../docs/sources/aws/Types/ec2-image.md | 17 + .../aws/Types/ec2-instance-event-window.md | 19 + .../sources/aws/Types/ec2-instance-status.md | 14 + .../docs/sources/aws/Types/ec2-instance.md | 67 + .../sources/aws/Types/ec2-internet-gateway.md | 23 + .../docs/sources/aws/Types/ec2-key-pair.md | 17 + .../aws/Types/ec2-launch-template-version.md | 51 + .../sources/aws/Types/ec2-launch-template.md | 17 + .../docs/sources/aws/Types/ec2-nat-gateway.md | 35 + .../docs/sources/aws/Types/ec2-network-acl.md | 27 + .../Types/ec2-network-interface-permission.md | 19 + .../aws/Types/ec2-network-interface.md | 43 + .../sources/aws/Types/ec2-placement-group.md | 16 + .../aws/Types/ec2-reserved-instance.md | 13 + .../docs/sources/aws/Types/ec2-route-table.md | 58 + .../aws/Types/ec2-security-group-rule.md | 25 + .../sources/aws/Types/ec2-security-group.md | 23 + .../docs/sources/aws/Types/ec2-snapshot.md | 19 + .../docs/sources/aws/Types/ec2-subnet.md | 24 + .../sources/aws/Types/ec2-volume-status.md | 19 + .../docs/sources/aws/Types/ec2-volume.md | 22 + .../sources/aws/Types/ec2-vpc-endpoint.md | 16 + .../aws/Types/ec2-vpc-peering-connection.md | 25 + .../docs/sources/aws/Types/ec2-vpc.md | 16 + .../aws/Types/ecs-capacity-provider.md | 23 + .../docs/sources/aws/Types/ecs-cluster.md | 35 + .../aws/Types/ecs-container-instance.md | 18 + .../docs/sources/aws/Types/ecs-service.md | 43 + .../sources/aws/Types/ecs-task-definition.md | 27 + .../docs/sources/aws/Types/ecs-task.md | 35 + .../sources/aws/Types/efs-access-point.md | 17 + .../sources/aws/Types/efs-backup-policy.md | 17 + .../docs/sources/aws/Types/efs-file-system.md | 16 + .../sources/aws/Types/efs-mount-target.md | 17 + .../Types/efs-replication-configuration.md | 17 + .../docs/sources/aws/Types/eks-addon.md | 17 + .../docs/sources/aws/Types/eks-cluster.md | 16 + .../sources/aws/Types/eks-fargate-profile.md | 26 + .../docs/sources/aws/Types/eks-nodegroup.md | 38 + .../sources/aws/Types/elb-instance-health.md | 19 + .../sources/aws/Types/elb-load-balancer.md | 47 + .../docs/sources/aws/Types/elbv2-listener.md | 36 + .../sources/aws/Types/elbv2-load-balancer.md | 55 + .../docs/sources/aws/Types/elbv2-rule.md | 17 + .../sources/aws/Types/elbv2-target-group.md | 32 + .../sources/aws/Types/elbv2-target-health.md | 33 + .../docs/sources/aws/Types/iam-group.md | 16 + .../sources/aws/Types/iam-instance-profile.md | 26 + .../docs/sources/aws/Types/iam-policy.md | 33 + .../docs/sources/aws/Types/iam-role.md | 23 + .../docs/sources/aws/Types/iam-user.md | 23 + .../docs/sources/aws/Types/kms-alias.md | 23 + .../sources/aws/Types/kms-custom-key-store.md | 16 + .../docs/sources/aws/Types/kms-grant.md | 30 + .../docs/sources/aws/Types/kms-key-policy.md | 23 + .../docs/sources/aws/Types/kms-key.md | 31 + .../aws/Types/lambda-event-source-mapping.md | 35 + .../docs/sources/aws/Types/lambda-function.md | 44 + .../sources/aws/Types/lambda-layer-version.md | 17 + .../docs/sources/aws/Types/lambda-layer.md | 19 + .../Types/network-firewall-firewall-policy.md | 31 + .../aws/Types/network-firewall-firewall.md | 42 + .../aws/Types/network-firewall-rule-group.md | 30 + ...k-firewall-tls-inspection-configuration.md | 13 + .../networkmanager-connect-attachment.md | 21 + ...networkmanager-connect-peer-association.md | 31 + .../aws/Types/networkmanager-connect-peer.md | 40 + .../aws/Types/networkmanager-connection.md | 30 + .../networkmanager-core-network-policy.md | 22 + .../aws/Types/networkmanager-core-network.md | 27 + .../aws/Types/networkmanager-device.md | 39 + .../Types/networkmanager-global-network.md | 51 + .../Types/networkmanager-link-association.md | 29 + .../sources/aws/Types/networkmanager-link.md | 35 + ...rkmanager-network-resource-relationship.md | 43 + ...workmanager-site-to-site-vpn-attachment.md | 23 + .../sources/aws/Types/networkmanager-site.md | 30 + ...ransit-gateway-connect-peer-association.md | 29 + .../networkmanager-transit-gateway-peering.md | 23 + ...orkmanager-transit-gateway-registration.md | 18 + ...-transit-gateway-route-table-attachment.md | 27 + .../Types/networkmanager-vpc-attachment.md | 23 + .../Types/rds-db-cluster-parameter-group.md | 16 + .../docs/sources/aws/Types/rds-db-cluster.md | 51 + .../docs/sources/aws/Types/rds-db-instance.md | 55 + .../aws/Types/rds-db-parameter-group.md | 17 + .../sources/aws/Types/rds-db-subnet-group.md | 27 + .../sources/aws/Types/rds-option-group.md | 17 + .../sources/aws/Types/route53-health-check.md | 23 + .../sources/aws/Types/route53-hosted-zone.md | 25 + .../aws/Types/route53-resource-record-set.md | 27 + .../docs/sources/aws/Types/s3-bucket.md | 55 + .../aws/Types/sns-data-protection-policy.md | 22 + .../docs/sources/aws/Types/sns-endpoint.md | 12 + .../aws/Types/sns-platform-application.md | 23 + .../sources/aws/Types/sns-subscription.md | 26 + .../docs/sources/aws/Types/sns-topic.md | 22 + .../docs/sources/aws/Types/sqs-queue.md | 27 + .../docs/sources/aws/Types/ssm-parameter.md | 31 + .../docs/sources/aws/_category_.json | 9 + .../docs/sources/aws/account_settings.png | Bin 0 -> 45804 bytes .../docs/sources/aws/aws_manual.png | Bin 0 -> 1896097 bytes .../docs/sources/aws/aws_source_settings.png | Bin 0 -> 116537 bytes .../aws/cloudformation-update-stack.png | Bin 0 -> 412841 bytes .../docs/sources/aws/configuration.md | 118 + .../docs/sources/aws/configure-aws.png | Bin 0 -> 56460 bytes .../sources/aws/data/apigateway-api-key.json | 18 + .../aws/data/apigateway-authorizer.json | 16 + .../aws/data/apigateway-deployment.json | 16 + .../aws/data/apigateway-domain-name.json | 19 + .../aws/data/apigateway-integration.json | 11 + .../aws/data/apigateway-method-response.json | 11 + .../sources/aws/data/apigateway-method.json | 17 + .../sources/aws/data/apigateway-model.json | 16 + .../sources/aws/data/apigateway-resource.json | 17 + .../sources/aws/data/apigateway-rest-api.json | 19 + .../sources/aws/data/apigateway-stage.json | 17 + .../data/autoscaling-auto-scaling-group.json | 27 + .../aws/data/cloudfront-cache-policy.json | 18 + ...oudfront-continuous-deployment-policy.json | 14 + .../aws/data/cloudfront-distribution.json | 33 + .../sources/aws/data/cloudfront-function.json | 18 + .../aws/data/cloudfront-key-group.json | 18 + .../cloudfront-origin-access-control.json | 18 + .../cloudfront-origin-request-policy.json | 18 + .../data/cloudfront-realtime-log-config.json | 19 + .../cloudfront-response-headers-policy.json | 18 + .../cloudfront-streaming-distribution.json | 23 + .../sources/aws/data/cloudwatch-alarm.json | 19 + .../aws/data/directconnect-connection.json | 24 + .../data/directconnect-customer-metadata.json | 13 + ...-connect-gateway-association-proposal.json | 19 + ...ct-direct-connect-gateway-association.json | 17 + ...ect-direct-connect-gateway-attachment.json | 15 + .../directconnect-direct-connect-gateway.json | 18 + .../data/directconnect-hosted-connection.json | 22 + .../aws/data/directconnect-interconnect.json | 19 + .../sources/aws/data/directconnect-lag.json | 23 + .../aws/data/directconnect-location.json | 18 + .../directconnect-router-configuration.json | 17 + .../data/directconnect-virtual-gateway.json | 13 + .../data/directconnect-virtual-interface.json | 31 + .../sources/aws/data/dynamodb-backup.json | 12 + .../docs/sources/aws/data/dynamodb-table.json | 25 + .../docs/sources/aws/data/ec2-address.json | 22 + .../data/ec2-capacity-reservation-fleet.json | 14 + .../aws/data/ec2-capacity-reservation.json | 23 + .../ec2-egress-only-internet-gateway.json | 19 + .../ec2-iam-instance-profile-association.json | 14 + .../docs/sources/aws/data/ec2-image.json | 18 + .../aws/data/ec2-instance-event-window.json | 14 + .../sources/aws/data/ec2-instance-status.json | 13 + .../docs/sources/aws/data/ec2-instance.json | 41 + .../aws/data/ec2-internet-gateway.json | 19 + .../docs/sources/aws/data/ec2-key-pair.json | 18 + .../aws/data/ec2-launch-template-version.json | 25 + .../sources/aws/data/ec2-launch-template.json | 18 + .../sources/aws/data/ec2-nat-gateway.json | 19 + .../sources/aws/data/ec2-network-acl.json | 19 + .../ec2-network-interface-permission.json | 14 + .../aws/data/ec2-network-interface.json | 26 + .../sources/aws/data/ec2-placement-group.json | 18 + .../aws/data/ec2-reserved-instance.json | 13 + .../sources/aws/data/ec2-route-table.json | 41 + .../aws/data/ec2-security-group-rule.json | 25 + .../sources/aws/data/ec2-security-group.json | 22 + .../docs/sources/aws/data/ec2-snapshot.json | 14 + .../docs/sources/aws/data/ec2-subnet.json | 22 + .../sources/aws/data/ec2-volume-status.json | 14 + .../docs/sources/aws/data/ec2-volume.json | 19 + .../sources/aws/data/ec2-vpc-endpoint.json | 18 + .../aws/data/ec2-vpc-peering-connection.json | 25 + .../docs/sources/aws/data/ec2-vpc.json | 16 + .../aws/data/ecs-capacity-provider.json | 20 + .../docs/sources/aws/data/ecs-cluster.json | 25 + .../aws/data/ecs-container-instance.json | 12 + .../docs/sources/aws/data/ecs-service.json | 27 + .../sources/aws/data/ecs-task-definition.json | 19 + .../docs/sources/aws/data/ecs-task.json | 18 + .../sources/aws/data/efs-access-point.json | 18 + .../sources/aws/data/efs-backup-policy.json | 16 + .../sources/aws/data/efs-file-system.json | 18 + .../sources/aws/data/efs-mount-target.json | 16 + .../data/efs-replication-configuration.json | 18 + .../docs/sources/aws/data/eks-addon.json | 16 + .../docs/sources/aws/data/eks-cluster.json | 19 + .../sources/aws/data/eks-fargate-profile.json | 17 + .../docs/sources/aws/data/eks-nodegroup.json | 23 + .../sources/aws/data/elb-instance-health.json | 12 + .../sources/aws/data/elb-load-balancer.json | 28 + .../docs/sources/aws/data/elbv2-listener.json | 27 + .../sources/aws/data/elbv2-load-balancer.json | 34 + .../docs/sources/aws/data/elbv2-rule.json | 19 + .../sources/aws/data/elbv2-target-group.json | 24 + .../sources/aws/data/elbv2-target-health.json | 17 + .../docs/sources/aws/data/iam-group.json | 19 + .../aws/data/iam-instance-profile.json | 20 + .../docs/sources/aws/data/iam-policy.json | 24 + .../docs/sources/aws/data/iam-role.json | 20 + .../docs/sources/aws/data/iam-user.json | 23 + .../docs/sources/aws/data/kms-alias.json | 19 + .../aws/data/kms-custom-key-store.json | 19 + .../docs/sources/aws/data/kms-grant.json | 17 + .../docs/sources/aws/data/kms-key-policy.json | 17 + .../docs/sources/aws/data/kms-key.json | 19 + .../aws/data/lambda-event-source-mapping.json | 28 + .../sources/aws/data/lambda-function.json | 35 + .../aws/data/lambda-layer-version.json | 17 + .../docs/sources/aws/data/lambda-layer.json | 10 + .../network-firewall-firewall-policy.json | 23 + .../aws/data/network-firewall-firewall.json | 28 + .../aws/data/network-firewall-rule-group.json | 19 + ...firewall-tls-inspection-configuration.json | 19 + .../networkmanager-connect-attachment.json | 14 + ...tworkmanager-connect-peer-association.json | 19 + .../aws/data/networkmanager-connect-peer.json | 21 + .../aws/data/networkmanager-connection.json | 22 + .../networkmanager-core-network-policy.json | 15 + .../aws/data/networkmanager-core-network.json | 18 + .../aws/data/networkmanager-device.json | 24 + .../data/networkmanager-global-network.json | 29 + .../data/networkmanager-link-association.json | 16 + .../sources/aws/data/networkmanager-link.json | 23 + ...manager-network-resource-relationship.json | 19 + ...rkmanager-site-to-site-vpn-attachment.json | 15 + .../sources/aws/data/networkmanager-site.json | 22 + ...nsit-gateway-connect-peer-association.json | 18 + ...etworkmanager-transit-gateway-peering.json | 19 + ...kmanager-transit-gateway-registration.json | 14 + ...ransit-gateway-route-table-attachment.json | 19 + .../data/networkmanager-vpc-attachment.json | 15 + .../data/rds-db-cluster-parameter-group.json | 19 + .../docs/sources/aws/data/rds-db-cluster.json | 30 + .../sources/aws/data/rds-db-instance.json | 37 + .../aws/data/rds-db-parameter-group.json | 19 + .../sources/aws/data/rds-db-subnet-group.json | 20 + .../sources/aws/data/rds-option-group.json | 19 + .../aws/data/route53-health-check.json | 19 + .../sources/aws/data/route53-hosted-zone.json | 25 + .../aws/data/route53-resource-record-set.json | 22 + .../docs/sources/aws/data/s3-bucket.json | 82 + .../aws/data/sns-data-protection-policy.json | 17 + .../docs/sources/aws/data/sns-endpoint.json | 11 + .../aws/data/sns-platform-application.json | 19 + .../sources/aws/data/sns-subscription.json | 19 + .../docs/sources/aws/data/sns-topic.json | 19 + .../docs/sources/aws/data/sqs-queue.json | 19 + .../docs/sources/aws/data/ssm-parameter.json | 23 + .../sources/aws/update-to-pod-identity.md | 177 + .../gcp-ai-platform-batch-prediction-job.md | 34 + .../gcp/Types/gcp-ai-platform-custom-job.md | 35 + .../gcp/Types/gcp-ai-platform-endpoint.md | 35 + ...latform-model-deployment-monitoring-job.md | 32 + .../gcp/Types/gcp-ai-platform-model.md | 31 + .../gcp/Types/gcp-ai-platform-pipeline-job.md | 31 + .../gcp-artifact-registry-docker-image.md | 17 + ...big-query-data-transfer-transfer-config.md | 31 + .../gcp/Types/gcp-big-query-dataset.md | 39 + .../sources/gcp/Types/gcp-big-query-model.md | 26 + .../gcp/Types/gcp-big-query-routine.md | 22 + .../sources/gcp/Types/gcp-big-query-table.md | 26 + .../Types/gcp-big-table-admin-app-profile.md | 27 + .../gcp/Types/gcp-big-table-admin-backup.md | 27 + .../gcp/Types/gcp-big-table-admin-cluster.md | 23 + .../gcp/Types/gcp-big-table-admin-instance.md | 24 + .../gcp/Types/gcp-big-table-admin-table.md | 30 + .../Types/gcp-cloud-billing-billing-info.md | 20 + .../gcp/Types/gcp-cloud-build-build.md | 31 + .../gcp/Types/gcp-cloud-functions-function.md | 34 + .../Types/gcp-cloud-kms-crypto-key-version.md | 30 + .../gcp/Types/gcp-cloud-kms-crypto-key.md | 19 + .../gcp/Types/gcp-cloud-kms-key-ring.md | 22 + .../gcp-cloud-resource-manager-project.md | 20 + .../gcp-cloud-resource-manager-tag-value.md | 16 + .../sources/gcp/Types/gcp-compute-address.md | 31 + .../gcp/Types/gcp-compute-autoscaler.md | 17 + .../gcp/Types/gcp-compute-backend-service.md | 29 + .../sources/gcp/Types/gcp-compute-disk.md | 39 + .../Types/gcp-compute-external-vpn-gateway.md | 17 + .../sources/gcp/Types/gcp-compute-firewall.md | 26 + .../gcp/Types/gcp-compute-forwarding-rule.md | 31 + .../gcp/Types/gcp-compute-global-address.md | 23 + .../gcp-compute-global-forwarding-rule.md | 31 + .../gcp/Types/gcp-compute-health-check.md | 17 + .../Types/gcp-compute-http-health-check.md | 18 + .../sources/gcp/Types/gcp-compute-image.md | 17 + .../gcp-compute-instance-group-manager.md | 34 + .../gcp/Types/gcp-compute-instance-group.md | 27 + .../Types/gcp-compute-instance-template.md | 63 + .../sources/gcp/Types/gcp-compute-instance.md | 30 + .../gcp/Types/gcp-compute-instant-snapshot.md | 23 + .../gcp/Types/gcp-compute-machine-image.md | 35 + .../gcp-compute-network-endpoint-group.md | 34 + .../sources/gcp/Types/gcp-compute-network.md | 27 + .../gcp/Types/gcp-compute-node-group.md | 18 + .../sources/gcp/Types/gcp-compute-project.md | 23 + .../gcp-compute-public-delegated-prefix.md | 27 + .../gcp-compute-region-backend-service.md | 31 + .../Types/gcp-compute-region-commitment.md | 22 + .../gcp/Types/gcp-compute-reservation.md | 22 + .../sources/gcp/Types/gcp-compute-route.md | 31 + .../sources/gcp/Types/gcp-compute-router.md | 31 + .../gcp/Types/gcp-compute-security-policy.md | 18 + .../sources/gcp/Types/gcp-compute-snapshot.md | 27 + .../gcp/Types/gcp-compute-ssl-certificate.md | 17 + .../gcp/Types/gcp-compute-ssl-policy.md | 17 + .../gcp/Types/gcp-compute-subnetwork.md | 22 + .../Types/gcp-compute-target-http-proxy.md | 17 + .../Types/gcp-compute-target-https-proxy.md | 31 + .../gcp/Types/gcp-compute-target-pool.md | 30 + .../sources/gcp/Types/gcp-compute-url-map.md | 23 + .../gcp/Types/gcp-compute-vpn-gateway.md | 23 + .../gcp/Types/gcp-compute-vpn-tunnel.md | 31 + .../gcp/Types/gcp-container-cluster.md | 47 + .../gcp/Types/gcp-container-node-pool.md | 35 + .../gcp/Types/gcp-dataform-repository.md | 31 + .../gcp/Types/gcp-dataplex-aspect-type.md | 17 + .../gcp/Types/gcp-dataplex-data-scan.md | 22 + .../gcp/Types/gcp-dataplex-entry-group.md | 17 + .../Types/gcp-dataproc-autoscaling-policy.md | 16 + .../sources/gcp/Types/gcp-dataproc-cluster.md | 54 + .../sources/gcp/Types/gcp-dns-managed-zone.md | 27 + .../Types/gcp-essential-contacts-contact.md | 17 + .../sources/gcp/Types/gcp-file-instance.md | 27 + .../docs/sources/gcp/Types/gcp-iam-role.md | 13 + .../gcp/Types/gcp-iam-service-account-key.md | 23 + .../gcp/Types/gcp-iam-service-account.md | 27 + .../sources/gcp/Types/gcp-logging-bucket.md | 23 + .../sources/gcp/Types/gcp-logging-link.md | 26 + .../gcp/Types/gcp-logging-saved-query.md | 13 + .../sources/gcp/Types/gcp-logging-sink.md | 31 + .../gcp/Types/gcp-monitoring-alert-policy.md | 23 + .../Types/gcp-monitoring-custom-dashboard.md | 17 + .../gcp-monitoring-notification-channel.md | 17 + .../sources/gcp/Types/gcp-orgpolicy-policy.md | 17 + .../gcp/Types/gcp-pub-sub-subscription.md | 35 + .../sources/gcp/Types/gcp-pub-sub-topic.md | 26 + .../sources/gcp/Types/gcp-redis-instance.md | 31 + .../sources/gcp/Types/gcp-run-revision.md | 46 + .../docs/sources/gcp/Types/gcp-run-service.md | 54 + .../gcp/Types/gcp-secret-manager-secret.md | 26 + ...nter-management-security-center-service.md | 13 + .../Types/gcp-service-directory-endpoint.md | 23 + .../gcp/Types/gcp-service-usage-service.md | 20 + .../sources/gcp/Types/gcp-spanner-database.md | 28 + .../sources/gcp/Types/gcp-spanner-instance.md | 23 + .../gcp/Types/gcp-sql-admin-backup-run.md | 22 + .../sources/gcp/Types/gcp-sql-admin-backup.md | 28 + .../gcp/Types/gcp-sql-admin-instance.md | 42 + .../sources/gcp/Types/gcp-storage-bucket.md | 30 + .../gcp-storage-transfer-transfer-job.md | 34 + .../docs/sources/gcp/_category_.json | 9 + .../docs/sources/gcp/configuration.md | 439 + .../gcp-ai-platform-batch-prediction-job.json | 18 + .../gcp/data/gcp-ai-platform-custom-job.json | 18 + .../gcp/data/gcp-ai-platform-endpoint.json | 18 + ...tform-model-deployment-monitoring-job.json | 17 + .../gcp/data/gcp-ai-platform-model.json | 17 + .../data/gcp-ai-platform-pipeline-job.json | 17 + .../gcp-artifact-registry-docker-image.json | 17 + ...g-query-data-transfer-transfer-config.json | 22 + .../gcp/data/gcp-big-query-dataset.json | 24 + .../sources/gcp/data/gcp-big-query-model.json | 16 + .../gcp/data/gcp-big-query-routine.json | 17 + .../sources/gcp/data/gcp-big-query-table.json | 17 + .../data/gcp-big-table-admin-app-profile.json | 21 + .../gcp/data/gcp-big-table-admin-backup.json | 15 + .../gcp/data/gcp-big-table-admin-cluster.json | 15 + .../data/gcp-big-table-admin-instance.json | 17 + .../gcp/data/gcp-big-table-admin-table.json | 22 + .../data/gcp-cloud-billing-billing-info.json | 10 + .../gcp/data/gcp-cloud-build-build.json | 17 + .../data/gcp-cloud-functions-function.json | 18 + .../gcp/data/gcp-cloud-kms-crypto-key.json | 12 + .../gcp/data/gcp-cloud-kms-key-ring.json | 17 + .../gcp-cloud-resource-manager-project.json | 9 + .../gcp-cloud-resource-manager-tag-value.json | 16 + .../sources/gcp/data/gcp-compute-address.json | 21 + .../gcp/data/gcp-compute-autoscaler.json | 17 + .../gcp/data/gcp-compute-backend-service.json | 17 + .../sources/gcp/data/gcp-compute-disk.json | 23 + .../gcp-compute-external-vpn-gateway.json | 16 + .../gcp/data/gcp-compute-firewall.json | 17 + .../gcp/data/gcp-compute-forwarding-rule.json | 21 + .../gcp/data/gcp-compute-global-address.json | 17 + .../gcp-compute-global-forwarding-rule.json | 21 + .../gcp/data/gcp-compute-health-check.json | 16 + .../data/gcp-compute-http-health-check.json | 16 + .../sources/gcp/data/gcp-compute-image.json | 16 + .../gcp-compute-instance-group-manager.json | 22 + .../gcp/data/gcp-compute-instance-group.json | 17 + .../data/gcp-compute-instance-template.json | 29 + .../gcp/data/gcp-compute-instance.json | 21 + .../data/gcp-compute-instant-snapshot.json | 17 + .../gcp/data/gcp-compute-machine-image.json | 22 + .../gcp-compute-network-endpoint-group.json | 22 + .../sources/gcp/data/gcp-compute-network.json | 17 + .../gcp/data/gcp-compute-node-group.json | 22 + .../sources/gcp/data/gcp-compute-project.json | 10 + .../gcp-compute-public-delegated-prefix.json | 23 + .../gcp-compute-region-backend-service.json | 21 + .../data/gcp-compute-region-commitment.json | 16 + .../gcp/data/gcp-compute-reservation.json | 19 + .../sources/gcp/data/gcp-compute-route.json | 21 + .../sources/gcp/data/gcp-compute-router.json | 24 + .../gcp/data/gcp-compute-security-policy.json | 16 + .../gcp/data/gcp-compute-snapshot.json | 17 + .../gcp/data/gcp-compute-ssl-certificate.json | 16 + .../gcp/data/gcp-compute-ssl-policy.json | 16 + .../gcp/data/gcp-compute-subnetwork.json | 17 + .../data/gcp-compute-target-http-proxy.json | 17 + .../data/gcp-compute-target-https-proxy.json | 21 + .../gcp/data/gcp-compute-target-pool.json | 24 + .../sources/gcp/data/gcp-compute-url-map.json | 17 + .../gcp/data/gcp-compute-vpn-gateway.json | 17 + .../gcp/data/gcp-compute-vpn-tunnel.json | 21 + .../gcp/data/gcp-container-cluster.json | 26 + .../gcp/data/gcp-container-node-pool.json | 23 + .../gcp/data/gcp-dataform-repository.json | 22 + .../gcp/data/gcp-dataplex-aspect-type.json | 17 + .../gcp/data/gcp-dataplex-data-scan.json | 18 + .../gcp/data/gcp-dataplex-entry-group.json | 17 + .../data/gcp-dataproc-autoscaling-policy.json | 16 + .../gcp/data/gcp-dataproc-cluster.json | 27 + .../gcp/data/gcp-dns-managed-zone.json | 17 + .../data/gcp-essential-contacts-contact.json | 18 + .../sources/gcp/data/gcp-file-instance.json | 18 + .../docs/sources/gcp/data/gcp-iam-role.json | 11 + .../gcp/data/gcp-iam-service-account-key.json | 17 + .../gcp/data/gcp-iam-service-account.json | 23 + .../sources/gcp/data/gcp-logging-bucket.json | 12 + .../sources/gcp/data/gcp-logging-link.json | 12 + .../gcp/data/gcp-logging-saved-query.json | 11 + .../sources/gcp/data/gcp-logging-sink.json | 17 + .../gcp/data/gcp-monitoring-alert-policy.json | 20 + .../data/gcp-monitoring-custom-dashboard.json | 19 + .../gcp-monitoring-notification-channel.json | 18 + .../gcp/data/gcp-orgpolicy-policy.json | 19 + .../gcp/data/gcp-pub-sub-subscription.json | 22 + .../sources/gcp/data/gcp-pub-sub-topic.json | 17 + .../sources/gcp/data/gcp-redis-instance.json | 22 + .../sources/gcp/data/gcp-run-revision.json | 21 + .../sources/gcp/data/gcp-run-service.json | 28 + .../gcp/data/gcp-secret-manager-secret.json | 17 + ...er-management-security-center-service.json | 11 + .../data/gcp-service-directory-endpoint.json | 18 + .../gcp/data/gcp-service-usage-service.json | 12 + .../gcp/data/gcp-spanner-database.json | 17 + .../gcp/data/gcp-spanner-instance.json | 17 + .../gcp/data/gcp-sql-admin-backup-run.json | 12 + .../gcp/data/gcp-sql-admin-backup.json | 16 + .../gcp/data/gcp-sql-admin-instance.json | 24 + .../sources/gcp/data/gcp-storage-bucket.json | 21 + .../gcp-storage-transfer-transfer-job.json | 22 + .../docs/sources/k8s/Types/ClusterRole.md | 17 + .../sources/k8s/Types/ClusterRoleBinding.md | 28 + .../docs/sources/k8s/Types/ConfigMap.md | 17 + .../docs/sources/k8s/Types/CronJob.md | 17 + .../docs/sources/k8s/Types/DaemonSet.md | 18 + .../docs/sources/k8s/Types/Deployment.md | 24 + .../docs/sources/k8s/Types/EndpointSlice.md | 36 + .../docs/sources/k8s/Types/Endpoints.md | 31 + .../k8s/Types/HorizontalPodAutoscaler.md | 16 + .../docs/sources/k8s/Types/Ingress.md | 27 + .../docs/sources/k8s/Types/Job.md | 23 + .../docs/sources/k8s/Types/LimitRange.md | 17 + .../docs/sources/k8s/Types/NetworkPolicy.md | 24 + .../docs/sources/k8s/Types/Node.md | 30 + .../sources/k8s/Types/PersistentVolume.md | 32 + .../k8s/Types/PersistentVolumeClaim.md | 24 + .../docs/sources/k8s/Types/Pod.md | 51 + .../sources/k8s/Types/PodDisruptionBudget.md | 23 + .../docs/sources/k8s/Types/PriorityClass.md | 17 + .../docs/sources/k8s/Types/ReplicaSet.md | 19 + .../k8s/Types/ReplicationController.md | 23 + .../docs/sources/k8s/Types/ResourceQuota.md | 18 + .../docs/sources/k8s/Types/Role.md | 18 + .../docs/sources/k8s/Types/RoleBinding.md | 31 + .../docs/sources/k8s/Types/Secret.md | 17 + .../docs/sources/k8s/Types/Service.md | 32 + .../docs/sources/k8s/Types/ServiceAccount.md | 23 + .../docs/sources/k8s/Types/StatefulSet.md | 18 + .../docs/sources/k8s/Types/StorageClass.md | 18 + .../sources/k8s/Types/VolumeAttachment.md | 23 + .../docs/sources/k8s/_category_.json | 9 + .../docs/sources/k8s/account_settings.png | Bin 0 -> 49931 bytes .../docs/sources/k8s/api_key.png | Bin 0 -> 74421 bytes .../docs/sources/k8s/configuration.md | 152 + .../docs/sources/k8s/data/ClusterRole.json | 18 + .../sources/k8s/data/ClusterRoleBinding.json | 22 + .../docs/sources/k8s/data/ConfigMap.json | 21 + .../docs/sources/k8s/data/CronJob.json | 21 + .../docs/sources/k8s/data/DaemonSet.json | 21 + .../docs/sources/k8s/data/Deployment.json | 22 + .../docs/sources/k8s/data/EndpointSlice.json | 22 + .../docs/sources/k8s/data/Endpoints.json | 22 + .../k8s/data/HorizontalPodAutoscaler.json | 18 + .../docs/sources/k8s/data/Ingress.json | 19 + .../docs/sources/k8s/data/Job.json | 22 + .../docs/sources/k8s/data/LimitRange.json | 21 + .../docs/sources/k8s/data/NetworkPolicy.json | 14 + .../docs/sources/k8s/data/Node.json | 19 + .../sources/k8s/data/PersistentVolume.json | 22 + .../k8s/data/PersistentVolumeClaim.json | 22 + .../docs/sources/k8s/data/Pod.json | 31 + .../sources/k8s/data/PodDisruptionBudget.json | 19 + .../docs/sources/k8s/data/PriorityClass.json | 21 + .../docs/sources/k8s/data/ReplicaSet.json | 14 + .../k8s/data/ReplicationController.json | 22 + .../docs/sources/k8s/data/ResourceQuota.json | 21 + .../docs/sources/k8s/data/Role.json | 21 + .../docs/sources/k8s/data/RoleBinding.json | 22 + .../docs/sources/k8s/data/Secret.json | 21 + .../docs/sources/k8s/data/Service.json | 22 + .../docs/sources/k8s/data/ServiceAccount.json | 22 + .../docs/sources/k8s/data/StatefulSet.json | 21 + .../docs/sources/k8s/data/StorageClass.json | 21 + .../sources/k8s/data/VolumeAttachment.json | 14 + .../docs/sources/stdlib/Types/certificate.md | 13 + .../docs/sources/stdlib/Types/dns.md | 27 + .../docs/sources/stdlib/Types/http.md | 31 + .../docs/sources/stdlib/Types/ip.md | 23 + .../docs/sources/stdlib/Types/rdap-asn.md | 18 + .../docs/sources/stdlib/Types/rdap-domain.md | 31 + .../docs/sources/stdlib/Types/rdap-entity.md | 20 + .../sources/stdlib/Types/rdap-ip-network.md | 19 + .../sources/stdlib/Types/rdap-nameserver.md | 28 + .../docs/sources/stdlib/_category_json | 9 + .../docs/sources/stdlib/data/certificate.json | 9 + .../docs/sources/stdlib/data/dns.json | 12 + .../docs/sources/stdlib/data/http.json | 12 + .../docs/sources/stdlib/data/ip.json | 10 + .../docs/sources/stdlib/data/rdap-asn.json | 10 + .../docs/sources/stdlib/data/rdap-domain.json | 15 + .../docs/sources/stdlib/data/rdap-entity.json | 12 + .../sources/stdlib/data/rdap-ip-network.json | 10 + .../sources/stdlib/data/rdap-nameserver.json | 10 + go.mod | 196 +- go.sum | 745 ++ k8s-source/adapters/clusterrole.go | 6 +- k8s-source/adapters/clusterrole_test.go | 2 +- k8s-source/adapters/clusterrolebinding.go | 6 +- .../adapters/clusterrolebinding_test.go | 4 +- k8s-source/adapters/configmap.go | 6 +- k8s-source/adapters/configmap_test.go | 4 +- k8s-source/adapters/cronjob.go | 6 +- k8s-source/adapters/cronjob_test.go | 2 +- k8s-source/adapters/daemonset.go | 6 +- k8s-source/adapters/daemonset_test.go | 2 +- k8s-source/adapters/deployment.go | 6 +- k8s-source/adapters/deployment_test.go | 4 +- k8s-source/adapters/endpoints.go | 6 +- k8s-source/adapters/endpoints_test.go | 4 +- k8s-source/adapters/endpointslice.go | 6 +- k8s-source/adapters/endpointslice_test.go | 4 +- k8s-source/adapters/generic_source.go | 4 +- k8s-source/adapters/generic_source_test.go | 6 +- .../adapters/horizontalpodautoscaler.go | 6 +- .../adapters/horizontalpodautoscaler_test.go | 4 +- k8s-source/adapters/ingress.go | 6 +- k8s-source/adapters/ingress_test.go | 4 +- k8s-source/adapters/job.go | 6 +- k8s-source/adapters/job_test.go | 4 +- k8s-source/adapters/limitrange.go | 6 +- k8s-source/adapters/limitrange_test.go | 2 +- k8s-source/adapters/main.go | 4 +- k8s-source/adapters/networkpolicy.go | 6 +- k8s-source/adapters/networkpolicy_test.go | 4 +- k8s-source/adapters/node.go | 6 +- k8s-source/adapters/node_test.go | 4 +- k8s-source/adapters/persistentvolume.go | 6 +- k8s-source/adapters/persistentvolume_test.go | 2 +- k8s-source/adapters/persistentvolumeclaim.go | 6 +- .../adapters/persistentvolumeclaim_test.go | 4 +- k8s-source/adapters/poddisruptionbudget.go | 6 +- .../adapters/poddisruptionbudget_test.go | 4 +- k8s-source/adapters/pods.go | 6 +- k8s-source/adapters/pods_test.go | 4 +- k8s-source/adapters/priorityclass.go | 6 +- k8s-source/adapters/priorityclass_test.go | 2 +- k8s-source/adapters/replicaset.go | 6 +- k8s-source/adapters/replicaset_test.go | 4 +- k8s-source/adapters/replicationcontroller.go | 6 +- .../adapters/replicationcontroller_test.go | 4 +- k8s-source/adapters/resourcequota.go | 6 +- k8s-source/adapters/resourcequota_test.go | 2 +- k8s-source/adapters/role.go | 6 +- k8s-source/adapters/role_test.go | 2 +- k8s-source/adapters/rolebinding.go | 6 +- k8s-source/adapters/rolebinding_test.go | 4 +- k8s-source/adapters/secret.go | 6 +- k8s-source/adapters/secret_test.go | 2 +- k8s-source/adapters/service.go | 6 +- k8s-source/adapters/service_test.go | 4 +- k8s-source/adapters/serviceaccount.go | 6 +- k8s-source/adapters/serviceaccount_test.go | 4 +- k8s-source/adapters/shared_util.go | 2 +- k8s-source/adapters/statefulset.go | 6 +- k8s-source/adapters/statefulset_test.go | 4 +- k8s-source/adapters/storageclass.go | 6 +- k8s-source/adapters/storageclass_test.go | 2 +- k8s-source/adapters/volumeattachment.go | 6 +- k8s-source/adapters/volumeattachment_test.go | 4 +- k8s-source/build/package/Dockerfile | 2 +- k8s-source/cmd/root.go | 8 +- logging/logging.go | 53 - logging/logging_test.go | 86 - sdp-go/.gitignore | 2 - sdp-go/account.go | 19 - sdp-go/account.pb.go | 4649 ----------- sdp-go/apikey.go | 11 - sdp-go/apikeys.pb.go | 1111 --- sdp-go/area51.pb.go | 336 - sdp-go/auth0support.pb.go | 258 - sdp-go/bookmarks.go | 23 - sdp-go/bookmarks.pb.go | 886 -- sdp-go/cached_entry.pb.go | 188 - sdp-go/changes.go | 455 -- sdp-go/changes.pb.go | 7252 ----------------- sdp-go/changes_test.go | 474 -- sdp-go/changetimeline.go | 164 - sdp-go/changetimeline_test.go | 154 - sdp-go/cli.pb.go | 270 - sdp-go/compare.go | 41 - sdp-go/config.pb.go | 1815 ----- sdp-go/connection.go | 180 - sdp-go/connection_test.go | 27 - sdp-go/encoder_test.go | 175 - sdp-go/errors.go | 54 - sdp-go/gateway.go | 190 - sdp-go/gateway.pb.go | 2336 ------ sdp-go/gateway_test.go | 120 - sdp-go/genhandler.go | 108 - sdp-go/graph/main.go | 362 - sdp-go/graph/main_test.go | 300 - sdp-go/handler_cancelquery.go | 56 - sdp-go/handler_gatewayresponse.go | 56 - sdp-go/handler_natsgetlogrecordsrequest.go | 56 - sdp-go/handler_natsgetlogrecordsresponse.go | 56 - sdp-go/handler_query.go | 56 - sdp-go/handler_queryresponse.go | 56 - sdp-go/instance_detect.go | 91 - sdp-go/invites.pb.go | 546 -- sdp-go/items.go | 659 -- sdp-go/items.pb.go | 1840 ----- sdp-go/items_test.go | 695 -- sdp-go/link_extract.go | 247 - sdp-go/link_extract_test.go | 795 -- sdp-go/logs.go | 77 - sdp-go/logs.pb.go | 1083 --- sdp-go/logs_test.go | 155 - sdp-go/progress.go | 721 -- sdp-go/progress_test.go | 1525 ---- sdp-go/proto_clone_test.go | 146 - sdp-go/responses.go | 27 - sdp-go/responses.pb.go | 246 - sdp-go/revlink.pb.go | 447 - sdp-go/sdpconnect/account.connect.go | 1187 --- sdp-go/sdpconnect/apikeys.connect.go | 298 - sdp-go/sdpconnect/area51.connect.go | 113 - sdp-go/sdpconnect/auth0support.connect.go | 145 - sdp-go/sdpconnect/bookmarks.connect.go | 262 - sdp-go/sdpconnect/changes.connect.go | 1132 --- sdp-go/sdpconnect/cli.connect.go | 136 - sdp-go/sdpconnect/config.connect.go | 399 - sdp-go/sdpconnect/invites.connect.go | 196 - sdp-go/sdpconnect/logs.connect.go | 117 - sdp-go/sdpconnect/revlink.connect.go | 189 - sdp-go/sdpconnect/signal.connect.go | 330 - sdp-go/sdpconnect/snapshots.connect.go | 254 - sdp-go/sdpws/client.go | 525 -- sdp-go/sdpws/client_test.go | 1064 --- sdp-go/sdpws/messagehandler.go | 191 - sdp-go/sdpws/utils.go | 343 - sdp-go/signal.pb.go | 1286 --- sdp-go/signals.go | 10 - sdp-go/snapshots.go | 37 - sdp-go/snapshots.pb.go | 1011 --- sdp-go/test_utils.go | 240 - sdp-go/test_utils_test.go | 143 - sdp-go/tracing.go | 106 - sdp-go/tracing_test.go | 72 - sdp-go/util.go | 156 - sdp-go/util.pb.go | 285 - sdp-go/util_test.go | 113 - sdp-go/validation.go | 148 - sdp-go/validation_test.go | 924 --- sdpcache/bolt_cache.go | 1505 ---- sdpcache/cache.go | 1009 --- sdpcache/cache_benchmark_test.go | 774 -- sdpcache/cache_stuck_test.go | 377 - sdpcache/cache_test.go | 2156 ----- sdpcache/item_generator_test.go | 135 - sdpcache/pending.go | 114 - sources/aws/apigateway-api-key.go | 2 +- sources/aws/apigateway-stage.go | 2 +- sources/aws/base.go | 2 +- sources/aws/errors.go | 2 +- sources/aws/validation_test.go | 4 +- sources/azure/build/package/Dockerfile | 2 +- sources/azure/clients/disk-accesses-client.go | 36 + sources/azure/cmd/root.go | 6 +- .../authorization-role-assignment_test.go | 6 +- .../batch-batch-accounts_test.go | 6 +- .../compute-availability-set_test.go | 6 +- .../compute-disk-access_test.go | 367 + .../compute-disk-encryption-set_test.go | 6 +- .../integration-tests/compute-disk_test.go | 6 +- .../integration-tests/compute-image_test.go | 6 +- .../compute-proximity-placement-group_test.go | 6 +- .../compute-virtual-machine-extension_test.go | 6 +- ...ompute-virtual-machine-run-command_test.go | 6 +- .../compute-virtual-machine-scale-set_test.go | 6 +- .../compute-virtual-machine_test.go | 6 +- .../dbforpostgresql-database_test.go | 6 +- .../dbforpostgresql-flexible-server_test.go | 6 +- .../documentdb-database-accounts_test.go | 4 +- .../keyvault-managed-hsm_test.go | 6 +- .../integration-tests/keyvault-secret_test.go | 4 +- .../integration-tests/keyvault-vault_test.go | 4 +- ...gedidentity-user-assigned-identity_test.go | 6 +- .../network-application-gateway_test.go | 6 +- .../network-load-balancer_test.go | 4 +- .../network-network-interface_test.go | 4 +- .../network-network-security-group_test.go | 6 +- .../network-public-ip-address_test.go | 6 +- .../network-route-table_test.go | 6 +- .../network-virtual-network_test.go | 6 +- .../integration-tests/network-zone_test.go | 6 +- .../integration-tests/sql-database_test.go | 6 +- .../integration-tests/sql-server_test.go | 6 +- .../integration-tests/storage-account_test.go | 6 +- .../storage-blob-container_test.go | 4 +- .../storage-fileshare_test.go | 4 +- .../integration-tests/storage-queues_test.go | 4 +- .../integration-tests/storage-table_test.go | 4 +- .../rules/azure-manual-adapter-creation.mdc | 6 +- sources/azure/manual/adapters.go | 13 +- .../manual/authorization-role-assignment.go | 6 +- .../authorization-role-assignment_test.go | 6 +- sources/azure/manual/batch-batch-accounts.go | 6 +- .../azure/manual/batch-batch-accounts_test.go | 6 +- .../azure/manual/compute-availability-set.go | 6 +- .../manual/compute-availability-set_test.go | 6 +- sources/azure/manual/compute-disk-access.go | 193 + .../azure/manual/compute-disk-access_test.go | 441 + .../manual/compute-disk-encryption-set.go | 6 +- .../compute-disk-encryption-set_test.go | 6 +- sources/azure/manual/compute-disk.go | 6 +- sources/azure/manual/compute-disk_test.go | 6 +- sources/azure/manual/compute-image.go | 6 +- sources/azure/manual/compute-image_test.go | 6 +- .../compute-proximity-placement-group.go | 6 +- .../compute-proximity-placement-group_test.go | 6 +- .../compute-virtual-machine-extension.go | 2 +- .../compute-virtual-machine-extension_test.go | 6 +- .../compute-virtual-machine-run-command.go | 2 +- ...ompute-virtual-machine-run-command_test.go | 6 +- .../compute-virtual-machine-scale-set.go | 6 +- .../compute-virtual-machine-scale-set_test.go | 6 +- .../azure/manual/compute-virtual-machine.go | 6 +- .../manual/compute-virtual-machine_test.go | 6 +- .../azure/manual/dbforpostgresql-database.go | 2 +- .../manual/dbforpostgresql-database_test.go | 6 +- .../manual/dbforpostgresql-flexible-server.go | 2 +- .../dbforpostgresql-flexible-server_test.go | 6 +- sources/azure/manual/dns_links.go | 2 +- .../manual/documentdb-database-accounts.go | 2 +- .../documentdb-database-accounts_test.go | 6 +- sources/azure/manual/keyvault-managed-hsm.go | 6 +- .../azure/manual/keyvault-managed-hsm_test.go | 6 +- sources/azure/manual/keyvault-secret.go | 2 +- sources/azure/manual/keyvault-secret_test.go | 6 +- sources/azure/manual/keyvault-vault.go | 6 +- sources/azure/manual/keyvault-vault_test.go | 6 +- sources/azure/manual/links_helpers.go | 2 +- .../managedidentity-user-assigned-identity.go | 6 +- ...gedidentity-user-assigned-identity_test.go | 6 +- .../manual/network-application-gateway.go | 6 +- .../network-application-gateway_test.go | 6 +- sources/azure/manual/network-load-balancer.go | 6 +- .../manual/network-load-balancer_test.go | 6 +- .../azure/manual/network-network-interface.go | 6 +- .../manual/network-network-interface_test.go | 6 +- .../manual/network-network-security-group.go | 6 +- .../network-network-security-group_test.go | 6 +- .../azure/manual/network-public-ip-address.go | 6 +- .../manual/network-public-ip-address_test.go | 6 +- sources/azure/manual/network-route-table.go | 6 +- .../azure/manual/network-route-table_test.go | 6 +- .../azure/manual/network-virtual-network.go | 6 +- .../manual/network-virtual-network_test.go | 6 +- sources/azure/manual/network-zone.go | 6 +- sources/azure/manual/network-zone_test.go | 6 +- sources/azure/manual/sql-database.go | 2 +- sources/azure/manual/sql-database_test.go | 6 +- sources/azure/manual/sql-server.go | 6 +- sources/azure/manual/sql-server_test.go | 6 +- sources/azure/manual/storage-account.go | 6 +- sources/azure/manual/storage-account_test.go | 6 +- .../azure/manual/storage-blob-container.go | 2 +- .../manual/storage-blob-container_test.go | 6 +- sources/azure/manual/storage-fileshare.go | 2 +- .../azure/manual/storage-fileshare_test.go | 6 +- sources/azure/manual/storage-queues.go | 2 +- sources/azure/manual/storage-queues_test.go | 6 +- sources/azure/manual/storage-table.go | 2 +- sources/azure/manual/storage-table_test.go | 6 +- sources/azure/proc/proc.go | 6 +- sources/azure/proc/proc_test.go | 2 +- sources/azure/shared/adapter-meta.go | 2 +- sources/azure/shared/base.go | 2 +- sources/azure/shared/errors.go | 2 +- sources/azure/shared/item-types.go | 33 +- .../shared/mocks/mock_disk_accesses_client.go | 72 + sources/azure/shared/models.go | 33 +- sources/azure/shared/scope.go | 2 +- sources/example/base.go | 2 +- sources/example/custom_searchable_listable.go | 2 +- sources/example/errors.go | 2 +- sources/example/metadata_test.go | 4 +- .../example/standard_searchable_listable.go | 2 +- .../standard_searchable_listable_test.go | 2 +- sources/example/validation_test.go | 4 +- sources/gcp/build/package/Dockerfile | 2 +- sources/gcp/cmd/root.go | 6 +- sources/gcp/dynamic/adapter-listable.go | 6 +- .../dynamic/adapter-searchable-listable.go | 6 +- sources/gcp/dynamic/adapter-searchable.go | 6 +- sources/gcp/dynamic/adapter.go | 6 +- sources/gcp/dynamic/adapters.go | 4 +- .../.cursor/rules/dynamic-adapter-testing.mdc | 4 +- .../ai-platform-batch-prediction-job.go | 2 +- .../ai-platform-batch-prediction-job_test.go | 6 +- .../adapters/ai-platform-custom-job.go | 2 +- .../adapters/ai-platform-custom-job_test.go | 6 +- .../dynamic/adapters/ai-platform-endpoint.go | 2 +- .../adapters/ai-platform-endpoint_test.go | 6 +- ...latform-model-deployment-monitoring-job.go | 2 +- ...rm-model-deployment-monitoring-job_test.go | 6 +- .../gcp/dynamic/adapters/ai-platform-model.go | 2 +- .../adapters/ai-platform-model_test.go | 6 +- .../adapters/ai-platform-pipeline-job.go | 2 +- .../adapters/ai-platform-pipeline-job_test.go | 6 +- .../artifact-registry-docker-image.go | 2 +- .../artifact-registry-docker-image_test.go | 6 +- .../adapters/artifact-registry-repository.go | 2 +- ...big-query-data-transfer-transfer-config.go | 2 +- ...uery-data-transfer-transfer-config_test.go | 6 +- .../adapters/big-table-admin-app-profile.go | 2 +- .../big-table-admin-app-profile_test.go | 6 +- .../adapters/big-table-admin-backup.go | 2 +- .../adapters/big-table-admin-backup_test.go | 6 +- .../adapters/big-table-admin-cluster.go | 2 +- .../adapters/big-table-admin-cluster_test.go | 6 +- .../adapters/big-table-admin-instance.go | 2 +- .../adapters/big-table-admin-instance_test.go | 6 +- .../dynamic/adapters/big-table-admin-table.go | 2 +- .../adapters/big-table-admin-table_test.go | 6 +- .../adapters/cloud-billing-billing-info.go | 2 +- .../cloud-billing-billing-info_test.go | 2 +- .../gcp/dynamic/adapters/cloud-build-build.go | 2 +- .../adapters/cloud-build-build_test.go | 6 +- .../cloud-resource-manager-project.go | 2 +- .../cloud-resource-manager-project_test.go | 2 +- .../cloud-resource-manager-tag-key.go | 2 +- .../cloud-resource-manager-tag-key_test.go | 4 +- .../cloud-resource-manager-tag-value.go | 2 +- .../cloud-resource-manager-tag-value_test.go | 6 +- .../adapters/cloudfunctions-function.go | 2 +- .../adapters/cloudfunctions-function_test.go | 6 +- .../adapters/compute-accelerator-type.go | 2 +- .../gcp/dynamic/adapters/compute-disk-type.go | 2 +- .../adapters/compute-external-vpn-gateway.go | 2 +- .../compute-external-vpn-gateway_test.go | 6 +- .../gcp/dynamic/adapters/compute-firewall.go | 2 +- .../dynamic/adapters/compute-firewall_test.go | 6 +- .../adapters/compute-global-address.go | 2 +- .../adapters/compute-global-address_test.go | 6 +- .../compute-global-forwarding-rule.go | 2 +- .../compute-global-forwarding-rule_test.go | 6 +- .../adapters/compute-http-health-check.go | 2 +- .../compute-http-health-check_test.go | 6 +- .../adapters/compute-instance-template.go | 2 +- .../compute-instance-template_test.go | 6 +- .../gcp/dynamic/adapters/compute-license.go | 2 +- .../compute-network-endpoint-group.go | 2 +- .../compute-network-endpoint-group_test.go | 6 +- .../gcp/dynamic/adapters/compute-network.go | 2 +- .../dynamic/adapters/compute-network_test.go | 6 +- .../gcp/dynamic/adapters/compute-project.go | 2 +- .../dynamic/adapters/compute-project_test.go | 4 +- .../compute-public-delegated-prefix.go | 2 +- .../compute-public-delegated-prefix_test.go | 6 +- .../adapters/compute-region-commitment.go | 2 +- .../compute-region-commitment_test.go | 6 +- .../adapters/compute-resource-policy.go | 2 +- sources/gcp/dynamic/adapters/compute-route.go | 2 +- .../dynamic/adapters/compute-route_test.go | 6 +- .../gcp/dynamic/adapters/compute-router.go | 2 +- .../dynamic/adapters/compute-router_test.go | 6 +- .../adapters/compute-ssl-certificate.go | 2 +- .../adapters/compute-ssl-certificate_test.go | 4 +- .../dynamic/adapters/compute-ssl-policy.go | 2 +- .../adapters/compute-ssl-policy_test.go | 4 +- .../dynamic/adapters/compute-storage-pool.go | 2 +- .../dynamic/adapters/compute-subnetwork.go | 2 +- .../adapters/compute-subnetwork_test.go | 6 +- .../adapters/compute-target-http-proxy.go | 2 +- .../compute-target-http-proxy_test.go | 6 +- .../adapters/compute-target-https-proxy.go | 2 +- .../compute-target-https-proxy_test.go | 6 +- .../dynamic/adapters/compute-target-pool.go | 2 +- .../adapters/compute-target-pool_test.go | 6 +- .../gcp/dynamic/adapters/compute-url-map.go | 2 +- .../dynamic/adapters/compute-url-map_test.go | 6 +- .../dynamic/adapters/compute-vpn-gateway.go | 2 +- .../adapters/compute-vpn-gateway_test.go | 6 +- .../dynamic/adapters/compute-vpn-tunnel.go | 2 +- .../adapters/compute-vpn-tunnel_test.go | 6 +- .../gcp/dynamic/adapters/container-cluster.go | 2 +- .../adapters/container-cluster_test.go | 6 +- .../dynamic/adapters/container-node-pool.go | 2 +- .../adapters/container-node-pool_test.go | 6 +- .../dynamic/adapters/dataform-repository.go | 2 +- .../adapters/dataform-repository_test.go | 6 +- .../dynamic/adapters/dataplex-aspect-type.go | 2 +- .../adapters/dataplex-aspect-type_test.go | 4 +- .../dynamic/adapters/dataplex-data-scan.go | 2 +- .../adapters/dataplex-data-scan_test.go | 6 +- .../dynamic/adapters/dataplex-entry-group.go | 2 +- .../adapters/dataplex-entry-group_test.go | 4 +- .../adapters/dataproc-auto-scaling-policy.go | 2 +- .../dataproc-auto-scaling-policy_test.go | 4 +- .../gcp/dynamic/adapters/dataproc-cluster.go | 2 +- .../dynamic/adapters/dataproc-cluster_test.go | 6 +- .../gcp/dynamic/adapters/dns-managed-zone.go | 2 +- .../dynamic/adapters/dns-managed-zone_test.go | 6 +- .../adapters/essential-contacts-contact.go | 2 +- .../essential-contacts-contact_test.go | 4 +- .../gcp/dynamic/adapters/eventarc-trigger.go | 2 +- sources/gcp/dynamic/adapters/file-instance.go | 2 +- .../dynamic/adapters/file-instance_test.go | 6 +- sources/gcp/dynamic/adapters/iam-role.go | 2 +- sources/gcp/dynamic/adapters/iam-role_test.go | 4 +- .../gcp/dynamic/adapters/logging-bucket.go | 2 +- .../dynamic/adapters/logging-bucket_test.go | 6 +- sources/gcp/dynamic/adapters/logging-link.go | 2 +- .../gcp/dynamic/adapters/logging-link_test.go | 6 +- .../dynamic/adapters/logging-saved-query.go | 2 +- .../adapters/logging-saved-query_test.go | 4 +- .../adapters/monitoring-alert-policy.go | 2 +- .../adapters/monitoring-alert-policy_test.go | 6 +- .../adapters/monitoring-custom-dashboard.go | 2 +- .../monitoring-custom-dashboard_test.go | 4 +- .../monitoring-notification-channel.go | 2 +- .../monitoring-notification-channel_test.go | 4 +- .../gcp/dynamic/adapters/orgpolicy-policy.go | 2 +- .../dynamic/adapters/orgpolicy-policy_test.go | 4 +- .../dynamic/adapters/pubsub-subscription.go | 2 +- .../adapters/pubsub-subscription_test.go | 6 +- sources/gcp/dynamic/adapters/pubsub-topic.go | 2 +- .../gcp/dynamic/adapters/pubsub-topic_test.go | 6 +- .../gcp/dynamic/adapters/redis-instance.go | 2 +- .../dynamic/adapters/redis-instance_test.go | 6 +- sources/gcp/dynamic/adapters/run-revision.go | 2 +- .../gcp/dynamic/adapters/run-revision_test.go | 6 +- sources/gcp/dynamic/adapters/run-service.go | 2 +- .../gcp/dynamic/adapters/run-service_test.go | 6 +- .../gcp/dynamic/adapters/run-worker-pool.go | 2 +- .../dynamic/adapters/secret-manager-secret.go | 2 +- .../adapters/secret-manager-secret_test.go | 6 +- ...nter-management-security-center-service.go | 2 +- ...management-security-center-service_test.go | 6 +- .../adapters/service-directory-endpoint.go | 2 +- .../service-directory-endpoint_test.go | 6 +- .../adapters/service-directory-service.go | 2 +- .../dynamic/adapters/service-usage-service.go | 2 +- .../adapters/service-usage-service_test.go | 6 +- .../gcp/dynamic/adapters/spanner-backup.go | 2 +- .../gcp/dynamic/adapters/spanner-database.go | 2 +- .../dynamic/adapters/spanner-database_test.go | 6 +- .../adapters/spanner-instance-config.go | 2 +- .../gcp/dynamic/adapters/spanner-instance.go | 2 +- .../dynamic/adapters/spanner-instance_test.go | 6 +- .../dynamic/adapters/sql-admin-backup-run.go | 2 +- .../gcp/dynamic/adapters/sql-admin-backup.go | 2 +- .../dynamic/adapters/sql-admin-backup_test.go | 6 +- .../dynamic/adapters/sql-admin-instance.go | 2 +- .../adapters/sql-admin-instance_test.go | 6 +- .../gcp/dynamic/adapters/storage-bucket.go | 2 +- .../dynamic/adapters/storage-bucket_test.go | 6 +- .../adapters/storage-transfer-transfer-job.go | 2 +- .../storage-transfer-transfer-job_test.go | 6 +- sources/gcp/dynamic/adapters_test.go | 6 +- sources/gcp/dynamic/shared.go | 6 +- sources/gcp/dynamic/shared_test.go | 2 +- .../integration-tests/big-query-model_test.go | 2 +- .../integration-tests/compute-address_test.go | 4 +- .../compute-autoscaler_test.go | 4 +- .../integration-tests/compute-disk_test.go | 4 +- .../compute-forwarding-rule_test.go | 4 +- .../compute-healthcheck_test.go | 4 +- .../integration-tests/compute-image_test.go | 4 +- .../compute-instance-group-manager_test.go | 4 +- .../compute-instance-group_test.go | 4 +- .../compute-instance_test.go | 4 +- .../compute-instant-snapshot_test.go | 4 +- .../compute-machine-image_test.go | 4 +- .../integration-tests/compute-network_test.go | 4 +- .../compute-node-group_test.go | 6 +- .../compute-reservation_test.go | 4 +- .../compute-snapshot_test.go | 4 +- .../compute-subnetwork_test.go | 4 +- .../computer-instance-template_test.go | 4 +- .../spanner-database_test.go | 2 +- .../spanner-instance_test.go | 4 +- .../rules/gcp-manual-adapter-creation.mdc | 6 +- sources/gcp/manual/adapters.go | 4 +- sources/gcp/manual/big-query-dataset.go | 6 +- sources/gcp/manual/big-query-dataset_test.go | 6 +- sources/gcp/manual/big-query-model.go | 6 +- sources/gcp/manual/big-query-model_test.go | 6 +- sources/gcp/manual/big-query-routine.go | 6 +- sources/gcp/manual/big-query-routine_test.go | 6 +- sources/gcp/manual/big-query-table.go | 6 +- sources/gcp/manual/big-query-table_test.go | 6 +- .../manual/cloud-kms-crypto-key-version.go | 6 +- .../cloud-kms-crypto-key-version_test.go | 6 +- sources/gcp/manual/cloud-kms-crypto-key.go | 6 +- .../gcp/manual/cloud-kms-crypto-key_test.go | 6 +- sources/gcp/manual/cloud-kms-key-ring.go | 6 +- sources/gcp/manual/cloud-kms-key-ring_test.go | 6 +- sources/gcp/manual/compute-address.go | 6 +- sources/gcp/manual/compute-address_test.go | 6 +- sources/gcp/manual/compute-autoscaler.go | 6 +- sources/gcp/manual/compute-autoscaler_test.go | 6 +- sources/gcp/manual/compute-backend-service.go | 6 +- .../manual/compute-backend-service_test.go | 6 +- sources/gcp/manual/compute-disk.go | 6 +- sources/gcp/manual/compute-disk_test.go | 6 +- sources/gcp/manual/compute-forwarding-rule.go | 6 +- .../manual/compute-forwarding-rule_test.go | 6 +- sources/gcp/manual/compute-healthcheck.go | 6 +- .../gcp/manual/compute-healthcheck_test.go | 6 +- sources/gcp/manual/compute-image.go | 6 +- sources/gcp/manual/compute-image_test.go | 6 +- .../compute-instance-group-manager-shared.go | 2 +- .../manual/compute-instance-group-manager.go | 6 +- .../compute-instance-group-manager_test.go | 6 +- sources/gcp/manual/compute-instance-group.go | 6 +- .../gcp/manual/compute-instance-group_test.go | 6 +- sources/gcp/manual/compute-instance.go | 6 +- sources/gcp/manual/compute-instance_test.go | 6 +- .../gcp/manual/compute-instant-snapshot.go | 6 +- .../manual/compute-instant-snapshot_test.go | 6 +- sources/gcp/manual/compute-machine-image.go | 6 +- .../gcp/manual/compute-machine-image_test.go | 6 +- sources/gcp/manual/compute-node-group.go | 6 +- sources/gcp/manual/compute-node-group_test.go | 6 +- sources/gcp/manual/compute-node-template.go | 6 +- .../gcp/manual/compute-node-template_test.go | 6 +- .../compute-region-instance-group-manager.go | 6 +- ...pute-region-instance-group-manager_test.go | 6 +- sources/gcp/manual/compute-reservation.go | 6 +- .../gcp/manual/compute-reservation_test.go | 6 +- sources/gcp/manual/compute-security-policy.go | 6 +- .../manual/compute-security-policy_test.go | 6 +- sources/gcp/manual/compute-snapshot.go | 6 +- sources/gcp/manual/compute-snapshot_test.go | 6 +- sources/gcp/manual/iam-service-account-key.go | 6 +- .../manual/iam-service-account-key_test.go | 6 +- sources/gcp/manual/iam-service-account.go | 6 +- .../gcp/manual/iam-service-account_test.go | 6 +- sources/gcp/manual/logging-sink.go | 6 +- sources/gcp/manual/logging-sink_test.go | 6 +- sources/gcp/proc/proc.go | 6 +- sources/gcp/proc/proc_test.go | 6 +- sources/gcp/shared/adapter-meta.go | 2 +- sources/gcp/shared/base.go | 6 +- sources/gcp/shared/big-query-clients.go | 6 +- sources/gcp/shared/blast-propagations.go | 2 +- .../gcp/shared/cross_project_linking_test.go | 2 +- sources/gcp/shared/errors.go | 2 +- sources/gcp/shared/kms-asset-loader.go | 6 +- sources/gcp/shared/linker.go | 2 +- sources/gcp/shared/linker_test.go | 2 +- sources/gcp/shared/manual-adapter-links.go | 2 +- .../gcp/shared/manual-adapter-links_test.go | 2 +- .../mocks/mock_big_query_dataset_client.go | 6 +- sources/gcp/shared/terraform-mappings.go | 2 +- sources/shared/base.go | 2 +- sources/shared/testing.go | 4 +- sources/shared/util.go | 2 +- sources/transformer.go | 6 +- sources/transformer_test.go | 4 +- stdlib-source/adapters/certificate.go | 2 +- stdlib-source/adapters/certificate_test.go | 4 +- stdlib-source/adapters/dns.go | 4 +- stdlib-source/adapters/dns_test.go | 6 +- stdlib-source/adapters/http.go | 4 +- stdlib-source/adapters/http_test.go | 6 +- stdlib-source/adapters/ip.go | 2 +- stdlib-source/adapters/ip_test.go | 4 +- stdlib-source/adapters/main.go | 6 +- stdlib-source/adapters/rdap-asn.go | 4 +- stdlib-source/adapters/rdap-asn_test.go | 2 +- stdlib-source/adapters/rdap-domain.go | 4 +- stdlib-source/adapters/rdap-domain_test.go | 2 +- stdlib-source/adapters/rdap-entity.go | 4 +- stdlib-source/adapters/rdap-entity_test.go | 4 +- stdlib-source/adapters/rdap-ip-network.go | 4 +- .../adapters/rdap-ip-network_test.go | 2 +- stdlib-source/adapters/rdap-nameserver.go | 4 +- .../adapters/rdap-nameserver_test.go | 2 +- stdlib-source/adapters/test/data.go | 2 +- stdlib-source/adapters/test/testdog.go | 2 +- stdlib-source/adapters/test/testfood.go | 2 +- stdlib-source/adapters/test/testgroup.go | 2 +- stdlib-source/adapters/test/testhobby.go | 2 +- stdlib-source/adapters/test/testlocation.go | 2 +- stdlib-source/adapters/test/testperson.go | 2 +- stdlib-source/adapters/test/testregion.go | 2 +- stdlib-source/build/package/Dockerfile | 2 +- stdlib-source/cmd/root.go | 6 +- tfutils/plan_mapper.go | 2 +- tfutils/plan_mapper_test.go | 2 +- tracing/deferlog.go | 77 - tracing/header_carrier.go | 31 - tracing/main.go | 383 - tracing/main_test.go | 12 - tracing/memory.go | 69 - tracing/memory_test.go | 75 - 1552 files changed, 17336 insertions(+), 63495 deletions(-) delete mode 100644 auth/auth.go delete mode 100644 auth/auth_client.go delete mode 100644 auth/auth_test.go delete mode 100644 auth/gcpauth.go delete mode 100644 auth/middleware.go delete mode 100644 auth/middleware_test.go delete mode 100644 auth/nats.go delete mode 100644 auth/nats_test.go delete mode 100644 auth/tracing.go delete mode 100644 discovery/adapter.go delete mode 100644 discovery/adapter_test.go delete mode 100644 discovery/adapterhost.go delete mode 100644 discovery/adapterhost_bench_test.go delete mode 100644 discovery/adapterhost_test.go delete mode 100644 discovery/cmd.go delete mode 100644 discovery/cmd_test.go delete mode 100644 discovery/doc.go delete mode 100644 discovery/engine.go delete mode 100644 discovery/engine_initerror_test.go delete mode 100644 discovery/engine_test.go delete mode 100644 discovery/enginerequests.go delete mode 100644 discovery/enginerequests_test.go delete mode 100644 discovery/getfindmutex.go delete mode 100644 discovery/getfindmutex_test.go delete mode 100644 discovery/heartbeat.go delete mode 100644 discovery/heartbeat_test.go delete mode 100644 discovery/item_tests.go delete mode 100644 discovery/logs.go delete mode 100644 discovery/logs_test.go delete mode 100644 discovery/main_test.go delete mode 100644 discovery/nats_shared_test.go delete mode 100644 discovery/nats_watcher.go delete mode 100644 discovery/nats_watcher_test.go delete mode 100644 discovery/nil_publisher.go delete mode 100644 discovery/performance_test.go delete mode 100644 discovery/querytracker.go delete mode 100644 discovery/querytracker_test.go delete mode 100644 discovery/shared_test.go delete mode 100644 discovery/tracing.go create mode 100644 docs.overmind.tech/docs/sources/_category_.json create mode 100644 docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-address.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-image.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-service.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ecs-task.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/eks-addon.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/iam-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/iam-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/iam-role.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/iam-user.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/kms-alias.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/kms-grant.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/kms-key.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/lambda-function.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sns-topic.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md create mode 100644 docs.overmind.tech/docs/sources/aws/_category_.json create mode 100644 docs.overmind.tech/docs/sources/aws/account_settings.png create mode 100644 docs.overmind.tech/docs/sources/aws/aws_manual.png create mode 100644 docs.overmind.tech/docs/sources/aws/aws_source_settings.png create mode 100644 docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png create mode 100644 docs.overmind.tech/docs/sources/aws/configuration.md create mode 100644 docs.overmind.tech/docs/sources/aws/configure-aws.png create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-method.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-model.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-location.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-address.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-image.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-instance.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-volume.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-service.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ecs-task.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/efs-access-point.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/efs-file-system.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/eks-addon.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/eks-cluster.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/iam-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/iam-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/iam-role.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/iam-user.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/kms-alias.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/kms-grant.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/kms-key.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/lambda-function.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/lambda-layer.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/rds-option-group.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/route53-health-check.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/s3-bucket.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sns-subscription.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sns-topic.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/sqs-queue.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json create mode 100644 docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md create mode 100644 docs.overmind.tech/docs/sources/gcp/_category_.json create mode 100644 docs.overmind.tech/docs/sources/gcp/configuration.md create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/CronJob.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Deployment.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Ingress.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Job.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Node.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Pod.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Role.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Secret.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/Service.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md create mode 100644 docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md create mode 100644 docs.overmind.tech/docs/sources/k8s/_category_.json create mode 100644 docs.overmind.tech/docs/sources/k8s/account_settings.png create mode 100644 docs.overmind.tech/docs/sources/k8s/api_key.png create mode 100644 docs.overmind.tech/docs/sources/k8s/configuration.md create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/CronJob.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Deployment.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Endpoints.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Ingress.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Job.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/LimitRange.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Node.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Pod.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Role.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Secret.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/Service.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/StorageClass.json create mode 100644 docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/certificate.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/dns.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/http.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/ip.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md create mode 100644 docs.overmind.tech/docs/sources/stdlib/_category_json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/certificate.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/dns.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/http.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/ip.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json create mode 100644 docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json delete mode 100644 logging/logging.go delete mode 100644 logging/logging_test.go delete mode 100644 sdp-go/.gitignore delete mode 100644 sdp-go/account.go delete mode 100644 sdp-go/account.pb.go delete mode 100644 sdp-go/apikey.go delete mode 100644 sdp-go/apikeys.pb.go delete mode 100644 sdp-go/area51.pb.go delete mode 100644 sdp-go/auth0support.pb.go delete mode 100644 sdp-go/bookmarks.go delete mode 100644 sdp-go/bookmarks.pb.go delete mode 100644 sdp-go/cached_entry.pb.go delete mode 100644 sdp-go/changes.go delete mode 100644 sdp-go/changes.pb.go delete mode 100644 sdp-go/changes_test.go delete mode 100644 sdp-go/changetimeline.go delete mode 100644 sdp-go/changetimeline_test.go delete mode 100644 sdp-go/cli.pb.go delete mode 100644 sdp-go/compare.go delete mode 100644 sdp-go/config.pb.go delete mode 100644 sdp-go/connection.go delete mode 100644 sdp-go/connection_test.go delete mode 100644 sdp-go/encoder_test.go delete mode 100644 sdp-go/errors.go delete mode 100644 sdp-go/gateway.go delete mode 100644 sdp-go/gateway.pb.go delete mode 100644 sdp-go/gateway_test.go delete mode 100644 sdp-go/genhandler.go delete mode 100644 sdp-go/graph/main.go delete mode 100644 sdp-go/graph/main_test.go delete mode 100644 sdp-go/handler_cancelquery.go delete mode 100644 sdp-go/handler_gatewayresponse.go delete mode 100644 sdp-go/handler_natsgetlogrecordsrequest.go delete mode 100644 sdp-go/handler_natsgetlogrecordsresponse.go delete mode 100644 sdp-go/handler_query.go delete mode 100644 sdp-go/handler_queryresponse.go delete mode 100644 sdp-go/instance_detect.go delete mode 100644 sdp-go/invites.pb.go delete mode 100644 sdp-go/items.go delete mode 100644 sdp-go/items.pb.go delete mode 100644 sdp-go/items_test.go delete mode 100644 sdp-go/link_extract.go delete mode 100644 sdp-go/link_extract_test.go delete mode 100644 sdp-go/logs.go delete mode 100644 sdp-go/logs.pb.go delete mode 100644 sdp-go/logs_test.go delete mode 100644 sdp-go/progress.go delete mode 100644 sdp-go/progress_test.go delete mode 100644 sdp-go/proto_clone_test.go delete mode 100644 sdp-go/responses.go delete mode 100644 sdp-go/responses.pb.go delete mode 100644 sdp-go/revlink.pb.go delete mode 100644 sdp-go/sdpconnect/account.connect.go delete mode 100644 sdp-go/sdpconnect/apikeys.connect.go delete mode 100644 sdp-go/sdpconnect/area51.connect.go delete mode 100644 sdp-go/sdpconnect/auth0support.connect.go delete mode 100644 sdp-go/sdpconnect/bookmarks.connect.go delete mode 100644 sdp-go/sdpconnect/changes.connect.go delete mode 100644 sdp-go/sdpconnect/cli.connect.go delete mode 100644 sdp-go/sdpconnect/config.connect.go delete mode 100644 sdp-go/sdpconnect/invites.connect.go delete mode 100644 sdp-go/sdpconnect/logs.connect.go delete mode 100644 sdp-go/sdpconnect/revlink.connect.go delete mode 100644 sdp-go/sdpconnect/signal.connect.go delete mode 100644 sdp-go/sdpconnect/snapshots.connect.go delete mode 100644 sdp-go/sdpws/client.go delete mode 100644 sdp-go/sdpws/client_test.go delete mode 100644 sdp-go/sdpws/messagehandler.go delete mode 100644 sdp-go/sdpws/utils.go delete mode 100644 sdp-go/signal.pb.go delete mode 100644 sdp-go/signals.go delete mode 100644 sdp-go/snapshots.go delete mode 100644 sdp-go/snapshots.pb.go delete mode 100644 sdp-go/test_utils.go delete mode 100644 sdp-go/test_utils_test.go delete mode 100644 sdp-go/tracing.go delete mode 100644 sdp-go/tracing_test.go delete mode 100644 sdp-go/util.go delete mode 100644 sdp-go/util.pb.go delete mode 100644 sdp-go/util_test.go delete mode 100644 sdp-go/validation.go delete mode 100644 sdp-go/validation_test.go delete mode 100644 sdpcache/bolt_cache.go delete mode 100644 sdpcache/cache.go delete mode 100644 sdpcache/cache_benchmark_test.go delete mode 100644 sdpcache/cache_stuck_test.go delete mode 100644 sdpcache/cache_test.go delete mode 100644 sdpcache/item_generator_test.go delete mode 100644 sdpcache/pending.go create mode 100644 sources/azure/clients/disk-accesses-client.go create mode 100644 sources/azure/integration-tests/compute-disk-access_test.go create mode 100644 sources/azure/manual/compute-disk-access.go create mode 100644 sources/azure/manual/compute-disk-access_test.go create mode 100644 sources/azure/shared/mocks/mock_disk_accesses_client.go delete mode 100644 tracing/deferlog.go delete mode 100644 tracing/header_carrier.go delete mode 100644 tracing/main.go delete mode 100644 tracing/main_test.go delete mode 100644 tracing/memory.go delete mode 100644 tracing/memory_test.go diff --git a/auth/auth.go b/auth/auth.go deleted file mode 100644 index 6e1bd323..00000000 --- a/auth/auth.go +++ /dev/null @@ -1,448 +0,0 @@ -package auth - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "os" - "time" - - "connectrpc.com/connect" - jose "github.com/go-jose/go-jose/v4" - josejwt "github.com/go-jose/go-jose/v4/jwt" - "github.com/nats-io/jwt/v2" - "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel/codes" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" -) - -const UserAgentVersion = "0.1" - -// TokenClient Represents something that is capable of getting NATS JWT tokens -// for a given set of NKeys -type TokenClient interface { - // Returns a NATS token that can be used to connect - GetJWT() (string, error) - - // Uses the NKeys associated with the token to sign some binary data - Sign([]byte) ([]byte, error) -} - -// BasicTokenClient stores a static token and returns it when called, ignoring -// any provided NKeys or context since it already has the token and doesn't need -// to make any requests -type BasicTokenClient struct { - staticToken string - staticKeys nkeys.KeyPair -} - -// assert interface implementation -var _ TokenClient = (*BasicTokenClient)(nil) - -// NewBasicTokenClient Creates a new basic token client that simply returns a static token -func NewBasicTokenClient(token string, keys nkeys.KeyPair) *BasicTokenClient { - return &BasicTokenClient{ - staticToken: token, - staticKeys: keys, - } -} - -func (b *BasicTokenClient) GetJWT() (string, error) { - return b.staticToken, nil -} - -func (b *BasicTokenClient) Sign(in []byte) ([]byte, error) { - return b.staticKeys.Sign(in) -} - -// ClientCredentialsConfig Authenticates to Overmind using the Client -// Credentials flow -// https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow -type ClientCredentialsConfig struct { - // The ClientID of the application that we'll be authenticating as - ClientID string - // ClientSecret that corresponds to the ClientID - ClientSecret string -} - -type TokenSourceOptionsFunc func(*clientcredentials.Config) - -// This option means that the token that is retrieved will have the following -// account embedded in it through impersonation. In order for this to work, the -// Auth0 ClientID must be added to workspace/deploy/auth0.tf. This will use -// deploy/auth0_embed_account_m2m.tftpl to update the Auth0 action that we use -// to allow impersonation. If this isn't done first you will get an error from -// Auth0. -func WithImpersonateAccount(account string) TokenSourceOptionsFunc { - return func(c *clientcredentials.Config) { - c.EndpointParams.Set("account_name", account) - } -} - -// TokenSource Returns a token source that can be used to get OAuth tokens. -// Cache this between invocations to avoid additional charges by Auth0 for M2M -// tokens. The oAuthTokenURL looks like this: -// https://somedomain.auth0.com/oauth/token -// -// The context that is passed to this function is used when getting new tokens, -// which will happen initially, and then subsequently when the token expires. -// This means that if this token source is going to be stored and used for many -// requests, it should not use the context of the request that created it, as -// this will be cancelled. Instead it should probably use `context.Background()` -// or similar. -func (flowConfig ClientCredentialsConfig) TokenSource(ctx context.Context, oAuthTokenURL, oAuthAudience string, opts ...TokenSourceOptionsFunc) oauth2.TokenSource { - // inject otel into oauth2 - ctx = context.WithValue(ctx, oauth2.HTTPClient, tracing.HTTPClient()) - - conf := &clientcredentials.Config{ - ClientID: flowConfig.ClientID, - ClientSecret: flowConfig.ClientSecret, - TokenURL: oAuthTokenURL, - EndpointParams: url.Values{ - "audience": []string{oAuthAudience}, - }, - } - - for _, opt := range opts { - opt(conf) - } - // this will be a `oauth2.ReuseTokenSource`, thus caching the M2M token. - // note that this token source is safe for concurrent use and will - // automatically refresh the token when it expires. Also note that this - // token source will use the passed in http client from otelhttp for all - // requests, but will not get the actual caller's context, so spans will not - // link up. - 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 -type natsTokenClient struct { - // The name of the account to impersonate. If this is omitted then the - // account will be determined based on the account included in the resulting - // token. - Account string - - // authenticated clients for the Overmind API - adminClient sdpconnect.AdminServiceClient - mgmtClient sdpconnect.ManagementServiceClient - - jwt string - keys nkeys.KeyPair -} - -// assert interface implementation -var _ TokenClient = (*natsTokenClient)(nil) - -// generateKeys Generates a new set of keys for the client -func (n *natsTokenClient) generateKeys() error { - var err error - - n.keys, err = nkeys.CreateUser() - - return err -} - -// generateJWT Gets a new JWT from the auth API -func (n *natsTokenClient) generateJWT(ctx context.Context) error { - if n.adminClient == nil || n.mgmtClient == nil { - return errors.New("no Overmind API client configured") - } - - // If we don't yet have keys generate them - if n.keys == nil { - err := n.generateKeys() - if err != nil { - return err - } - } - - pubKey, err := n.keys.PublicKey() - if err != nil { - return err - } - - hostname, err := os.Hostname() - if err != nil { - return err - } - - req := &sdp.CreateTokenRequest{ - UserPublicNkey: pubKey, - UserName: hostname, - } - - // Create the request for a NATS token - var response *connect.Response[sdp.CreateTokenResponse] - if n.Account == "" { - // Use the regular API and let the client authentication determine what our org should be - log.WithFields(log.Fields{ - "account": n.Account, - "publicNKey": req.GetUserPublicNkey(), - "UserName": req.GetUserName(), - }).Trace("Using regular API to get NATS token") - response, err = n.mgmtClient.CreateToken(ctx, connect.NewRequest(req)) - } else { - log.WithFields(log.Fields{ - "account": n.Account, - "publicNKey": req.GetUserPublicNkey(), - "UserName": req.GetUserName(), - }).Trace("Using admin API to get NATS token") - // Explicitly request an org - response, err = n.adminClient.CreateToken(ctx, connect.NewRequest(&sdp.AdminCreateTokenRequest{ - Account: n.Account, - Request: req, - })) - } - if err != nil { - return fmt.Errorf("getting NATS token failed: %w", err) - } - - n.jwt = response.Msg.GetToken() - - return nil -} - -func (n *natsTokenClient) GetJWT() (string, error) { - ctx, span := tracer.Start(context.Background(), "connect.GetJWT") - defer span.End() - - // If we don't yet have a JWT, generate one - if n.jwt == "" { - err := n.generateJWT(ctx) - if err != nil { - err = fmt.Errorf("error generating JWT: %w", err) - span.SetStatus(codes.Error, err.Error()) - return "", err - } - } - - claims, err := jwt.DecodeUserClaims(n.jwt) - if err != nil { - err = fmt.Errorf("error decoding JWT: %w", err) - span.SetStatus(codes.Error, err.Error()) - return n.jwt, err - } - - // Validate to make sure the JWT is valid. If it isn't we'll generate a new - // one - var vr jwt.ValidationResults - - claims.Validate(&vr) - - if vr.IsBlocking(true) { - // Regenerate the token - err := n.generateJWT(ctx) - if err != nil { - err = fmt.Errorf("error validating JWT: %w", err) - span.SetStatus(codes.Error, err.Error()) - return "", err - } - } - - span.SetStatus(codes.Ok, "Completed") - return n.jwt, nil -} - -func (n *natsTokenClient) Sign(in []byte) ([]byte, error) { - if n.keys == nil { - err := n.generateKeys() - if err != nil { - return []byte{}, err - } - } - - return n.keys.Sign(in) -} - -// An OAuth2 token source which uses an Overmind API token as a source for OAuth -// tokens -type APIKeyTokenSource struct { - // The API Key to use to authenticate to the Overmind API - ApiKey string - token *oauth2.Token - apiKeyClient sdpconnect.ApiKeyServiceClient -} - -func NewAPIKeyTokenSource(apiKey string, overmindAPIURL string) *APIKeyTokenSource { - httpClient := http.Client{ - Timeout: 10 * time.Second, - Transport: otelhttp.NewTransport(http.DefaultTransport), - } - - // Create a client that exchanges the API key for a JWT - apiKeyClient := sdpconnect.NewApiKeyServiceClient(&httpClient, overmindAPIURL) - - return &APIKeyTokenSource{ - ApiKey: apiKey, - apiKeyClient: apiKeyClient, - } -} - -// Exchange an API key for an OAuth token -func (ats *APIKeyTokenSource) Token() (*oauth2.Token, error) { - if ats.token != nil { - // If we already have a token, and it is valid, return it - if ats.token.Valid() { - return ats.token, nil - } - } - - // Get a new token - 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) - } - - if res.Msg.GetAccessToken() == "" { - return nil, errors.New("no access token returned") - } - - // 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) - } - - claims := josejwt.Claims{} - - err = token.UnsafeClaimsWithoutVerification(&claims) - if err != nil { - return nil, fmt.Errorf("error parsing JWT claims: %w", err) - } - - ats.token = &oauth2.Token{ - AccessToken: res.Msg.GetAccessToken(), - TokenType: "Bearer", - Expiry: claims.Expiry.Time(), - } - - return ats.token, nil -} - -// NewAPIKeyClient Creates a new token client that authenticates to Overmind -// using an API key. This is exchanged for an OAuth token, which is then used to -// get a NATS token. -// -// The provided `overmindAPIURL` parameter should be the root URL of the -// Overmind API, without the /api suffix e.g. https://api.app.overmind.tech -func NewAPIKeyClient(overmindAPIURL string, apiKey string) (*natsTokenClient, error) { - // Create a token source that exchanges the API key for an OAuth token - tokenSource := NewAPIKeyTokenSource(apiKey, overmindAPIURL) - transport := oauth2.Transport{ - Source: tokenSource, - Base: http.DefaultTransport, - } - httpClient := http.Client{ - Transport: otelhttp.NewTransport(&transport), - } - - return &natsTokenClient{ - adminClient: sdpconnect.NewAdminServiceClient(&httpClient, overmindAPIURL), - mgmtClient: sdpconnect.NewManagementServiceClient(&httpClient, overmindAPIURL), - }, nil -} - -// NewStaticTokenClient Creates a new token client that uses a static token -// The user must pass the Overmind API URL to configure the client to connect -// to, the raw JWT OAuth access token, and the type of token. This is almost -// always "Bearer" -func NewStaticTokenClient(overmindAPIURL, token, tokenType string) (*natsTokenClient, error) { - transport := oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: token, - TokenType: tokenType, - }), - } - - httpClient := http.Client{ - Transport: otelhttp.NewTransport(&transport), - } - - return &natsTokenClient{ - adminClient: sdpconnect.NewAdminServiceClient(&httpClient, overmindAPIURL), - mgmtClient: sdpconnect.NewManagementServiceClient(&httpClient, overmindAPIURL), - }, nil -} - -// NewOAuthTokenClient creates a token client that uses the provided TokenSource -// to get a NATS token. `overmindAPIURL` is the root URL of the NATS token -// exchange API that will be used e.g. https://api.server.test/v1 -// -// Tokens will be minted under the specified account as long as the client has -// admin permissions, if not, the account that is attached to the client via -// Auth0 metadata will be used -func NewOAuthTokenClient(overmindAPIURL string, account string, ts oauth2.TokenSource) *natsTokenClient { - return NewOAuthTokenClientWithContext(context.Background(), overmindAPIURL, account, ts) -} - -// NewOAuthTokenClientWithContext creates a token client that uses the provided -// TokenSource to get a NATS token. `overmindAPIURL` is the root URL of the NATS -// token exchange API that will be used e.g. https://api.server.test/v1 -// -// Tokens will be minted under the specified account as long as the client has -// admin permissions, if not, the account that is attached to the client via -// Auth0 metadata will be used -// -// The provided context is used for cancellation and to lookup the HTTP client -// used by oauth2. See the oauth2.HTTPClient variable. -// -// Provide an account name and an admin token to create a token client for a -// foreign account. -func NewOAuthTokenClientWithContext(ctx context.Context, overmindAPIURL string, account string, ts oauth2.TokenSource) *natsTokenClient { - authenticatedClient := oauth2.NewClient(ctx, ts) - - // backwards compatibility: remove previously existing "/api" suffix from URL for connect - apiUrl, err := url.Parse(overmindAPIURL) - if err == nil { - apiUrl.Path = "" - overmindAPIURL = apiUrl.String() - } - - return &natsTokenClient{ - Account: account, - adminClient: sdpconnect.NewAdminServiceClient(authenticatedClient, overmindAPIURL), - mgmtClient: sdpconnect.NewManagementServiceClient(authenticatedClient, overmindAPIURL), - } -} diff --git a/auth/auth_client.go b/auth/auth_client.go deleted file mode 100644 index 73b5dc08..00000000 --- a/auth/auth_client.go +++ /dev/null @@ -1,124 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "net/http" - - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" -) - -// AuthenticatedClient is a http.Client that will automatically add the required -// Authorization header to the request, which is taken from the context that it -// is created with. We also always set the X-overmind-interactive header to -// false to connect opentelemetry traces. -type AuthenticatedTransport struct { - from http.RoundTripper - token string -} - -// RoundTrip Adds the Authorization header to the request then call the -// underlying roundTripper -func (y *AuthenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // ask for otel trace linkup - req.Header.Set("X-Overmind-Interactive", "false") - - if y.token != "" { - bearer := fmt.Sprintf("Bearer %v", y.token) - req.Header.Set("Authorization", bearer) - } - - return y.from.RoundTrip(req) -} - -// NewAuthenticatedClient creates a new AuthenticatedClient from the given -// context and http.Client. -func NewAuthenticatedClient(ctx context.Context, from *http.Client) *http.Client { - token, ok := ctx.Value(UserTokenContextKey{}).(string) - if !ok { - token = "" - } - - return &http.Client{ - Transport: &AuthenticatedTransport{ - from: from.Transport, - token: token, - }, - CheckRedirect: from.CheckRedirect, - Jar: from.Jar, - Timeout: from.Timeout, - } -} - -// AuthenticatedAdminClient Returns a bookmark client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedAdminClient(ctx context.Context, apiUrl string) sdpconnect.AdminServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind admin API (pre-authenticated)") - return sdpconnect.NewAdminServiceClient(httpClient, apiUrl) -} - -// AuthenticatedApiKeyClient Returns an apikey client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedApiKeyClient(ctx context.Context, apiUrl string) sdpconnect.ApiKeyServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind apikeys API (pre-authenticated)") - return sdpconnect.NewApiKeyServiceClient(httpClient, apiUrl) -} - -// UnauthenticatedApiKeyClient Returns an apikey client with otel instrumentation -// but no authentication. Can only be used for ExchangeKeyForToken -func UnauthenticatedApiKeyClient(ctx context.Context, apiUrl string) sdpconnect.ApiKeyServiceClient { - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind apikeys API") - return sdpconnect.NewApiKeyServiceClient(tracing.HTTPClient(), apiUrl) -} - -// AuthenticatedBookmarkClient Returns a bookmark client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedBookmarkClient(ctx context.Context, apiUrl string) sdpconnect.BookmarksServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind bookmark API (pre-authenticated)") - return sdpconnect.NewBookmarksServiceClient(httpClient, apiUrl) -} - -// AuthenticatedChangesClient Returns a bookmark client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedChangesClient(ctx context.Context, apiUrl string) sdpconnect.ChangesServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind changes API (pre-authenticated)") - return sdpconnect.NewChangesServiceClient(httpClient, apiUrl) -} - -// AuthenticatedConfigurationClient Returns a bookmark client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedConfigurationClient(ctx context.Context, apiUrl string) sdpconnect.ConfigurationServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind configuration API (pre-authenticated)") - return sdpconnect.NewConfigurationServiceClient(httpClient, apiUrl) -} - -// AuthenticatedManagementClient Returns a bookmark client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedManagementClient(ctx context.Context, apiUrl string) sdpconnect.ManagementServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind management API (pre-authenticated)") - return sdpconnect.NewManagementServiceClient(httpClient, apiUrl) -} - -// AuthenticatedSnapshotsClient Returns a Snapshots client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedSnapshotsClient(ctx context.Context, apiUrl string) sdpconnect.SnapshotsServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind snapshot API (pre-authenticated)") - return sdpconnect.NewSnapshotsServiceClient(httpClient, apiUrl) -} - -// AuthenticatedInviteClient Returns a Invite client that uses the auth -// embedded in the context and otel instrumentation -func AuthenticatedInviteClient(ctx context.Context, apiUrl string) sdpconnect.InviteServiceClient { - httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) - log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind invite API (pre-authenticated)") - return sdpconnect.NewInviteServiceClient(httpClient, apiUrl) -} diff --git a/auth/auth_test.go b/auth/auth_test.go deleted file mode 100644 index 62c1c835..00000000 --- a/auth/auth_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "net" - "net/http/httptest" - "net/url" - "os" - "testing" - "time" - - "connectrpc.com/connect" - "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" -) - -var tokenExchangeURLs = []string{ - "http://api-server:8080", - "http://localhost:8080", -} - -func TestBasicTokenClient(t *testing.T) { - var c TokenClient - - keys, err := nkeys.CreateUser() - - if err != nil { - t.Fatal(err) - } - - c = NewBasicTokenClient("tokeny_mc_tokenface", keys) - - var token string - - token, err = c.GetJWT() - - if err != nil { - t.Error(err) - } - - if token != "tokeny_mc_tokenface" { - t.Error("token mismatch") - } - - data := []byte{1, 156, 230, 4, 23, 175, 11} - - signed, err := c.Sign(data) - - if err != nil { - t.Fatal(err) - } - - err = keys.Verify(data, signed) - - if err != nil { - t.Error(err) - } -} - -func GetTestOAuthTokenClient(t *testing.T) *natsTokenClient { - var domain string - var clientID string - var clientSecret string - var exists bool - - errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/auth0-test-data" - - // Read secrets form the environment - if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_DOMAIN") - t.Skip("Skipping due to missing environment setup") - } - - if clientID, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_ID"); !exists || clientID == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_ID") - t.Skip("Skipping due to missing environment setup") - } - - if clientSecret, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_SECRET"); !exists || clientSecret == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_SECRET") - t.Skip("Skipping due to missing environment setup") - } - - exchangeURL, err := GetWorkingTokenExchange() - - if err != nil { - t.Fatal(err) - } - - flowConfig := ClientCredentialsConfig{ - ClientID: clientID, - ClientSecret: clientSecret, - } - - return NewOAuthTokenClient( - exchangeURL, - "overmind-development", - flowConfig.TokenSource(t.Context(), fmt.Sprintf("https://%v/oauth/token", domain), os.Getenv("API_SERVER_AUDIENCE")), - ) -} - -func TestOAuthTokenClient(t *testing.T) { - if os.Getenv("CI") == "true" { - t.Skip("Skipping test in CI environment, missing nats token exchange server") - } - - c := GetTestOAuthTokenClient(t) - - var err error - - _, err = c.GetJWT() - - if err != nil { - t.Error(err) - } - - // Make sure it can sign - data := []byte{1, 156, 230, 4, 23, 175, 11} - - _, err = c.Sign(data) - - if err != nil { - t.Fatal(err) - } - -} - -type testAPIKeyHandler struct { - sdpconnect.UnimplementedApiKeyServiceHandler -} - -// Always return a valid token -func (h *testAPIKeyHandler) ExchangeKeyForToken(ctx context.Context, req *connect.Request[sdp.ExchangeKeyForTokenRequest]) (*connect.Response[sdp.ExchangeKeyForTokenResponse], error) { - return &connect.Response[sdp.ExchangeKeyForTokenResponse]{ - Msg: &sdp.ExchangeKeyForTokenResponse{ - AccessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyfQ.Tt0D8zOO3uzfbR1VLc3v7S1_jNrP9_crU1Gi_LpVinEXn4hndTWnI9rMd9r9D0iiv6U-CZAb9JKlun58MO3Pbf_S7apiLGHGE11coIMdk5OKuQFepwXPEk4ixs8_51wmWtJAKg7L5JJG6NuLGnGK8a53hzSHjoK80ROBqlsE9dJ4lpgigj8ZcL-xWpjS4TnUiGLHOvNDnHdqP5D_3DA1teWk9PNh9uU6Wn3U3ShH9rRCI9mKz9amdZ7QzH44J5Gsh2-uo0m2BtZILBE5_p-BeJ7op2RicEXbm69Vae8SPjkJLorBQxbO2lMG4y00q1n-wRDfg_eLFH8ZVC-5lpVXIw", - }, - }, nil -} - -func TestNewAPIKeyTokenSource(t *testing.T) { - _, handler := sdpconnect.NewApiKeyServiceHandler(&testAPIKeyHandler{}) - - testServer := httptest.NewServer(handler) - defer testServer.Close() - - ts := NewAPIKeyTokenSource("test", testServer.URL) - - token, err := ts.Token() - - if err != nil { - t.Fatal(err) - } - - // Make sure the expiry is correct - if token.Expiry.Unix() != 1516239022 { - t.Errorf("token expiry incorrect. Expected 1516239022, got %v", token.Expiry.Unix()) - } -} - -func GetWorkingTokenExchange() (string, error) { - errMap := make(map[string]error) - - for _, url := range tokenExchangeURLs { - var err error - if err = testURL(url); err == nil { - return url, nil - } - errMap[url] = err - } - - var errString string - - for url, err := range errMap { - errString = errString + fmt.Sprintf(" %v: %v\n", url, err.Error()) - } - - return "", fmt.Errorf("no working token exchanges found:\n%v", errString) -} - -func testURL(testURL string) error { - url, err := url.Parse(testURL) - - if err != nil { - return fmt.Errorf("could not parse NATS URL: %v. Error: %w", testURL, err) - } - - dialer := &net.Dialer{ - Timeout: time.Second, - } - conn, err := dialer.DialContext(context.Background(), "tcp", net.JoinHostPort(url.Hostname(), url.Port())) - - if err == nil { - conn.Close() - return nil - } - - return err -} diff --git a/auth/gcpauth.go b/auth/gcpauth.go deleted file mode 100644 index 6cf24ba1..00000000 --- a/auth/gcpauth.go +++ /dev/null @@ -1,58 +0,0 @@ -// This file is adapted from https://gist.github.com/ahmetb/548059cdbf12fb571e4e2f1e29c48997 - -package auth - -import ( - "context" - "fmt" - "log" - "net/http" - "strings" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "k8s.io/client-go/rest" -) - -var ( - googleScopes = []string{ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/userinfo.email"} -) - -const ( - GoogleAuthPlugin = "custom_gcp" // so that this is different than "gcp" that's already in client-go tree. -) - -func init() { - if err := rest.RegisterAuthProviderPlugin(GoogleAuthPlugin, newGoogleAuthProvider); err != nil { - log.Fatalf("Failed to register %s auth plugin: %v", GoogleAuthPlugin, err) - } -} - -var _ rest.AuthProvider = &googleAuthProvider{} - -type googleAuthProvider struct { - tokenSource oauth2.TokenSource -} - -func (g *googleAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { - return &oauth2.Transport{ - Base: rt, - Source: g.tokenSource, - } -} -func (g *googleAuthProvider) Login() error { return nil } - -func newGoogleAuthProvider(addr string, config map[string]string, persister rest.AuthProviderConfigPersister) (rest.AuthProvider, error) { - scopes := googleScopes - scopesCfg, found := config["scopes"] - if found { - scopes = strings.Split(scopesCfg, " ") - } - ts, err := google.DefaultTokenSource(context.Background(), scopes...) - if err != nil { - return nil, fmt.Errorf("failed to create google token source: %w", err) - } - return &googleAuthProvider{tokenSource: ts}, nil -} diff --git a/auth/middleware.go b/auth/middleware.go deleted file mode 100644 index 5a806c55..00000000 --- a/auth/middleware.go +++ /dev/null @@ -1,499 +0,0 @@ -package auth - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "regexp" - "strings" - "time" - - jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" - "github.com/auth0/go-jwt-middleware/v2/jwks" - "github.com/auth0/go-jwt-middleware/v2/validator" - "github.com/getsentry/sentry-go" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" -) - -// ScopeCheckBypassedContextKey is a key that is stored in the request context -// when scope checking is actively being bypassed, e.g. in development. When -// this is set the `HasScopes()` function will always return true, and can be -// set using the `WithBypassScopeCheck()` middleware. -type ScopeCheckBypassedContextKey struct{} - -// CustomClaimsContextKey is the key that is used to store the custom claims -// from the JWT -type CustomClaimsContextKey struct{} - -// AccountNameContextKey is the key that is used to store the currently acting -// account name -type AccountNameContextKey struct{} - -// UserTokenContextKey is the key that is used to store the full JWT token of the user -type UserTokenContextKey struct{} - -// CurrentSubjectContextKey is the key that is used to store the current subject attribute. -// This will be the auth0 `user_id` from the tokens `sub` claim. -type CurrentSubjectContextKey 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 - // checked in order with the first one that is found being used - AuthCookieNames []string - - // Use this to specify the full issuer URL for validating the JWTs. This - // should only be used if we aren't using Auth0 as a source for tokens (such - // as in testing). Auth0Domain will take precedence if both are set. - IssuerURL string - - // Bypasses all auth checks, meaning that HasScopes() will always return - // true. This should be used in conjunction with the `AccountOverride` field - // since there won't be a token to parse the account from - BypassAuth bool - - // Bypasses auth for the given paths. This is a regular expression that is - // matched against the path of the request. If the regex matches then the - // request will be allowed through without auth. This should be used with - // `AccountOverride` in order to avoid the required context values not being - // set and therefore causing issues (probably nil pointer panics) - BypassAuthForPaths *regexp.Regexp - - // Overrides the account name stored in the CustomClaimsContextKey - AccountOverride *string - - // Overrides the scope stored in the CustomClaimsContextKey - ScopeOverride *string -} - -// HasScopes compatibility alias for HasAllScopes -func HasScopes(ctx context.Context, requiredScopes ...string) bool { - return HasAllScopes(ctx, requiredScopes...) -} - -// HasAllScopes checks that the authenticated user in the request context has all the -// required scopes. If auth has been bypassed, this will always return true -func HasAllScopes(ctx context.Context, requiredScopes ...string) bool { - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.StringSlice("ovm.auth.requiredScopes.all", requiredScopes), - ) - - if ctx.Value(ScopeCheckBypassedContextKey{}) == true { - // this is always set when auth is bypassed - // set it here again to capture non-standard auth configs - span.SetAttributes(attribute.Bool("ovm.auth.bypass", true)) - - // Bypass all auth - return true - } - - claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) - if !ok { - span.SetAttributes(attribute.String("ovm.auth.missingClaims", "all")) - return false - } - - for _, scope := range requiredScopes { - if !claims.HasScope(scope) { - span.SetAttributes(attribute.String("ovm.auth.missingClaims", scope)) - return false - } - } - return true -} - -// HasAnyScopes checks that the authenticated user in the request context has any of the -// required scopes. If auth has been bypassed, this will always return true -func HasAnyScopes(ctx context.Context, requiredScopes ...string) bool { - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.StringSlice("ovm.auth.requiredScopes.any", requiredScopes), - ) - - if ctx.Value(ScopeCheckBypassedContextKey{}) == true { - // this is always set when auth is bypassed - // set it here again to capture non-standard auth configs - span.SetAttributes(attribute.Bool("ovm.auth.bypass", true)) - - // Bypass all auth - return true - } - - claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) - if !ok { - span.SetAttributes(attribute.String("ovm.auth.missingClaims", "all")) - return false - } - - span.SetAttributes( - attribute.String("ovm.auth.tokenScopes", claims.Scope), - ) - - for _, scope := range requiredScopes { - if claims.HasScope(scope) { - span.SetAttributes(attribute.String("ovm.auth.usedClaim", scope)) - return true - } - } - return false -} - -var ErrNoClaims = errors.New("error extracting claims from token") - -// ExtractAccount Extracts the account name from a context -func ExtractAccount(ctx context.Context) (string, error) { - claims := ctx.Value(CustomClaimsContextKey{}) - - if claims == nil { - return "", ErrNoClaims - } - - return claims.(*CustomClaims).AccountName, nil -} - -// NewAuthMiddleware Creates new auth middleware. The options allow you to -// bypass the authentication process or not, but either way this middleware will -// set the `CustomClaimsContextKey` in the request context which allows you to -// use the `HasScopes()` function to check the scopes without having to worry -// about whether the server is using auth or not. -// -// If auth is not bypassed, then tokens will be validated using Auth0 and -// 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 MiddlewareConfig, next http.Handler) http.Handler { - processOverrides := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - options := []OverrideAuthOptionFunc{} - - if config.ScopeOverride != nil { - options = append(options, WithScope(*config.ScopeOverride)) - } - - if config.AccountOverride != nil { - options = append(options, WithAccount(*config.AccountOverride)) - } - - ctx := r.Context() - if len(options) > 0 { - ctx = OverrideAuth(r.Context(), options...) - } - - r = r.Clone(ctx) - - next.ServeHTTP(w, r) - }) - - return ensureValidTokenHandler(config, processOverrides) -} - -type OverrideAuthOptionFunc func(ctx context.Context) context.Context - -// Sets the scope in the context to the given value. This should be the value -// that would be embedded directly in the token, with each scope being separated -// by a space. -func WithScope(scope string) OverrideAuthOptionFunc { - return withCustomClaims(func(claims *CustomClaims) { - claims.Scope = scope - }) -} - -// Sets the account in the context to the given value. -func WithAccount(account string) OverrideAuthOptionFunc { - return withCustomClaims(func(claims *CustomClaims) { - claims.AccountName = account - }) -} - -// Sets the auth info in the context directly from the validated claims produced -// by the `github.com/auth0/go-jwt-middleware/v2/validator` package. This is -// essentially what the middleware already does when receiving a request, and -// therefore should only be used in exceptional circumstances, like testing, when the -// middleware is not being used. -// -// If this is being used, there is no need to use the `WithScope` or `WithAccount` -// options as the claims will be extracted directly from the validated claims. -func WithValidatedClaims(claims *validator.ValidatedClaims) OverrideAuthOptionFunc { - return func(ctx context.Context) context.Context { - customClaims := claims.CustomClaims.(*CustomClaims) - ctx = context.WithValue(ctx, jwtmiddleware.ContextKey{}, claims) - ctx = context.WithValue(ctx, CustomClaimsContextKey{}, customClaims) - ctx = context.WithValue(ctx, CurrentSubjectContextKey{}, claims.RegisteredClaims.Subject) - ctx = context.WithValue(ctx, AccountNameContextKey{}, customClaims.AccountName) - return ctx - } -} - -// Bypasses the scope check, meaning that `HasScopes()` and `HasAllScopes` will -// always return true. This is useful for testing. -func WithBypassScopeCheck() OverrideAuthOptionFunc { - return func(ctx context.Context) context.Context { - return context.WithValue(ctx, ScopeCheckBypassedContextKey{}, true) - } -} - -// Overrides the authentication that is currently stored in the context. This -// can only be used within a single process, and doesn't mean that the overrides -// set here will be passed on if you are using `NewAuthenticatedClient` to pass -// through auth. It is however useful for testing, or for calling other handlers -// within the same process. -func OverrideAuth(ctx context.Context, opts ...OverrideAuthOptionFunc) context.Context { - for _, opt := range opts { - ctx = opt(ctx) - } - return ctx -} - -func withCustomClaims(modify func(*CustomClaims)) OverrideAuthOptionFunc { - return func(ctx context.Context) context.Context { - i := ctx.Value(CustomClaimsContextKey{}) - var claims *CustomClaims - var newClaims CustomClaims - var ok bool - - if claims, ok = i.(*CustomClaims); ok { - // clone out the values to avoid sharing - newClaims = *claims - } - - modify(&newClaims) - - // Store the new claims in the context - ctx = context.WithValue(ctx, CustomClaimsContextKey{}, &newClaims) - ctx = context.WithValue(ctx, AccountNameContextKey{}, newClaims.AccountName) - - return ctx - } -} - -// ensureValidTokenHandler is a middleware that will check the validity of our -// JWT. -// -// This will fail if all of Auth0Domain, Auth0Audience and AuthCookieName are -// empty. -// -// This middleware also extract custom claims form the token and stores them in -// CustomClaimsContextKey -func ensureValidTokenHandler(config MiddlewareConfig, next http.Handler) http.Handler { - if config.Auth0Domain == "" && config.IssuerURL == "" && config.Auth0Audience == "" { - log.Fatalf("Auth0 configuration is missing") - } - - var issuerURL *url.URL - var err error - - if config.Auth0Domain != "" { - issuerURL, err = url.Parse("https://" + config.Auth0Domain + "/") - } else { - issuerURL, err = url.Parse(config.IssuerURL) - } - if err != nil { - log.Fatalf("Failed to parse the issuer url: %v", err) - } - - provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute) - - jwtValidator, err := validator.New( - provider.KeyFunc, - validator.RS256, - issuerURL.String(), - []string{config.Auth0Audience}, - validator.WithCustomClaims( - func() validator.CustomClaims { - return &CustomClaims{} - }, - ), - validator.WithAllowedClockSkew(time.Minute), - ) - if err != nil { - log.Fatalf("Failed to set up the jwt validator") - } - - errorHandler := func(w http.ResponseWriter, r *http.Request, err error) { - // copied from auth0's DefaultErrorHandler, but with some extra logging and reporting - span := trace.SpanFromContext(r.Context()) - span.SetAttributes( - attribute.String("ovm.auth.error", err.Error()), - attribute.String("ovm.auth.audience", config.Auth0Audience), - attribute.String("ovm.auth.domain", config.Auth0Domain), - attribute.String("ovm.auth.expectedIssuer", issuerURL.String()), - ) - - // Check if this is a Connect/gRPC request by looking at the Content-Type header - // Connect requests use content types like: - // - application/connect+proto - // - application/connect+json - // - application/grpc (base type without suffix) - // - application/grpc+proto - // - application/grpc+json - // For these requests, we should not set Content-Type: application/json - // as it will cause content-type mismatch errors on the client side - contentType := r.Header.Get("Content-Type") - isConnectRequest := strings.HasPrefix(contentType, "application/connect+") || - strings.HasPrefix(contentType, "application/grpc") - - // Only set JSON content-type for non-Connect requests - if !isConnectRequest { - w.Header().Set("Content-Type", "application/json") - } - - switch { - case errors.Is(err, jwtmiddleware.ErrJWTMissing): - // since connectrpc would translate the original `BadRequest` to a - // `CodeInternal` instead of something sensible, we also need to - // return StatusUnauthorized here, to provide the correct status - // code to the client. - w.WriteHeader(http.StatusUnauthorized) - if !isConnectRequest { - _, _ = w.Write([]byte(`{"message":"JWT is missing."}`)) - } - case errors.Is(err, jwtmiddleware.ErrJWTInvalid): - w.WriteHeader(http.StatusUnauthorized) - if !isConnectRequest { - _, _ = w.Write([]byte(`{"message":"JWT is invalid."}`)) - } - default: - span.SetStatus(codes.Error, "Something went wrong while checking the JWT") - sentry.CaptureException(err) - - w.WriteHeader(http.StatusInternalServerError) - if !isConnectRequest { - _, _ = w.Write([]byte(`{"message":"Something went wrong while checking the JWT."}`)) - } - } - } - - // Set up token extractors based on what env vars are available - extractors := []jwtmiddleware.TokenExtractor{ - jwtmiddleware.AuthHeaderTokenExtractor, - } - - for _, cookieName := range config.AuthCookieNames { - extractors = append(extractors, jwtmiddleware.CookieTokenExtractor(cookieName)) - } - - tokenExtractor := jwtmiddleware.MultiTokenExtractor(extractors...) - - middleware := jwtmiddleware.New( - jwtValidator.ValidateToken, - jwtmiddleware.WithErrorHandler(errorHandler), - jwtmiddleware.WithTokenExtractor(tokenExtractor), - ) - - jwtValidationMiddleware := middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // extract account name and setup otel attributes after the JWT was validated, but before the actual handler runs - claims := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims) - - token, err := tokenExtractor(r) - // we should never hit this as the middleware wouldn't call the handler - if err != nil { - // This is not ErrJWTMissing because an error here means that the - // tokenExtractor had an error and _not_ that the token was missing. - errorHandler(w, r, fmt.Errorf("error extracting token: %w", err)) - return - } - - customClaims := claims.CustomClaims.(*CustomClaims) - if customClaims == nil { - errorHandler(w, r, fmt.Errorf("couldn't get claims from: %v", claims)) - return - } - - ctx := r.Context() - - // note that the values are looked up in last-in-first-out order, so - // there is an absolutely minor perf optimisation to have the context - // values set in ascending order of access frequency. - ctx = context.WithValue(ctx, UserTokenContextKey{}, token) - ctx = context.WithValue(ctx, CustomClaimsContextKey{}, customClaims) - ctx = context.WithValue(ctx, CurrentSubjectContextKey{}, claims.RegisteredClaims.Subject) - ctx = context.WithValue(ctx, AccountNameContextKey{}, customClaims.AccountName) - - trace.SpanFromContext(ctx).SetAttributes( - attribute.String("ovm.auth.accountName", customClaims.AccountName), - attribute.Int64("ovm.auth.expiry", claims.RegisteredClaims.Expiry), - attribute.String("ovm.auth.scopes", customClaims.Scope), - // subject is the auth0 client id or user id - attribute.String("ovm.auth.subject", claims.RegisteredClaims.Subject), - ) - - // if its a service impersonating an account, we should mark it as impersonation - if strings.HasSuffix(claims.RegisteredClaims.Subject, "@clients") { - trace.SpanFromContext(ctx).SetAttributes( - attribute.Bool("ovm.auth.impersonation", true), - ) - } - - r = r.Clone(ctx) - - next.ServeHTTP(w, r) - })) - - // Basically what I need to do here is I need to have a middleware that - // checks for bypassing, then passes on to middleware.checkJWT. - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - span := trace.SpanFromContext(ctx) - - var shouldBypass bool - - // If config.BypassAuth is true then bypass - if config.BypassAuth { - shouldBypass = true - } - - // If we aren't bypassing always and we have a regex then check if we - // should bypass - if !shouldBypass && config.BypassAuthForPaths != nil { - shouldBypass = config.BypassAuthForPaths.MatchString(r.URL.Path) - if shouldBypass { - span.SetAttributes(attribute.String("ovm.auth.bypassedPath", r.URL.Path)) - } - } - - span.SetAttributes(attribute.Bool("ovm.auth.bypass", shouldBypass)) - - if shouldBypass { - ctx = OverrideAuth(ctx, WithBypassScopeCheck()) - - r = r.Clone(ctx) - - // Call the next handler without adding any JWT validation - next.ServeHTTP(w, r) - } else { - // Otherwise we need to inject the JWT validation middleware - jwtValidationMiddleware.ServeHTTP(w, r) - } - }) -} - -// CustomClaims contains custom data we want from the token. -type CustomClaims struct { - Scope string `json:"scope"` - AccountName string `json:"https://api.overmind.tech/account-name"` -} - -// HasScope checks whether our claims have a specific scope. -func (c CustomClaims) HasScope(expectedScope string) bool { - result := strings.Split(c.Scope, " ") - for i := range result { - if result[i] == expectedScope { - return true - } - } - - return false -} - -// Validate does nothing for this example, but we need -// it to satisfy validator.CustomClaims interface. -func (c CustomClaims) Validate(ctx context.Context) error { - return nil -} diff --git a/auth/middleware_test.go b/auth/middleware_test.go deleted file mode 100644 index 0e18fd26..00000000 --- a/auth/middleware_test.go +++ /dev/null @@ -1,792 +0,0 @@ -package auth - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "regexp" - "testing" - "time" - - "github.com/auth0/go-jwt-middleware/v2/validator" - "github.com/go-jose/go-jose/v4" - "github.com/go-jose/go-jose/v4/jwt" - log "github.com/sirupsen/logrus" -) - -func TestHasScopes(t *testing.T) { - t.Run("with auth bypassed", func(t *testing.T) { - t.Parallel() - - ctx := OverrideAuth(context.Background(), WithBypassScopeCheck()) - - pass := HasAllScopes(ctx, "test") - - if !pass { - t.Error("expected to allow since auth is bypassed") - } - }) - - t.Run("with good scopes", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAllScopes(ctx, "test") - - if !pass { - t.Error("expected to allow since `test` scope is present") - } - }) - - t.Run("with multiple good scopes", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAllScopes(ctx, "test", "foo") - - if !pass { - t.Error("expected to allow since `test` scope is present") - } - }) - - t.Run("with bad scopes", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAllScopes(ctx, "baz") - - if pass { - t.Error("expected to deny since `baz` scope is not present") - } - }) - - t.Run("with one scope missing", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAllScopes(ctx, "test", "baz") - - if pass { - t.Error("expected to deny since `baz` scope is not present") - } - }) - - t.Run("with any scopes", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAnyScopes(ctx, "fail", "foo") - - if !pass { - t.Error("expected to allow since `foo` scope is present") - } - }) - - t.Run("without any scopes", func(t *testing.T) { - t.Parallel() - - account := "foo" - scope := "test foo bar" - ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) - - pass := HasAnyScopes(ctx, "fail", "fail harder") - - if pass { - t.Error("expected to deny since no matching scope is present") - } - }) -} - -func TestNewAuthMiddleware(t *testing.T) { - server, err := NewTestJWTServer() - if err != nil { - t.Fatal(err) - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - jwksURL := server.Start(ctx) - - defaultConfig := MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - } - - bypassHealthConfig := MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - BypassAuthForPaths: regexp.MustCompile("/health"), - } - - correctAccount := "test" - correctScope := "test:pass" - - tests := []struct { - Name string - TokenOptions *TestTokenOptions - ExpectedCode int - AuthConfig MiddlewareConfig - Path string - }{ - { - Name: "with expired token", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(-time.Hour), - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusUnauthorized, - }, - { - Name: "with wrong audience", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://something.not.expected"}, - Expiry: time.Now().Add(time.Hour), - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusUnauthorized, - }, - { - Name: "with insufficient scopes", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:fail", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusUnauthorized, - }, - { - Name: "with correct scopes but wrong account", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "fail", - Scope: "test:pass", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusUnauthorized, - }, - { - Name: "with correct scopes and account", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusOK, - }, - { - Name: "with the correct scope and many others", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass test:fail foo:bar something", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusOK, - }, - { - Name: "with many audiences and many scopes", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech", "https://api.overmind.tech/other"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass test:other", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusOK, - }, - { - Name: "with many audiences and one scope", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech", "https://api.overmind.tech/other"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass", - }, - }, - AuthConfig: defaultConfig, - ExpectedCode: http.StatusOK, - }, - { - Name: "with good token and some bypassed paths", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass", - }, - }, - AuthConfig: MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - BypassAuthForPaths: regexp.MustCompile("/health"), - }, - ExpectedCode: http.StatusOK, - }, - { - Name: "with no token on a non-bypassed path", - Path: "/", - AuthConfig: bypassHealthConfig, - ExpectedCode: http.StatusUnauthorized, - }, - { - Name: "with no token on a bypassed path", - Path: "/health", - AuthConfig: bypassHealthConfig, - ExpectedCode: http.StatusOK, - }, - { - Name: "with bad token on a non-bypassed path", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:fail", - }, - }, - ExpectedCode: http.StatusUnauthorized, - AuthConfig: bypassHealthConfig, - }, - { - Name: "with bad token on a bypassed path", - Path: "/health", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:fail", - }, - }, - ExpectedCode: http.StatusOK, - AuthConfig: bypassHealthConfig, - }, - { - Name: "with a good token and bypassed auth", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass", - }, - }, - ExpectedCode: http.StatusOK, - AuthConfig: MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - BypassAuth: true, - }, - }, - { - Name: "with a bad token and bypassed auth", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(-time.Hour), // expired - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:pass", - }, - }, - ExpectedCode: http.StatusOK, - AuthConfig: MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - BypassAuth: true, - }, - }, - { - Name: "with account override", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "bad", - Scope: "test:pass", - }, - }, - ExpectedCode: http.StatusOK, - AuthConfig: MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - AccountOverride: &correctAccount, - }, - }, - { - Name: "with scope override", - Path: "/", - TokenOptions: &TestTokenOptions{ - Audience: []string{"https://api.overmind.tech"}, - Expiry: time.Now().Add(time.Hour), - CustomClaims: CustomClaims{ - AccountName: "test", - Scope: "test:fail", - }, - }, - ExpectedCode: http.StatusOK, - AuthConfig: MiddlewareConfig{ - IssuerURL: jwksURL, - Auth0Audience: "https://api.overmind.tech", - ScopeOverride: &correctScope, - }, - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - handler := NewAuthMiddleware(test.AuthConfig, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - // This is a test handler that always does the same thing, it checks - // that the account is set to the correct value and that the user has - // the test:pass scope - if !HasAnyScopes(ctx, "test:pass") { - w.WriteHeader(http.StatusUnauthorized) - _, err := w.Write([]byte("missing required scope")) - if err != nil { - t.Error(err) - } - return - } - - if ctx.Value(ScopeCheckBypassedContextKey{}) == true { - // If we are bypassing auth then we don't want to check the account - } else { - claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - _, err := fmt.Fprintf(w, "expected *CustomClaims in context, got %T", ctx.Value(CustomClaimsContextKey{})) - if err != nil { - t.Error(err) - } - return - } - - if claims.AccountName != "test" { - w.WriteHeader(http.StatusUnauthorized) - _, err := fmt.Fprintf(w, "expected account to be 'test', but was '%s'", claims.AccountName) - if err != nil { - t.Error(err) - } - return - } - } - })) - - rr := httptest.NewRecorder() - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, test.Path, nil) - if err != nil { - t.Fatal(err) - } - - if test.TokenOptions != nil { - // Create a test Token - token, err := server.GenerateJWT(test.TokenOptions) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - } - - handler.ServeHTTP(rr, req) - - if rr.Code != test.ExpectedCode { - t.Errorf("expected status code %d, but got %d", test.ExpectedCode, rr.Code) - t.Error(rr.Body.String()) - } - }) - } -} - -func TestOverrideAuth(t *testing.T) { - tests := []struct { - Name string - Options []OverrideAuthOptionFunc - HasAllScopes []string - HasAccountName string - }{ - { - Name: "with account override", - Options: []OverrideAuthOptionFunc{ - WithAccount("test"), - }, - HasAccountName: "test", - }, - { - Name: "with scope override", - Options: []OverrideAuthOptionFunc{ - WithScope("test:pass"), - }, - HasAllScopes: []string{"test:pass"}, - }, - { - Name: "with account and scope override", - Options: []OverrideAuthOptionFunc{ - WithAccount("test"), - WithScope("test:pass"), - }, - HasAccountName: "test", - HasAllScopes: []string{"test:pass"}, - }, - { - Name: "with account and scope override in reverse order", - Options: []OverrideAuthOptionFunc{ - WithScope("test:pass"), - WithAccount("test"), - }, - HasAccountName: "test", - HasAllScopes: []string{"test:pass"}, - }, - { - Name: "with validated custom claims", - Options: []OverrideAuthOptionFunc{ - WithValidatedClaims(&validator.ValidatedClaims{ - CustomClaims: &CustomClaims{ - Scope: "test:pass", - AccountName: "test", - }, - RegisteredClaims: validator.RegisteredClaims{ - Issuer: "https://api.overmind.tech", - Subject: "test", - Audience: []string{"https://api.overmind.tech"}, - ID: "test", - }, - }), - }, - HasAccountName: "test", - HasAllScopes: []string{"test:pass"}, - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - ctx := context.Background() - - ctx = OverrideAuth(ctx, test.Options...) - - if test.HasAccountName != "" { - accountName, err := ExtractAccount(ctx) - if err != nil { - t.Error(err) - } - - if accountName != test.HasAccountName { - t.Errorf("expected account name to be %s, but got %s", test.HasAccountName, accountName) - } - } - - for _, scope := range test.HasAllScopes { - if !HasAllScopes(ctx, scope) { - t.Errorf("expected to have scope %s, but did not", scope) - } - } - }) - } -} - -func BenchmarkAuthMiddleware(b *testing.B) { - config := MiddlewareConfig{ - Auth0Domain: "auth.overmind-demo.com", - Auth0Audience: "https://api.overmind.tech", - } - - okHandler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - } - - handler := NewAuthMiddleware(config, http.HandlerFunc(okHandler)) - - // Reduce logging - log.SetLevel(log.FatalLevel) - - for range b.N { - // Create a request to pass to our handler. We don't have any query parameters for now, so we'll - // pass 'nil' as the third parameter. - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) - - if err != nil { - b.Fatal(err) - } - - // Set to a known bad JWT (this JWT is garbage don't freak out) - req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InBpQWx1Q1FkQTB4MTNweG1JQzM4dyJ9.eyJodHRwczovL2FwaS5vdmVybWluZC50ZWNoL2FjY291bnQtbmFtZSI6IlRFU1QiLCJpc3MiOiJodHRwczovL29tLWRvZ2Zvb2QuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfFRFU1QiLCJhdWQiOlsiaHR0cHM6Ly9hcGkuZGYub3Zlcm1pbmQtZGVtby5jb20iLCJodHRwczovL29tLWRvZ2Zvb2QuZXUuYXV0aDAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTcxNDA0MjA5MiwiZXhwIjoxNzE0MTI4NDkyLCJzY29wZSI6Im1hbnkgc2NvcGVzIiwiYXpwIjoiVEVTVCJ9.cEEh8jVnEItZel4SoyPybLUg7sArwduCrmSJHMz3YNRfzpRl9lxry39psuDUHFKdgOoNVxUv3Lgm-JWG-9uddCKYOW_zQxEvQvj6o8tcpQkmBZBlc8huG21dLPz7yrPhogVAcApLjdHf1fqii9EHxQegxch9FHlyfF7Xii5t9Hus62l4vdZ5dVWaIuiOLtcbG_hLxl9yqBf5tzN8eEC-Pa1SoAciRPesqH4AARfKyBFBhN774Fu3NzfNtW3wD_ASvnv7aFwzblS8ff5clqdTr2GuuJKdIPcmjQV2LaGSExHg2riCryf5guAhitAuwhugssW__STQmwp8dJmhifs7DA") - - // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. - rr := httptest.NewRecorder() - - handler.ServeHTTP(rr, req) - - if rr.Code != http.StatusUnauthorized { - b.Errorf("expected status code %d, but got %d", http.StatusUnauthorized, rr.Code) - } - } -} - -// Creates a new server that mints real, signed JWTs for testing. It even -// provides its own JWKS endpoint so they can be externally validated. To start -// the JWKS server you should call .Start() -func NewTestJWTServer() (*TestJWTServer, error) { - // Generate an RSA private key - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - return nil, err - } - - // Wrap this in a JWK object - jwk := jose.JSONWebKey{ - Key: privateKey, - KeyID: "test-signing-key", - Algorithm: string(jose.RS256), - } - - // Create a signer that will sign all of our tokens - signingKey := jose.SigningKey{ - Algorithm: jose.RS256, - Key: jwk, - } - signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) - - if err != nil { - return nil, err - } - - // Export the public key to be used for validation - pubJwk := jwk.Public() - - keySet := jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{pubJwk}, - } - - return &TestJWTServer{ - signer: signer, - privateKey: jwk, - publicKey: pubJwk, - publicKeySet: keySet, - }, nil -} - -// This server is used to mint JWTs for testing purposes. It is basically the -// same as Auth0 when it comes to creating tokens in that it returns a JWKS -// endpoint that can be used to validate the tokens it creates, and the tokens -// use the same algorithm as Auth0 -type TestJWTServer struct { - signer jose.Signer - privateKey jose.JSONWebKey - publicKey jose.JSONWebKey - publicKeySet jose.JSONWebKeySet - server *httptest.Server -} - -type TestTokenOptions struct { - Audience []string - Expiry time.Time - - CustomClaims -} - -func (s *TestJWTServer) GenerateJWT(options *TestTokenOptions) (string, error) { - builder := jwt.Signed(s.signer) - - builder = builder.Claims(jwt.Claims{ - Issuer: s.server.URL, - Subject: "test", - Audience: jwt.Audience(options.Audience), - Expiry: jwt.NewNumericDate(options.Expiry), - IssuedAt: jwt.NewNumericDate(time.Now()), - }) - - builder = builder.Claims(options.CustomClaims) - - return builder.Serialize() -} - -// Starts the server in the background, the server will exit when the context is -// cancelled. Returns the URL of the server -func (s *TestJWTServer) Start(ctx context.Context) string { - s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/.well-known/openid-configuration": - // The endpoint tells the validating party where to find the JWKS, - // this contains our public keys that can be used to validate tokens - // issued by our server - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - _, err := fmt.Fprintf(w, `{"jwks_uri": "%s/.well-known/jwks.json"}`, s.server.URL) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - case "/.well-known/jwks.json": - // Write the public key set as JSON - w.Header().Set("Content-Type", "application/json") - - b, err := json.MarshalIndent(s.publicKeySet, "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - _, err = w.Write(b) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } - })) - - go func() { - <-ctx.Done() - s.server.Close() - }() - - return s.server.URL -} - -func TestConnectErrorHandling(t *testing.T) { - // Create a test JWT server - server, err := NewTestJWTServer() - if err != nil { - t.Fatal(err) - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - jwksURL := server.Start(ctx) - - // Create the middleware - handler := NewAuthMiddleware(MiddlewareConfig{ - Auth0Domain: "", - Auth0Audience: "test", - IssuerURL: jwksURL, - }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - tests := []struct { - Name string - ContentType string - ExpectJSONResponse bool - ExpectContentType string - }{ - { - Name: "Regular JSON request without auth", - ContentType: "application/json", - ExpectJSONResponse: true, - ExpectContentType: "application/json", - }, - { - Name: "Connect proto request without auth", - ContentType: "application/connect+proto", - ExpectJSONResponse: false, - ExpectContentType: "", - }, - { - Name: "Connect json request without auth", - ContentType: "application/connect+json", - ExpectJSONResponse: false, - ExpectContentType: "", - }, - { - Name: "gRPC base request without auth", - ContentType: "application/grpc", - ExpectJSONResponse: false, - ExpectContentType: "", - }, - { - Name: "gRPC proto request without auth", - ContentType: "application/grpc+proto", - ExpectJSONResponse: false, - ExpectContentType: "", - }, - { - Name: "gRPC json request without auth", - ContentType: "application/grpc+json", - ExpectJSONResponse: false, - ExpectContentType: "", - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - rr := httptest.NewRecorder() - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/test", nil) - if err != nil { - t.Fatal(err) - } - - // Set the Content-Type header - req.Header.Set("Content-Type", test.ContentType) - - // Don't set any auth token, so it will fail auth - handler.ServeHTTP(rr, req) - - // Should return 401 Unauthorized - if rr.Code != http.StatusUnauthorized { - t.Errorf("expected status code %d, but got %d", http.StatusUnauthorized, rr.Code) - } - - // Check Content-Type header - contentType := rr.Header().Get("Content-Type") - if test.ExpectContentType != contentType { - t.Errorf("expected Content-Type header to be '%s', but got '%s'", test.ExpectContentType, contentType) - } - - // Check if response has JSON body - hasJSONBody := len(rr.Body.Bytes()) > 0 && contentType == "application/json" - if test.ExpectJSONResponse != hasJSONBody { - t.Errorf("expected JSON response: %v, but got: %v (body length: %d)", test.ExpectJSONResponse, hasJSONBody, len(rr.Body.Bytes())) - } - }) - } -} diff --git a/auth/nats.go b/auth/nats.go deleted file mode 100644 index 2e3ef9ee..00000000 --- a/auth/nats.go +++ /dev/null @@ -1,251 +0,0 @@ -package auth - -import ( - "errors" - "strings" - "time" - - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" - - "github.com/nats-io/nats.go" -) - -// Defaults -const MaxReconnectsDefault = -1 -const ReconnectWaitDefault = 1 * time.Second -const ReconnectJitterDefault = 5 * time.Second -const ConnectionTimeoutDefault = 10 * time.Second - -type MaxRetriesError struct{} - -func (m MaxRetriesError) Error() string { - return "maximum retries reached" -} - -func fieldsFromConn(c *nats.Conn) log.Fields { - fields := log.Fields{} - - if c != nil { - fields["ovm.nats.address"] = c.ConnectedAddr() - fields["ovm.nats.reconnects"] = c.Reconnects - fields["ovm.nats.serverId"] = c.ConnectedServerId() - fields["ovm.nats.url"] = c.ConnectedUrl() - - if c.LastError() != nil { - fields["ovm.nats.lastError"] = c.LastError() - } - } - - return fields -} - -var DisconnectErrHandlerDefault = func(c *nats.Conn, err error) { - fields := fieldsFromConn(c) - - if err != nil { - log.WithError(err).WithFields(fields).Error("NATS disconnected") - } else { - log.WithFields(fields).Debug("NATS disconnected") - } -} - -var ConnectHandlerDefault = func(c *nats.Conn) { - fields := fieldsFromConn(c) - - log.WithFields(fields).Debug("NATS connected") -} -var ReconnectHandlerDefault = func(c *nats.Conn) { - fields := fieldsFromConn(c) - - log.WithFields(fields).Debug("NATS reconnected") -} -var ClosedHandlerDefault = func(c *nats.Conn) { - fields := fieldsFromConn(c) - - log.WithFields(fields).Debug("NATS connection closed") -} -var LameDuckModeHandlerDefault = func(c *nats.Conn) { - fields := fieldsFromConn(c) - - log.WithFields(fields).Debug("NATS server has entered lame duck mode") -} -var ErrorHandlerDefault = func(c *nats.Conn, s *nats.Subscription, err error) { - fields := fieldsFromConn(c) - - if s != nil { - fields["ovm.nats.subject"] = s.Subject - fields["ovm.nats.queue"] = s.Queue - } - - log.WithFields(fields).WithError(err).Error("NATS error") -} - -type NATSOptions struct { - Servers []string // List of server to connect to - ConnectionName string // The client name - MaxReconnects int // The maximum number of reconnect attempts - ConnectionTimeout time.Duration // The timeout for Dial on a connection - ReconnectWait time.Duration // Wait time between reconnect attempts - ReconnectJitter time.Duration // The upper bound of a random delay added ReconnectWait - TokenClient TokenClient // The client to use to get NATS tokens - ConnectHandler nats.ConnHandler // Runs when NATS is connected - DisconnectErrHandler nats.ConnErrHandler // Runs when NATS is disconnected - ReconnectHandler nats.ConnHandler // Runs when NATS has successfully reconnected - ClosedHandler nats.ConnHandler // Runs when NATS will no longer be connected - ErrorHandler nats.ErrHandler // Runs when there is a NATS error - LameDuckModeHandler nats.ConnHandler // Runs when the connection enters "lame duck mode" - AdditionalOptions []nats.Option // Addition options to pass to the connection - NumRetries int // How many times to retry connecting initially, use -1 to retry indefinitely - RetryDelay time.Duration // Delay between connection attempts -} - -// Creates a copy of the NATS options, **excluding** the token client as these -// should not be re-used -func (o NATSOptions) Copy() NATSOptions { - return NATSOptions{ - Servers: o.Servers, - ConnectionName: o.ConnectionName, - MaxReconnects: o.MaxReconnects, - ConnectionTimeout: o.ConnectionTimeout, - ReconnectWait: o.ReconnectWait, - ReconnectJitter: o.ReconnectJitter, - ConnectHandler: o.ConnectHandler, - DisconnectErrHandler: o.DisconnectErrHandler, - ReconnectHandler: o.ReconnectHandler, - ClosedHandler: o.ClosedHandler, - LameDuckModeHandler: o.LameDuckModeHandler, - ErrorHandler: o.ErrorHandler, - AdditionalOptions: o.AdditionalOptions, - NumRetries: o.NumRetries, - RetryDelay: o.RetryDelay, - } -} - -// ToNatsOptions Converts the struct to connection string and a set of NATS -// options -func (o NATSOptions) ToNatsOptions() (string, []nats.Option) { - serverString := strings.Join(o.Servers, ",") - options := []nats.Option{} - - if o.ConnectionName != "" { - options = append(options, nats.Name(o.ConnectionName)) - } - - if o.MaxReconnects != 0 { - options = append(options, nats.MaxReconnects(o.MaxReconnects)) - } else { - options = append(options, nats.MaxReconnects(MaxReconnectsDefault)) - } - - if o.ConnectionTimeout != 0 { - options = append(options, nats.Timeout(o.ConnectionTimeout)) - } else { - options = append(options, nats.Timeout(ConnectionTimeoutDefault)) - } - - if o.ReconnectWait != 0 { - options = append(options, nats.ReconnectWait(o.ReconnectWait)) - } else { - options = append(options, nats.ReconnectWait(ReconnectWaitDefault)) - } - - if o.ReconnectJitter != 0 { - options = append(options, nats.ReconnectJitter(o.ReconnectJitter, o.ReconnectJitter)) - } else { - options = append(options, nats.ReconnectJitter(ReconnectJitterDefault, ReconnectJitterDefault)) - } - - if o.TokenClient != nil { - options = append(options, nats.UserJWT(func() (string, error) { - return o.TokenClient.GetJWT() - }, o.TokenClient.Sign)) - } - - if o.ConnectHandler != nil { - options = append(options, nats.ConnectHandler(o.ConnectHandler)) - } else { - options = append(options, nats.ConnectHandler(ConnectHandlerDefault)) - } - - if o.DisconnectErrHandler != nil { - options = append(options, nats.DisconnectErrHandler(o.DisconnectErrHandler)) - } else { - options = append(options, nats.DisconnectErrHandler(DisconnectErrHandlerDefault)) - } - - if o.ReconnectHandler != nil { - options = append(options, nats.ReconnectHandler(o.ReconnectHandler)) - } else { - options = append(options, nats.ReconnectHandler(ReconnectHandlerDefault)) - } - - if o.ClosedHandler != nil { - options = append(options, nats.ClosedHandler(o.ClosedHandler)) - } else { - options = append(options, nats.ClosedHandler(ClosedHandlerDefault)) - } - - if o.LameDuckModeHandler != nil { - options = append(options, nats.LameDuckModeHandler(o.LameDuckModeHandler)) - } else { - options = append(options, nats.LameDuckModeHandler(LameDuckModeHandlerDefault)) - } - - if o.ErrorHandler != nil { - options = append(options, nats.ErrorHandler(o.ErrorHandler)) - } else { - options = append(options, nats.ErrorHandler(ErrorHandlerDefault)) - } - - options = append(options, o.AdditionalOptions...) - - return serverString, options -} - -// ConnectAs Connects to NATS using the supplied options, including retrying if -// unavailable -func (o NATSOptions) Connect() (sdp.EncodedConnection, error) { - servers, opts := o.ToNatsOptions() - - var triesLeft int - - if o.NumRetries >= 0 { - triesLeft = o.NumRetries + 1 - } else { - triesLeft = -1 - } - - var nc *nats.Conn - var err error - - for triesLeft != 0 { - triesLeft-- - lf := log.Fields{ - "servers": servers, - "triesLeft": triesLeft, - } - log.WithFields(lf).Info("NATS connecting") - - nc, err = nats.Connect( - servers, - opts..., - ) - - if err != nil && triesLeft != 0 { - log.WithError(err).WithFields(lf).Error("Error connecting to NATS") - time.Sleep(o.RetryDelay) - continue - } - - log.WithFields(lf).Info("NATS connected") - break - } - - if err != nil { - err = errors.Join(err, MaxRetriesError{}) - return &sdp.EncodedConnectionImpl{}, err - } - - return &sdp.EncodedConnectionImpl{Conn: nc}, nil -} diff --git a/auth/nats_test.go b/auth/nats_test.go deleted file mode 100644 index 7be1fae4..00000000 --- a/auth/nats_test.go +++ /dev/null @@ -1,423 +0,0 @@ -package auth - -import ( - "context" - "errors" - "os" - "testing" - "time" - - "github.com/google/uuid" - "github.com/nats-io/jwt/v2" - "github.com/nats-io/nats.go" - "github.com/nats-io/nkeys" - "github.com/overmindtech/cli/sdp-go" -) - -func TestToNatsOptions(t *testing.T) { - t.Run("with defaults", func(t *testing.T) { - o := NATSOptions{} - - expectedOptions, err := optionsToStruct([]nats.Option{ - nats.Timeout(ConnectionTimeoutDefault), - nats.MaxReconnects(MaxReconnectsDefault), - nats.ReconnectWait(ReconnectWaitDefault), - nats.ReconnectJitter(ReconnectJitterDefault, ReconnectJitterDefault), - nats.ConnectHandler(ConnectHandlerDefault), - nats.DisconnectErrHandler(DisconnectErrHandlerDefault), - nats.ReconnectHandler(ReconnectHandlerDefault), - nats.ClosedHandler(ClosedHandlerDefault), - nats.LameDuckModeHandler(LameDuckModeHandlerDefault), - nats.ErrorHandler(ErrorHandlerDefault), - }) - if err != nil { - t.Fatal(err) - } - - server, options := o.ToNatsOptions() - - if server != "" { - t.Error("Expected server to be empty") - } - - actualOptions, err := optionsToStruct(options) - if err != nil { - t.Fatal(err) - } - - if expectedOptions.MaxReconnect != actualOptions.MaxReconnect { - t.Errorf("Expected MaxReconnect to be %v, got %v", expectedOptions.MaxReconnect, actualOptions.MaxReconnect) - } - - if expectedOptions.Timeout != actualOptions.Timeout { - t.Errorf("Expected ConnectionTimeout to be %v, got %v", expectedOptions.Timeout, actualOptions.Timeout) - } - - if expectedOptions.ReconnectWait != actualOptions.ReconnectWait { - t.Errorf("Expected ReconnectWait to be %v, got %v", expectedOptions.ReconnectWait, actualOptions.ReconnectWait) - } - - if expectedOptions.ReconnectJitter != actualOptions.ReconnectJitter { - t.Errorf("Expected ReconnectJitter to be %v, got %v", expectedOptions.ReconnectJitter, actualOptions.ReconnectJitter) - } - - // TokenClient - if expectedOptions.UserJWT != nil || expectedOptions.SignatureCB != nil { - t.Error("Expected TokenClient to be nil") - } - - if actualOptions.DisconnectedErrCB == nil { - t.Error("Expected DisconnectedErrCB to be non-nil") - } - - if actualOptions.ReconnectedCB == nil { - t.Error("Expected ReconnectedCB to be non-nil") - } - - if actualOptions.ClosedCB == nil { - t.Error("Expected ClosedCB to be non-nil") - } - - if actualOptions.LameDuckModeHandler == nil { - t.Error("Expected LameDuckModeHandler to be non-nil") - } - - if actualOptions.AsyncErrorCB == nil { - t.Error("Expected AsyncErrorCB to be non-nil") - } - }) - - t.Run("with non-defaults", func(t *testing.T) { - var connectHandlerUsed bool - var disconnectErrHandlerUsed bool - var reconnectHandlerUsed bool - var closedHandlerUsed bool - var lameDuckModeHandlerUsed bool - var errorHandlerUsed bool - - o := NATSOptions{ - Servers: []string{"one", "two"}, - ConnectionName: "foo", - MaxReconnects: 999, - ReconnectWait: 999, - ReconnectJitter: 999, - ConnectHandler: func(c *nats.Conn) { connectHandlerUsed = true }, - DisconnectErrHandler: func(c *nats.Conn, err error) { disconnectErrHandlerUsed = true }, - ReconnectHandler: func(c *nats.Conn) { reconnectHandlerUsed = true }, - ClosedHandler: func(c *nats.Conn) { closedHandlerUsed = true }, - LameDuckModeHandler: func(c *nats.Conn) { lameDuckModeHandlerUsed = true }, - ErrorHandler: func(c *nats.Conn, s *nats.Subscription, err error) { errorHandlerUsed = true }, - } - - expectedOptions, err := optionsToStruct([]nats.Option{ - nats.Name("foo"), - nats.MaxReconnects(999), - nats.ReconnectWait(999), - nats.ReconnectJitter(999, 999), - nats.DisconnectErrHandler(nil), - nats.ReconnectHandler(nil), - nats.ClosedHandler(nil), - nats.LameDuckModeHandler(nil), - nats.ErrorHandler(nil), - }) - if err != nil { - t.Fatal(err) - } - - server, options := o.ToNatsOptions() - - if server != "one,two" { - t.Errorf("Expected server to be one,two got %v", server) - } - - actualOptions, err := optionsToStruct(options) - if err != nil { - t.Fatal(err) - } - - if expectedOptions.MaxReconnect != actualOptions.MaxReconnect { - t.Errorf("Expected MaxReconnect to be %v, got %v", expectedOptions.MaxReconnect, actualOptions.MaxReconnect) - } - - if expectedOptions.ReconnectWait != actualOptions.ReconnectWait { - t.Errorf("Expected ReconnectWait to be %v, got %v", expectedOptions.ReconnectWait, actualOptions.ReconnectWait) - } - - if expectedOptions.ReconnectJitter != actualOptions.ReconnectJitter { - t.Errorf("Expected ReconnectJitter to be %v, got %v", expectedOptions.ReconnectJitter, actualOptions.ReconnectJitter) - } - - if actualOptions.DisconnectedErrCB != nil { - actualOptions.DisconnectedErrCB(nil, nil) - if !disconnectErrHandlerUsed { - t.Error("DisconnectErrHandler not used") - } - } else { - t.Error("Expected DisconnectedErrCB to non-nil") - } - - if actualOptions.ConnectedCB != nil { - actualOptions.ConnectedCB(nil) - if !connectHandlerUsed { - t.Error("ConnectHandler not used") - } - } else { - t.Error("Expected ConnectedCB to non-nil") - } - - if actualOptions.ReconnectedCB != nil { - actualOptions.ReconnectedCB(nil) - if !reconnectHandlerUsed { - t.Error("ReconnectHandler not used") - } - } else { - t.Error("Expected ReconnectedCB to non-nil") - } - - if actualOptions.ClosedCB != nil { - actualOptions.ClosedCB(nil) - if !closedHandlerUsed { - t.Error("ClosedHandler not used") - } - } else { - t.Error("Expected ClosedCB to non-nil") - } - - if actualOptions.LameDuckModeHandler != nil { - actualOptions.LameDuckModeHandler(nil) - if !lameDuckModeHandlerUsed { - t.Error("LameDuckModeHandler not used") - } - } else { - t.Error("Expected LameDuckModeHandler to non-nil") - } - - if actualOptions.AsyncErrorCB != nil { - actualOptions.AsyncErrorCB(nil, nil, nil) - if !errorHandlerUsed { - t.Error("ErrorHandler not used") - } - } else { - t.Error("Expected AsyncErrorCB to non-nil") - } - }) -} - -func TestNATSConnect(t *testing.T) { - if os.Getenv("CI") == "true" { - t.Skip("Skipping test in CI environment, missing nats token exchange server") - } - - t.Run("with a bad URL", func(t *testing.T) { - o := NATSOptions{ - Servers: []string{"nats://badname.dontresolve.com"}, - NumRetries: 5, - RetryDelay: 100 * time.Millisecond, - } - - start := time.Now() - - _, err := o.Connect() - - // Just sanity check the duration here, it should not be less than - // NumRetries * RetryDelay and it should be more than... Some larger - // number of seconds. This is very much dependant on how long it takes - // to not resolve the name - if time.Since(start) < 5*100*time.Millisecond { - t.Errorf("Reconnecting didn't take long enough, expected >0.5s got: %v", time.Since(start).String()) - } - - if time.Since(start) > 3*time.Second { - t.Errorf("Reconnecting took too long, expected <3s got: %v", time.Since(start).String()) - } - - var maxRetriesError MaxRetriesError - if !errors.As(err, &maxRetriesError) { - t.Errorf("Unknown error type %T: %v", err, err) - } - }) - - t.Run("with a bad URL, but a good token", func(t *testing.T) { - tk := GetTestOAuthTokenClient(t) - - startToken, err := tk.GetJWT() - if err != nil { - t.Fatal(err) - } - - o := NATSOptions{ - Servers: []string{"nats://badname.dontresolve.com"}, - TokenClient: tk, - NumRetries: 3, - RetryDelay: 100 * time.Millisecond, - } - - _, err = o.Connect() - - var maxRetriesError MaxRetriesError - if errors.As(err, &maxRetriesError) { - // Make sure we have only got one token, not three - currentToken, err := o.TokenClient.GetJWT() - if err != nil { - t.Fatal(err) - } - - if currentToken != startToken { - t.Error("Tokens have changed") - } - } else { - t.Errorf("Unknown error type %T", err) - } - }) - - t.Run("with a good URL", func(t *testing.T) { - o := NATSOptions{ - Servers: []string{ - "nats://nats:4222", - "nats://localhost:4222", - }, - NumRetries: 3, - RetryDelay: 100 * time.Millisecond, - } - - conn, err := o.Connect() - if err != nil { - t.Fatal(err) - } - - ValidateNATSConnection(t, conn) - }) - - t.Run("with a good URL but no retries", func(t *testing.T) { - o := NATSOptions{ - Servers: []string{ - "nats://nats:4222", - "nats://localhost:4222", - }, - } - - conn, err := o.Connect() - if err != nil { - t.Fatal(err) - } - - ValidateNATSConnection(t, conn) - }) - - t.Run("with a good URL and infinite retries", func(t *testing.T) { - o := NATSOptions{ - Servers: []string{ - "nats://nats:4222", - "nats://localhost:4222", - }, - NumRetries: -1, - RetryDelay: 100 * time.Millisecond, - } - - conn, err := o.Connect() - if err != nil { - t.Error(err) - } - - ValidateNATSConnection(t, conn) - }) -} - -func TestTokenRefresh(t *testing.T) { - if os.Getenv("CI") == "true" { - t.Skip("Skipping test in CI environment, missing nats token exchange server") - } - - tk := GetTestOAuthTokenClient(t) - - // Get a token - token, err := tk.GetJWT() - if err != nil { - t.Fatal(err) - } - - // Artificially set the expiry and replace the token - claims, err := jwt.DecodeUserClaims(token) - if err != nil { - t.Fatal(err) - } - - pair, err := nkeys.CreateAccount() - if err != nil { - t.Fatal(err) - } - - claims.Expires = time.Now().Add(-10 * time.Second).Unix() - tk.jwt, err = claims.Encode(pair) - expiredToken := tk.jwt - - if err != nil { - t.Error(err) - } - - // Get the token again - newToken, err := tk.GetJWT() - if err != nil { - t.Error(err) - } - - if expiredToken == newToken { - t.Error("token is unchanged") - } -} - -func ValidateNATSConnection(t *testing.T, ec sdp.EncodedConnection) { - t.Helper() - done := make(chan struct{}) - - sub, err := ec.Subscribe("test", sdp.NewQueryResponseHandler("test", func(ctx context.Context, qr *sdp.QueryResponse) { - rt, ok := qr.GetResponseType().(*sdp.QueryResponse_Response) - if !ok { - t.Errorf("Received unexpected message: %v", qr) - } - - if rt.Response.GetResponder() == "test" { - done <- struct{}{} - } - })) - if err != nil { - t.Error(err) - } - - ru := uuid.New() - err = ec.Publish(context.Background(), "test", sdp.NewQueryResponseFromResponse(&sdp.Response{ - Responder: "test", - ResponderUUID: ru[:], - State: sdp.ResponderState_COMPLETE, - })) - if err != nil { - t.Error(err) - } - - // Wait for the message to come back - select { - case <-done: - // Good - case <-time.After(500 * time.Millisecond): - t.Error("Didn't get message after 500ms") - } - - err = sub.Unsubscribe() - if err != nil { - t.Error(err) - } -} - -func optionsToStruct(options []nats.Option) (nats.Options, error) { - var o nats.Options - var err error - - for _, option := range options { - err = option(&o) - if err != nil { - return o, err - } - } - - return o, nil -} diff --git a/auth/tracing.go b/auth/tracing.go deleted file mode 100644 index d85f3bdd..00000000 --- a/auth/tracing.go +++ /dev/null @@ -1,18 +0,0 @@ -package auth - -import ( - "go.opentelemetry.io/otel" - semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - "go.opentelemetry.io/otel/trace" -) - -const ( - instrumentationName = "github.com/overmindtech/cli/auth" - instrumentationVersion = "0.0.1" -) - -var tracer = otel.GetTracerProvider().Tracer( - instrumentationName, - trace.WithInstrumentationVersion(instrumentationVersion), - trace.WithSchemaURL(semconv.SchemaURL), -) diff --git a/aws-source/adapters/adapterhelpers_always_get_source.go b/aws-source/adapters/adapterhelpers_always_get_source.go index 85c730ee..19c80f6f 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source.go +++ b/aws-source/adapters/adapterhelpers_always_get_source.go @@ -9,9 +9,9 @@ import ( "buf.build/go/protovalidate" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/sourcegraph/conc/pool" ) diff --git a/aws-source/adapters/adapterhelpers_always_get_source_test.go b/aws-source/adapters/adapterhelpers_always_get_source_test.go index 66d1b10b..4801e8f6 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source_test.go +++ b/aws-source/adapters/adapterhelpers_always_get_source_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_describe_source.go b/aws-source/adapters/adapterhelpers_describe_source.go index a5826ec6..c7049d59 100644 --- a/aws-source/adapters/adapterhelpers_describe_source.go +++ b/aws-source/adapters/adapterhelpers_describe_source.go @@ -8,9 +8,9 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // relatively short cache duration to cover a single Change Analysis run. diff --git a/aws-source/adapters/adapterhelpers_describe_source_test.go b/aws-source/adapters/adapterhelpers_describe_source_test.go index 323a500c..10edbded 100644 --- a/aws-source/adapters/adapterhelpers_describe_source_test.go +++ b/aws-source/adapters/adapterhelpers_describe_source_test.go @@ -7,9 +7,9 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go index b156e921..424a9132 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go @@ -6,9 +6,9 @@ import ( "fmt" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // GetListAdapterV2 A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go index 6ca9a41f..389bf47a 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_source.go b/aws-source/adapters/adapterhelpers_get_list_source.go index abeb23d4..7363b281 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source.go +++ b/aws-source/adapters/adapterhelpers_get_list_source.go @@ -7,8 +7,8 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // GetListAdapter A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_source_test.go b/aws-source/adapters/adapterhelpers_get_list_source_test.go index b0601b84..08471eda 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_source_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_notfound_cache_test.go b/aws-source/adapters/adapterhelpers_notfound_cache_test.go index 3b17fc1d..9fb26ca5 100644 --- a/aws-source/adapters/adapterhelpers_notfound_cache_test.go +++ b/aws-source/adapters/adapterhelpers_notfound_cache_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // TestGetListAdapterV2_GetNotFoundCaching tests that GetListAdapterV2 caches not-found error results diff --git a/aws-source/adapters/adapterhelpers_shared_tests.go b/aws-source/adapters/adapterhelpers_shared_tests.go index 5a690c5f..65107d27 100644 --- a/aws-source/adapters/adapterhelpers_shared_tests.go +++ b/aws-source/adapters/adapterhelpers_shared_tests.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func PtrString(v string) *string { diff --git a/aws-source/adapters/adapterhelpers_util.go b/aws-source/adapters/adapterhelpers_util.go index 82c60cae..ad49e56f 100644 --- a/aws-source/adapters/adapterhelpers_util.go +++ b/aws-source/adapters/adapterhelpers_util.go @@ -17,8 +17,8 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/apigateway-api-key.go b/aws-source/adapters/apigateway-api-key.go index 8d055b73..cba52585 100644 --- a/aws-source/adapters/apigateway-api-key.go +++ b/aws-source/adapters/apigateway-api-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // convertGetApiKeyOutputToApiKey converts a GetApiKeyOutput to an ApiKey diff --git a/aws-source/adapters/apigateway-api-key_test.go b/aws-source/adapters/apigateway-api-key_test.go index 21e4243b..3e011531 100644 --- a/aws-source/adapters/apigateway-api-key_test.go +++ b/aws-source/adapters/apigateway-api-key_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestApiKeyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-authorizer.go b/aws-source/adapters/apigateway-authorizer.go index 0b4251f0..e8e89ee0 100644 --- a/aws-source/adapters/apigateway-authorizer.go +++ b/aws-source/adapters/apigateway-authorizer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // convertGetAuthorizerOutputToAuthorizer converts a GetAuthorizerOutput to an Authorizer diff --git a/aws-source/adapters/apigateway-authorizer_test.go b/aws-source/adapters/apigateway-authorizer_test.go index e7ff174e..babafe43 100644 --- a/aws-source/adapters/apigateway-authorizer_test.go +++ b/aws-source/adapters/apigateway-authorizer_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestAuthorizerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-deployment.go b/aws-source/adapters/apigateway-deployment.go index c395d374..8d23731f 100644 --- a/aws-source/adapters/apigateway-deployment.go +++ b/aws-source/adapters/apigateway-deployment.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // convertGetDeploymentOutputToDeployment converts a GetDeploymentOutput to a Deployment diff --git a/aws-source/adapters/apigateway-deployment_test.go b/aws-source/adapters/apigateway-deployment_test.go index 98ae4f60..c7af47e5 100644 --- a/aws-source/adapters/apigateway-deployment_test.go +++ b/aws-source/adapters/apigateway-deployment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDeploymentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-domain-name.go b/aws-source/adapters/apigateway-domain-name.go index 9b767355..2f536bb7 100644 --- a/aws-source/adapters/apigateway-domain-name.go +++ b/aws-source/adapters/apigateway-domain-name.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func convertGetDomainNameOutputToDomainName(output *apigateway.GetDomainNameOutput) *types.DomainName { diff --git a/aws-source/adapters/apigateway-domain-name_test.go b/aws-source/adapters/apigateway-domain-name_test.go index 9a4f058d..acad7369 100644 --- a/aws-source/adapters/apigateway-domain-name_test.go +++ b/aws-source/adapters/apigateway-domain-name_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-integration.go b/aws-source/adapters/apigateway-integration.go index bca58a04..51355005 100644 --- a/aws-source/adapters/apigateway-integration.go +++ b/aws-source/adapters/apigateway-integration.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type apiGatewayIntegrationGetter interface { diff --git a/aws-source/adapters/apigateway-integration_test.go b/aws-source/adapters/apigateway-integration_test.go index efec09ee..b6170185 100644 --- a/aws-source/adapters/apigateway-integration_test.go +++ b/aws-source/adapters/apigateway-integration_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type mockAPIGatewayIntegrationClient struct{} diff --git a/aws-source/adapters/apigateway-method-response.go b/aws-source/adapters/apigateway-method-response.go index 7e88cc03..6ac891ad 100644 --- a/aws-source/adapters/apigateway-method-response.go +++ b/aws-source/adapters/apigateway-method-response.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodResponseInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/apigateway-method-response_test.go b/aws-source/adapters/apigateway-method-response_test.go index 6c351c4f..9152c93b 100644 --- a/aws-source/adapters/apigateway-method-response_test.go +++ b/aws-source/adapters/apigateway-method-response_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (m *mockAPIGatewayClient) GetMethodResponse(ctx context.Context, params *apigateway.GetMethodResponseInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodResponseOutput, error) { diff --git a/aws-source/adapters/apigateway-method.go b/aws-source/adapters/apigateway-method.go index 997af413..86e71f51 100644 --- a/aws-source/adapters/apigateway-method.go +++ b/aws-source/adapters/apigateway-method.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type apigatewayClient interface { diff --git a/aws-source/adapters/apigateway-method_test.go b/aws-source/adapters/apigateway-method_test.go index 8e71e819..ed715c4e 100644 --- a/aws-source/adapters/apigateway-method_test.go +++ b/aws-source/adapters/apigateway-method_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type mockAPIGatewayClient struct{} diff --git a/aws-source/adapters/apigateway-model.go b/aws-source/adapters/apigateway-model.go index 8abe16c6..6e47df54 100644 --- a/aws-source/adapters/apigateway-model.go +++ b/aws-source/adapters/apigateway-model.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func convertGetModelOutputToModel(output *apigateway.GetModelOutput) *types.Model { diff --git a/aws-source/adapters/apigateway-model_test.go b/aws-source/adapters/apigateway-model_test.go index 33f2d343..339d23e1 100644 --- a/aws-source/adapters/apigateway-model_test.go +++ b/aws-source/adapters/apigateway-model_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestModelOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-resource.go b/aws-source/adapters/apigateway-resource.go index 0513fb7c..f2815201 100644 --- a/aws-source/adapters/apigateway-resource.go +++ b/aws-source/adapters/apigateway-resource.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func convertGetResourceOutputToResource(output *apigateway.GetResourceOutput) *types.Resource { diff --git a/aws-source/adapters/apigateway-resource_test.go b/aws-source/adapters/apigateway-resource_test.go index a6bac53c..837f2a23 100644 --- a/aws-source/adapters/apigateway-resource_test.go +++ b/aws-source/adapters/apigateway-resource_test.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-rest-api.go b/aws-source/adapters/apigateway-rest-api.go index d0515aba..a8d8aff7 100644 --- a/aws-source/adapters/apigateway-rest-api.go +++ b/aws-source/adapters/apigateway-rest-api.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/apigateway-rest-api_test.go b/aws-source/adapters/apigateway-rest-api_test.go index f53a0468..632102c4 100644 --- a/aws-source/adapters/apigateway-rest-api_test.go +++ b/aws-source/adapters/apigateway-rest-api_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-stage.go b/aws-source/adapters/apigateway-stage.go index 12688dcf..411dc774 100644 --- a/aws-source/adapters/apigateway-stage.go +++ b/aws-source/adapters/apigateway-stage.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func convertGetStageOutputToStage(output *apigateway.GetStageOutput) *types.Stage { diff --git a/aws-source/adapters/apigateway-stage_test.go b/aws-source/adapters/apigateway-stage_test.go index c852377c..950a4936 100644 --- a/aws-source/adapters/apigateway-stage_test.go +++ b/aws-source/adapters/apigateway-stage_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestStageOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-group.go b/aws-source/adapters/autoscaling-auto-scaling-group.go index c291bfdd..29088faa 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribeAutoScalingGroupsInput, output *autoscaling.DescribeAutoScalingGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-group_test.go b/aws-source/adapters/autoscaling-auto-scaling-group_test.go index ea3a4c79..a5e25683 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestAutoScalingGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy.go b/aws-source/adapters/autoscaling-auto-scaling-policy.go index b6ce7685..1fbe10d0 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribePoliciesInput, output *autoscaling.DescribePoliciesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go index 27762c28..55112b26 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestScalingPolicyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-cache-policy.go b/aws-source/adapters/cloudfront-cache-policy.go index d6a972ed..c18cbfc6 100644 --- a/aws-source/adapters/cloudfront-cache-policy.go +++ b/aws-source/adapters/cloudfront-cache-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func cachePolicyListFunc(ctx context.Context, client CloudFrontClient, scope string) ([]*types.CachePolicy, error) { diff --git a/aws-source/adapters/cloudfront-cache-policy_test.go b/aws-source/adapters/cloudfront-cache-policy_test.go index db2b8cac..763fc286 100644 --- a/aws-source/adapters/cloudfront-cache-policy_test.go +++ b/aws-source/adapters/cloudfront-cache-policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var testCachePolicy = &types.CachePolicy{ diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy.go b/aws-source/adapters/cloudfront-continuous-deployment-policy.go index 3fe11481..21386807 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func continuousDeploymentPolicyItemMapper(_, scope string, awsItem *types.ContinuousDeploymentPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go index ad4f2576..72bc651c 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestContinuousDeploymentPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-distribution.go b/aws-source/adapters/cloudfront-distribution.go index 78422672..2f181467 100644 --- a/aws-source/adapters/cloudfront-distribution.go +++ b/aws-source/adapters/cloudfront-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var s3DnsRegex = regexp.MustCompile(`([^\.]+)\.s3\.([^\.]+)\.amazonaws\.com`) diff --git a/aws-source/adapters/cloudfront-distribution_test.go b/aws-source/adapters/cloudfront-distribution_test.go index 4a35cab8..dfc09d41 100644 --- a/aws-source/adapters/cloudfront-distribution_test.go +++ b/aws-source/adapters/cloudfront-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloudfront.GetDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetDistributionOutput, error) { diff --git a/aws-source/adapters/cloudfront-function.go b/aws-source/adapters/cloudfront-function.go index 29dc5a82..08a2ede3 100644 --- a/aws-source/adapters/cloudfront-function.go +++ b/aws-source/adapters/cloudfront-function.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func functionItemMapper(_, scope string, awsItem *types.FunctionSummary) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-function_test.go b/aws-source/adapters/cloudfront-function_test.go index a1c903f0..d87382f1 100644 --- a/aws-source/adapters/cloudfront-function_test.go +++ b/aws-source/adapters/cloudfront-function_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestFunctionItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-key-group.go b/aws-source/adapters/cloudfront-key-group.go index 32a8d30b..91a74dd1 100644 --- a/aws-source/adapters/cloudfront-key-group.go +++ b/aws-source/adapters/cloudfront-key-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func KeyGroupItemMapper(_, scope string, awsItem *types.KeyGroup) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-key-group_test.go b/aws-source/adapters/cloudfront-key-group_test.go index 2e9defe0..aa5d399c 100644 --- a/aws-source/adapters/cloudfront-key-group_test.go +++ b/aws-source/adapters/cloudfront-key-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestKeyGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-access-control.go b/aws-source/adapters/cloudfront-origin-access-control.go index 36dfd9ad..b32a5450 100644 --- a/aws-source/adapters/cloudfront-origin-access-control.go +++ b/aws-source/adapters/cloudfront-origin-access-control.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func originAccessControlListFunc(ctx context.Context, client *cloudfront.Client, scope string) ([]*types.OriginAccessControl, error) { diff --git a/aws-source/adapters/cloudfront-origin-access-control_test.go b/aws-source/adapters/cloudfront-origin-access-control_test.go index 52ce8711..ee769e3b 100644 --- a/aws-source/adapters/cloudfront-origin-access-control_test.go +++ b/aws-source/adapters/cloudfront-origin-access-control_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestOriginAccessControlItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy.go b/aws-source/adapters/cloudfront-origin-request-policy.go index 00c3069f..03920407 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy.go +++ b/aws-source/adapters/cloudfront-origin-request-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func originRequestPolicyItemMapper(_, scope string, awsItem *types.OriginRequestPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy_test.go b/aws-source/adapters/cloudfront-origin-request-policy_test.go index fb00556d..b1894816 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy_test.go +++ b/aws-source/adapters/cloudfront-origin-request-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestOriginRequestPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-realtime-log-config.go b/aws-source/adapters/cloudfront-realtime-log-config.go index ac2f259b..aafc5099 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config.go +++ b/aws-source/adapters/cloudfront-realtime-log-config.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogConfig) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-realtime-log-config_test.go b/aws-source/adapters/cloudfront-realtime-log-config_test.go index 23ebbe9e..e0692268 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config_test.go +++ b/aws-source/adapters/cloudfront-realtime-log-config_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestRealtimeLogConfigsItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy.go b/aws-source/adapters/cloudfront-response-headers-policy.go index 6a4d80e9..a441f366 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy.go +++ b/aws-source/adapters/cloudfront-response-headers-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func ResponseHeadersPolicyItemMapper(_, scope string, awsItem *types.ResponseHeadersPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy_test.go b/aws-source/adapters/cloudfront-response-headers-policy_test.go index 9557d875..06ad692c 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy_test.go +++ b/aws-source/adapters/cloudfront-response-headers-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestResponseHeadersPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-streaming-distribution.go b/aws-source/adapters/cloudfront-streaming-distribution.go index 4ea0525e..c2b1f89f 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution.go +++ b/aws-source/adapters/cloudfront-streaming-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, scope string, input *cloudfront.GetStreamingDistributionInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-streaming-distribution_test.go b/aws-source/adapters/cloudfront-streaming-distribution_test.go index 6ae8ff7c..d5089cd4 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution_test.go +++ b/aws-source/adapters/cloudfront-streaming-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, params *cloudfront.GetStreamingDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetStreamingDistributionOutput, error) { diff --git a/aws-source/adapters/cloudwatch-alarm.go b/aws-source/adapters/cloudwatch-alarm.go index 05353876..72331ed0 100644 --- a/aws-source/adapters/cloudwatch-alarm.go +++ b/aws-source/adapters/cloudwatch-alarm.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type CloudwatchClient interface { diff --git a/aws-source/adapters/cloudwatch-alarm_test.go b/aws-source/adapters/cloudwatch-alarm_test.go index 929e2269..85968b54 100644 --- a/aws-source/adapters/cloudwatch-alarm_test.go +++ b/aws-source/adapters/cloudwatch-alarm_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type testCloudwatchClient struct{} diff --git a/aws-source/adapters/cloudwatch-instance-metric.go b/aws-source/adapters/cloudwatch-instance-metric.go index 9823da80..63de89c4 100644 --- a/aws-source/adapters/cloudwatch-instance-metric.go +++ b/aws-source/adapters/cloudwatch-instance-metric.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // CloudwatchMetricClient defines the CloudWatch client interface for metrics diff --git a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go index 24f6598b..000ca15c 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) // TestCloudwatchInstanceMetricIntegration fetches real CloudWatch metrics for an EC2 instance diff --git a/aws-source/adapters/cloudwatch-instance-metric_test.go b/aws-source/adapters/cloudwatch-instance-metric_test.go index e3aefc1f..d802a13f 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_test.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) // testCloudwatchMetricClient is a mock client for testing GetMetricData diff --git a/aws-source/adapters/cloudwatch_metric_links.go b/aws-source/adapters/cloudwatch_metric_links.go index edc8b3d2..b88b805a 100644 --- a/aws-source/adapters/cloudwatch_metric_links.go +++ b/aws-source/adapters/cloudwatch_metric_links.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) var ErrNoQuery = errors.New("no query found") diff --git a/aws-source/adapters/directconnect-connection.go b/aws-source/adapters/directconnect-connection.go index 19f28b3d..f3e63a7e 100644 --- a/aws-source/adapters/directconnect-connection.go +++ b/aws-source/adapters/directconnect-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeConnectionsInput, output *directconnect.DescribeConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-connection_test.go b/aws-source/adapters/directconnect-connection_test.go index c4b5e264..bfbdc31a 100644 --- a/aws-source/adapters/directconnect-connection_test.go +++ b/aws-source/adapters/directconnect-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDirectconnectConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-customer-metadata.go b/aws-source/adapters/directconnect-customer-metadata.go index 6f50dfe2..9cf83252 100644 --- a/aws-source/adapters/directconnect-customer-metadata.go +++ b/aws-source/adapters/directconnect-customer-metadata.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func customerMetadataOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeCustomerMetadataInput, output *directconnect.DescribeCustomerMetadataOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-customer-metadata_test.go b/aws-source/adapters/directconnect-customer-metadata_test.go index 491a2b13..8179344d 100644 --- a/aws-source/adapters/directconnect-customer-metadata_test.go +++ b/aws-source/adapters/directconnect-customer-metadata_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestCustomerMetadataOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go index f4a77d43..4b01bcea 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func directConnectGatewayAssociationProposalOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAssociationProposalsInput, output *directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go index 5c836b06..327ec577 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDirectConnectGatewayAssociationProposalOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association.go b/aws-source/adapters/directconnect-direct-connect-gateway-association.go index 3f3f5a9c..916219e3 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) const ( diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go index f46e8b0c..833afdc6 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDirectConnectGatewayAssociationOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go index 81ac79d9..f1c7d2b4 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAttachmentsInput, output *directconnect.DescribeDirectConnectGatewayAttachmentsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go index 1b8ad52d..68752a29 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDirectConnectGatewayAttachmentOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway.go b/aws-source/adapters/directconnect-direct-connect-gateway.go index 60c7b462..8a03d217 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func directConnectGatewayOutputMapper(ctx context.Context, cli *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewaysInput, output *directconnect.DescribeDirectConnectGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway_test.go b/aws-source/adapters/directconnect-direct-connect-gateway_test.go index c70f9f36..b89f382f 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDirectConnectGatewayOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-hosted-connection.go b/aws-source/adapters/directconnect-hosted-connection.go index 66d3baa4..7ed66f52 100644 --- a/aws-source/adapters/directconnect-hosted-connection.go +++ b/aws-source/adapters/directconnect-hosted-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeHostedConnectionsInput, output *directconnect.DescribeHostedConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-hosted-connection_test.go b/aws-source/adapters/directconnect-hosted-connection_test.go index 1ef0f8e2..4921f61d 100644 --- a/aws-source/adapters/directconnect-hosted-connection_test.go +++ b/aws-source/adapters/directconnect-hosted-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestHostedConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-interconnect.go b/aws-source/adapters/directconnect-interconnect.go index 5d0713d0..26d5cbbd 100644 --- a/aws-source/adapters/directconnect-interconnect.go +++ b/aws-source/adapters/directconnect-interconnect.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeInterconnectsInput, output *directconnect.DescribeInterconnectsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-interconnect_test.go b/aws-source/adapters/directconnect-interconnect_test.go index 108f15d5..8c0d4f55 100644 --- a/aws-source/adapters/directconnect-interconnect_test.go +++ b/aws-source/adapters/directconnect-interconnect_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestInterconnectOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-lag.go b/aws-source/adapters/directconnect-lag.go index 35e4c330..fb64f1f6 100644 --- a/aws-source/adapters/directconnect-lag.go +++ b/aws-source/adapters/directconnect-lag.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLagsInput, output *directconnect.DescribeLagsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-lag_test.go b/aws-source/adapters/directconnect-lag_test.go index 95b0815c..10da521f 100644 --- a/aws-source/adapters/directconnect-lag_test.go +++ b/aws-source/adapters/directconnect-lag_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" diff --git a/aws-source/adapters/directconnect-location.go b/aws-source/adapters/directconnect-location.go index 4d95c95b..1c620f87 100644 --- a/aws-source/adapters/directconnect-location.go +++ b/aws-source/adapters/directconnect-location.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func locationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLocationsInput, output *directconnect.DescribeLocationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-location_test.go b/aws-source/adapters/directconnect-location_test.go index bd36c612..a714e5a1 100644 --- a/aws-source/adapters/directconnect-location_test.go +++ b/aws-source/adapters/directconnect-location_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestLocationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-router-configuration.go b/aws-source/adapters/directconnect-router-configuration.go index 70c1fc98..75939f66 100644 --- a/aws-source/adapters/directconnect-router-configuration.go +++ b/aws-source/adapters/directconnect-router-configuration.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func routerConfigurationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeRouterConfigurationInput, output *directconnect.DescribeRouterConfigurationOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-router-configuration_test.go b/aws-source/adapters/directconnect-router-configuration_test.go index 8a124b6e..9bee05df 100644 --- a/aws-source/adapters/directconnect-router-configuration_test.go +++ b/aws-source/adapters/directconnect-router-configuration_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestRouterConfigurationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-gateway.go b/aws-source/adapters/directconnect-virtual-gateway.go index 53db1e36..47bb351a 100644 --- a/aws-source/adapters/directconnect-virtual-gateway.go +++ b/aws-source/adapters/directconnect-virtual-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func virtualGatewayOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeVirtualGatewaysInput, output *directconnect.DescribeVirtualGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-virtual-gateway_test.go b/aws-source/adapters/directconnect-virtual-gateway_test.go index 2f6252b9..49dd6a35 100644 --- a/aws-source/adapters/directconnect-virtual-gateway_test.go +++ b/aws-source/adapters/directconnect-virtual-gateway_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestVirtualGatewayOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-interface.go b/aws-source/adapters/directconnect-virtual-interface.go index cda44169..8e885617 100644 --- a/aws-source/adapters/directconnect-virtual-interface.go +++ b/aws-source/adapters/directconnect-virtual-interface.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) const gatewayIDVirtualInterfaceIDFormat = "gateway_id/virtual_interface_id" diff --git a/aws-source/adapters/directconnect-virtual-interface_test.go b/aws-source/adapters/directconnect-virtual-interface_test.go index 6a5463e8..0c6b9c32 100644 --- a/aws-source/adapters/directconnect-virtual-interface_test.go +++ b/aws-source/adapters/directconnect-virtual-interface_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestVirtualInterfaceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/dynamodb-backup.go b/aws-source/adapters/dynamodb-backup.go index cf67c84d..0528cf65 100644 --- a/aws-source/adapters/dynamodb-backup.go +++ b/aws-source/adapters/dynamodb-backup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func backupGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeBackupInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/dynamodb-backup_test.go b/aws-source/adapters/dynamodb-backup_test.go index e4f4b718..5750d0ff 100644 --- a/aws-source/adapters/dynamodb-backup_test.go +++ b/aws-source/adapters/dynamodb-backup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *DynamoDBTestClient) DescribeBackup(ctx context.Context, params *dynamodb.DescribeBackupInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DescribeBackupOutput, error) { diff --git a/aws-source/adapters/dynamodb-table.go b/aws-source/adapters/dynamodb-table.go index 8aa3c8ad..c934e388 100644 --- a/aws-source/adapters/dynamodb-table.go +++ b/aws-source/adapters/dynamodb-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func tableGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeTableInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/dynamodb-table_test.go b/aws-source/adapters/dynamodb-table_test.go index 13589e01..ba5dc94f 100644 --- a/aws-source/adapters/dynamodb-table_test.go +++ b/aws-source/adapters/dynamodb-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) { diff --git a/aws-source/adapters/ec2-address.go b/aws-source/adapters/ec2-address.go index 999fd4a5..bdaaccb6 100644 --- a/aws-source/adapters/ec2-address.go +++ b/aws-source/adapters/ec2-address.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // AddressInputMapperGet Maps adapter calls to the correct input for the AZ API diff --git a/aws-source/adapters/ec2-address_test.go b/aws-source/adapters/ec2-address_test.go index 3132a84f..edd03592 100644 --- a/aws-source/adapters/ec2-address_test.go +++ b/aws-source/adapters/ec2-address_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestAddressInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet.go b/aws-source/adapters/ec2-capacity-reservation-fleet.go index d07f2819..4c844c41 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func capacityReservationFleetOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationFleetsInput, output *ec2.DescribeCapacityReservationFleetsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go index 48bf1419..d72391a8 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestCapacityReservationFleetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation.go b/aws-source/adapters/ec2-capacity-reservation.go index 66188745..5081c15c 100644 --- a/aws-source/adapters/ec2-capacity-reservation.go +++ b/aws-source/adapters/ec2-capacity-reservation.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationsInput, output *ec2.DescribeCapacityReservationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-capacity-reservation_test.go b/aws-source/adapters/ec2-capacity-reservation_test.go index e2819dc4..c43dd0ef 100644 --- a/aws-source/adapters/ec2-capacity-reservation_test.go +++ b/aws-source/adapters/ec2-capacity-reservation_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestCapacityReservationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway.go b/aws-source/adapters/ec2-egress-only-internet-gateway.go index c1eec343..a15f6f0e 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func egressOnlyInternetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeEgressOnlyInternetGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go index 677b4b73..daa57359 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestEgressOnlyInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-iam-instance-profile-association.go b/aws-source/adapters/ec2-iam-instance-profile-association.go index 2f0ca0a7..4fbef64e 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeIamInstanceProfileAssociationsInput, output *ec2.DescribeIamInstanceProfileAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-iam-instance-profile-association_test.go b/aws-source/adapters/ec2-iam-instance-profile-association_test.go index 72ab47a6..0403ffbc 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association_test.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestIamInstanceProfileAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-image.go b/aws-source/adapters/ec2-image.go index e0ac5f24..dc5e77f0 100644 --- a/aws-source/adapters/ec2-image.go +++ b/aws-source/adapters/ec2-image.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // ImageInputMapperGet Gets a given image. As opposed to list, get will get diff --git a/aws-source/adapters/ec2-image_test.go b/aws-source/adapters/ec2-image_test.go index b642e410..c9221f37 100644 --- a/aws-source/adapters/ec2-image_test.go +++ b/aws-source/adapters/ec2-image_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestImageInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-event-window.go b/aws-source/adapters/ec2-instance-event-window.go index b7db1d44..2f1223a2 100644 --- a/aws-source/adapters/ec2-instance-event-window.go +++ b/aws-source/adapters/ec2-instance-event-window.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func instanceEventWindowInputMapperGet(scope, query string) (*ec2.DescribeInstanceEventWindowsInput, error) { diff --git a/aws-source/adapters/ec2-instance-event-window_test.go b/aws-source/adapters/ec2-instance-event-window_test.go index a8a7eb16..5046efe2 100644 --- a/aws-source/adapters/ec2-instance-event-window_test.go +++ b/aws-source/adapters/ec2-instance-event-window_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestInstanceEventWindowInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-status.go b/aws-source/adapters/ec2-instance-status.go index 0163e343..b3ab2e78 100644 --- a/aws-source/adapters/ec2-instance-status.go +++ b/aws-source/adapters/ec2-instance-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func instanceStatusInputMapperGet(scope, query string) (*ec2.DescribeInstanceStatusInput, error) { diff --git a/aws-source/adapters/ec2-instance-status_test.go b/aws-source/adapters/ec2-instance-status_test.go index 212d16a0..4a6868d9 100644 --- a/aws-source/adapters/ec2-instance-status_test.go +++ b/aws-source/adapters/ec2-instance-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestInstanceStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance.go b/aws-source/adapters/ec2-instance.go index 3c8d8218..716cf95a 100644 --- a/aws-source/adapters/ec2-instance.go +++ b/aws-source/adapters/ec2-instance.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var ( diff --git a/aws-source/adapters/ec2-instance_test.go b/aws-source/adapters/ec2-instance_test.go index 88288ed6..accbbc5c 100644 --- a/aws-source/adapters/ec2-instance_test.go +++ b/aws-source/adapters/ec2-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-internet-gateway.go b/aws-source/adapters/ec2-internet-gateway.go index cf280e00..d5e15e6e 100644 --- a/aws-source/adapters/ec2-internet-gateway.go +++ b/aws-source/adapters/ec2-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func internetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeInternetGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-internet-gateway_test.go b/aws-source/adapters/ec2-internet-gateway_test.go index 4d2bb563..0db2bd43 100644 --- a/aws-source/adapters/ec2-internet-gateway_test.go +++ b/aws-source/adapters/ec2-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-key-pair.go b/aws-source/adapters/ec2-key-pair.go index 75bb1795..bae9dda5 100644 --- a/aws-source/adapters/ec2-key-pair.go +++ b/aws-source/adapters/ec2-key-pair.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func keyPairInputMapperGet(scope string, query string) (*ec2.DescribeKeyPairsInput, error) { diff --git a/aws-source/adapters/ec2-key-pair_test.go b/aws-source/adapters/ec2-key-pair_test.go index 89522608..100302bf 100644 --- a/aws-source/adapters/ec2-key-pair_test.go +++ b/aws-source/adapters/ec2-key-pair_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestKeyPairInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template-version.go b/aws-source/adapters/ec2-launch-template-version.go index 4d05a4be..a8736734 100644 --- a/aws-source/adapters/ec2-launch-template-version.go +++ b/aws-source/adapters/ec2-launch-template-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func launchTemplateVersionInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplateVersionsInput, error) { diff --git a/aws-source/adapters/ec2-launch-template-version_test.go b/aws-source/adapters/ec2-launch-template-version_test.go index a86e9d43..b2a5d305 100644 --- a/aws-source/adapters/ec2-launch-template-version_test.go +++ b/aws-source/adapters/ec2-launch-template-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestLaunchTemplateVersionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template.go b/aws-source/adapters/ec2-launch-template.go index 43afaff4..b3653a2b 100644 --- a/aws-source/adapters/ec2-launch-template.go +++ b/aws-source/adapters/ec2-launch-template.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func launchTemplateInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplatesInput, error) { diff --git a/aws-source/adapters/ec2-launch-template_test.go b/aws-source/adapters/ec2-launch-template_test.go index c2f8e4ff..e67d0d28 100644 --- a/aws-source/adapters/ec2-launch-template_test.go +++ b/aws-source/adapters/ec2-launch-template_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestLaunchTemplateInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-nat-gateway.go b/aws-source/adapters/ec2-nat-gateway.go index e6ad5c21..df8b4a57 100644 --- a/aws-source/adapters/ec2-nat-gateway.go +++ b/aws-source/adapters/ec2-nat-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func natGatewayInputMapperGet(scope string, query string) (*ec2.DescribeNatGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-nat-gateway_test.go b/aws-source/adapters/ec2-nat-gateway_test.go index 70e4f351..7dffc6f6 100644 --- a/aws-source/adapters/ec2-nat-gateway_test.go +++ b/aws-source/adapters/ec2-nat-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestNatGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-acl.go b/aws-source/adapters/ec2-network-acl.go index 0999159f..46da380b 100644 --- a/aws-source/adapters/ec2-network-acl.go +++ b/aws-source/adapters/ec2-network-acl.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func networkAclInputMapperGet(scope string, query string) (*ec2.DescribeNetworkAclsInput, error) { diff --git a/aws-source/adapters/ec2-network-acl_test.go b/aws-source/adapters/ec2-network-acl_test.go index a0197d24..3c4b88ba 100644 --- a/aws-source/adapters/ec2-network-acl_test.go +++ b/aws-source/adapters/ec2-network-acl_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestNetworkAclInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface-permission.go b/aws-source/adapters/ec2-network-interface-permission.go index bb25592d..9023762b 100644 --- a/aws-source/adapters/ec2-network-interface-permission.go +++ b/aws-source/adapters/ec2-network-interface-permission.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func networkInterfacePermissionInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacePermissionsInput, error) { diff --git a/aws-source/adapters/ec2-network-interface-permission_test.go b/aws-source/adapters/ec2-network-interface-permission_test.go index 0d07c507..72c0d564 100644 --- a/aws-source/adapters/ec2-network-interface-permission_test.go +++ b/aws-source/adapters/ec2-network-interface-permission_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestNetworkInterfacePermissionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface.go b/aws-source/adapters/ec2-network-interface.go index bc8f5e7a..fc795ee3 100644 --- a/aws-source/adapters/ec2-network-interface.go +++ b/aws-source/adapters/ec2-network-interface.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func networkInterfaceInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacesInput, error) { diff --git a/aws-source/adapters/ec2-network-interface_test.go b/aws-source/adapters/ec2-network-interface_test.go index bbf5713b..861cbd79 100644 --- a/aws-source/adapters/ec2-network-interface_test.go +++ b/aws-source/adapters/ec2-network-interface_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestNetworkInterfaceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-placement-group.go b/aws-source/adapters/ec2-placement-group.go index ec89d3cd..c03701f5 100644 --- a/aws-source/adapters/ec2-placement-group.go +++ b/aws-source/adapters/ec2-placement-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func placementGroupInputMapperGet(scope string, query string) (*ec2.DescribePlacementGroupsInput, error) { diff --git a/aws-source/adapters/ec2-placement-group_test.go b/aws-source/adapters/ec2-placement-group_test.go index f17dfbd9..db743000 100644 --- a/aws-source/adapters/ec2-placement-group_test.go +++ b/aws-source/adapters/ec2-placement-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestPlacementGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-reserved-instance.go b/aws-source/adapters/ec2-reserved-instance.go index be7ea5d5..3e8a3472 100644 --- a/aws-source/adapters/ec2-reserved-instance.go +++ b/aws-source/adapters/ec2-reserved-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func reservedInstanceInputMapperGet(scope, query string) (*ec2.DescribeReservedInstancesInput, error) { diff --git a/aws-source/adapters/ec2-reserved-instance_test.go b/aws-source/adapters/ec2-reserved-instance_test.go index ae67edc8..9ef700da 100644 --- a/aws-source/adapters/ec2-reserved-instance_test.go +++ b/aws-source/adapters/ec2-reserved-instance_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestReservedInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-route-table.go b/aws-source/adapters/ec2-route-table.go index 733ae7bd..1e12f1cf 100644 --- a/aws-source/adapters/ec2-route-table.go +++ b/aws-source/adapters/ec2-route-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func routeTableInputMapperGet(scope string, query string) (*ec2.DescribeRouteTablesInput, error) { diff --git a/aws-source/adapters/ec2-route-table_test.go b/aws-source/adapters/ec2-route-table_test.go index 468e7a9a..867dc678 100644 --- a/aws-source/adapters/ec2-route-table_test.go +++ b/aws-source/adapters/ec2-route-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestRouteTableInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group-rule.go b/aws-source/adapters/ec2-security-group-rule.go index fec023b6..f51ec404 100644 --- a/aws-source/adapters/ec2-security-group-rule.go +++ b/aws-source/adapters/ec2-security-group-rule.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func securityGroupRuleInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupRulesInput, error) { diff --git a/aws-source/adapters/ec2-security-group-rule_test.go b/aws-source/adapters/ec2-security-group-rule_test.go index 3fdf07ad..a34989ca 100644 --- a/aws-source/adapters/ec2-security-group-rule_test.go +++ b/aws-source/adapters/ec2-security-group-rule_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSecurityGroupRuleInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group.go b/aws-source/adapters/ec2-security-group.go index 148de33d..de801c34 100644 --- a/aws-source/adapters/ec2-security-group.go +++ b/aws-source/adapters/ec2-security-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func securityGroupInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupsInput, error) { diff --git a/aws-source/adapters/ec2-security-group_test.go b/aws-source/adapters/ec2-security-group_test.go index 9ccb2422..ae9ddbac 100644 --- a/aws-source/adapters/ec2-security-group_test.go +++ b/aws-source/adapters/ec2-security-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSecurityGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-snapshot.go b/aws-source/adapters/ec2-snapshot.go index 6aae9f3f..445c815c 100644 --- a/aws-source/adapters/ec2-snapshot.go +++ b/aws-source/adapters/ec2-snapshot.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func snapshotInputMapperGet(scope string, query string) (*ec2.DescribeSnapshotsInput, error) { diff --git a/aws-source/adapters/ec2-snapshot_test.go b/aws-source/adapters/ec2-snapshot_test.go index 4bacc657..732e96ab 100644 --- a/aws-source/adapters/ec2-snapshot_test.go +++ b/aws-source/adapters/ec2-snapshot_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSnapshotInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-subnet.go b/aws-source/adapters/ec2-subnet.go index a293d096..7ef9f311 100644 --- a/aws-source/adapters/ec2-subnet.go +++ b/aws-source/adapters/ec2-subnet.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func subnetInputMapperGet(scope string, query string) (*ec2.DescribeSubnetsInput, error) { diff --git a/aws-source/adapters/ec2-subnet_test.go b/aws-source/adapters/ec2-subnet_test.go index 64e121eb..1f78abab 100644 --- a/aws-source/adapters/ec2-subnet_test.go +++ b/aws-source/adapters/ec2-subnet_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSubnetInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-volume-status.go b/aws-source/adapters/ec2-volume-status.go index a8db5d2c..476096db 100644 --- a/aws-source/adapters/ec2-volume-status.go +++ b/aws-source/adapters/ec2-volume-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func volumeStatusInputMapperGet(scope string, query string) (*ec2.DescribeVolumeStatusInput, error) { diff --git a/aws-source/adapters/ec2-volume-status_test.go b/aws-source/adapters/ec2-volume-status_test.go index 7c01e8e3..d3b5b9d6 100644 --- a/aws-source/adapters/ec2-volume-status_test.go +++ b/aws-source/adapters/ec2-volume-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestVolumeStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-volume.go b/aws-source/adapters/ec2-volume.go index 64351895..e1dc1a3a 100644 --- a/aws-source/adapters/ec2-volume.go +++ b/aws-source/adapters/ec2-volume.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func volumeInputMapperGet(scope string, query string) (*ec2.DescribeVolumesInput, error) { diff --git a/aws-source/adapters/ec2-volume_test.go b/aws-source/adapters/ec2-volume_test.go index 9dd02fcd..45014b13 100644 --- a/aws-source/adapters/ec2-volume_test.go +++ b/aws-source/adapters/ec2-volume_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestVolumeInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-endpoint.go b/aws-source/adapters/ec2-vpc-endpoint.go index fab2bff3..d0fd94a0 100644 --- a/aws-source/adapters/ec2-vpc-endpoint.go +++ b/aws-source/adapters/ec2-vpc-endpoint.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func vpcEndpointInputMapperGet(scope string, query string) (*ec2.DescribeVpcEndpointsInput, error) { diff --git a/aws-source/adapters/ec2-vpc-endpoint_test.go b/aws-source/adapters/ec2-vpc-endpoint_test.go index 8010afaa..41436e97 100644 --- a/aws-source/adapters/ec2-vpc-endpoint_test.go +++ b/aws-source/adapters/ec2-vpc-endpoint_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestVpcEndpointInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-peering-connection.go b/aws-source/adapters/ec2-vpc-peering-connection.go index 35969cf2..9aaadbdb 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection.go +++ b/aws-source/adapters/ec2-vpc-peering-connection.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope string, input *ec2.DescribeVpcPeeringConnectionsInput, output *ec2.DescribeVpcPeeringConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-vpc-peering-connection_test.go b/aws-source/adapters/ec2-vpc-peering-connection_test.go index 4a31a669..770234fa 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection_test.go +++ b/aws-source/adapters/ec2-vpc-peering-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestVpcPeeringConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc.go b/aws-source/adapters/ec2-vpc.go index 89f39dc6..5530b0c8 100644 --- a/aws-source/adapters/ec2-vpc.go +++ b/aws-source/adapters/ec2-vpc.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func vpcInputMapperGet(scope string, query string) (*ec2.DescribeVpcsInput, error) { diff --git a/aws-source/adapters/ec2-vpc_test.go b/aws-source/adapters/ec2-vpc_test.go index edd5cff9..b4032a3c 100644 --- a/aws-source/adapters/ec2-vpc_test.go +++ b/aws-source/adapters/ec2-vpc_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestVpcInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ecs-capacity-provider.go b/aws-source/adapters/ecs-capacity-provider.go index 88490428..b963a199 100644 --- a/aws-source/adapters/ecs-capacity-provider.go +++ b/aws-source/adapters/ecs-capacity-provider.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var CapacityProviderIncludeFields = []types.CapacityProviderField{ diff --git a/aws-source/adapters/ecs-capacity-provider_test.go b/aws-source/adapters/ecs-capacity-provider_test.go index faf453a8..41024f74 100644 --- a/aws-source/adapters/ecs-capacity-provider_test.go +++ b/aws-source/adapters/ecs-capacity-provider_test.go @@ -7,9 +7,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeCapacityProviders(ctx context.Context, params *ecs.DescribeCapacityProvidersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeCapacityProvidersOutput, error) { diff --git a/aws-source/adapters/ecs-cluster.go b/aws-source/adapters/ecs-cluster.go index 2f957fa5..b19ba099 100644 --- a/aws-source/adapters/ecs-cluster.go +++ b/aws-source/adapters/ecs-cluster.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // ClusterIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-cluster_test.go b/aws-source/adapters/ecs-cluster_test.go index ebe86965..97a88b5f 100644 --- a/aws-source/adapters/ecs-cluster_test.go +++ b/aws-source/adapters/ecs-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeClusters(ctx context.Context, params *ecs.DescribeClustersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) { diff --git a/aws-source/adapters/ecs-container-instance.go b/aws-source/adapters/ecs-container-instance.go index 3eef002d..e212d9f1 100644 --- a/aws-source/adapters/ecs-container-instance.go +++ b/aws-source/adapters/ecs-container-instance.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // ContainerInstanceIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-container-instance_test.go b/aws-source/adapters/ecs-container-instance_test.go index 73fb9818..f547cae4 100644 --- a/aws-source/adapters/ecs-container-instance_test.go +++ b/aws-source/adapters/ecs-container-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { diff --git a/aws-source/adapters/ecs-service.go b/aws-source/adapters/ecs-service.go index f0b5fcc4..327cc1fd 100644 --- a/aws-source/adapters/ecs-service.go +++ b/aws-source/adapters/ecs-service.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // ServiceIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-service_test.go b/aws-source/adapters/ecs-service_test.go index 41b6789d..f9cc70ed 100644 --- a/aws-source/adapters/ecs-service_test.go +++ b/aws-source/adapters/ecs-service_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) { diff --git a/aws-source/adapters/ecs-task-definition.go b/aws-source/adapters/ecs-task-definition.go index ba26315d..8ebcf4e3 100644 --- a/aws-source/adapters/ecs-task-definition.go +++ b/aws-source/adapters/ecs-task-definition.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // TaskDefinitionIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-task-definition_test.go b/aws-source/adapters/ecs-task-definition_test.go index 7f65a16d..0f4c97aa 100644 --- a/aws-source/adapters/ecs-task-definition_test.go +++ b/aws-source/adapters/ecs-task-definition_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { diff --git a/aws-source/adapters/ecs-task.go b/aws-source/adapters/ecs-task.go index 9632beed..72ec8e65 100644 --- a/aws-source/adapters/ecs-task.go +++ b/aws-source/adapters/ecs-task.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // TaskIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-task_test.go b/aws-source/adapters/ecs-task_test.go index 0f460c8b..cb137acf 100644 --- a/aws-source/adapters/ecs-task_test.go +++ b/aws-source/adapters/ecs-task_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *ecsTestClient) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { diff --git a/aws-source/adapters/efs-access-point.go b/aws-source/adapters/efs-access-point.go index ef52ff49..d747ae01 100644 --- a/aws-source/adapters/efs-access-point.go +++ b/aws-source/adapters/efs-access-point.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func AccessPointOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeAccessPointsInput, output *efs.DescribeAccessPointsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-access-point_test.go b/aws-source/adapters/efs-access-point_test.go index 9327b0d0..561c04ba 100644 --- a/aws-source/adapters/efs-access-point_test.go +++ b/aws-source/adapters/efs-access-point_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestAccessPointOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-backup-policy.go b/aws-source/adapters/efs-backup-policy.go index 442aea01..5c6c64d2 100644 --- a/aws-source/adapters/efs-backup-policy.go +++ b/aws-source/adapters/efs-backup-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func BackupPolicyOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeBackupPolicyInput, output *efs.DescribeBackupPolicyOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-file-system.go b/aws-source/adapters/efs-file-system.go index b6654cce..41f5dbc3 100644 --- a/aws-source/adapters/efs-file-system.go +++ b/aws-source/adapters/efs-file-system.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeFileSystemsInput, output *efs.DescribeFileSystemsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-file-system_test.go b/aws-source/adapters/efs-file-system_test.go index b0528b98..fd7ff8f6 100644 --- a/aws-source/adapters/efs-file-system_test.go +++ b/aws-source/adapters/efs-file-system_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestFileSystemOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-mount-target.go b/aws-source/adapters/efs-mount-target.go index 5223e644..dc364cfe 100644 --- a/aws-source/adapters/efs-mount-target.go +++ b/aws-source/adapters/efs-mount-target.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeMountTargetsInput, output *efs.DescribeMountTargetsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-mount-target_test.go b/aws-source/adapters/efs-mount-target_test.go index d5f34d3c..5344f8a2 100644 --- a/aws-source/adapters/efs-mount-target_test.go +++ b/aws-source/adapters/efs-mount-target_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestMountTargetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-replication-configuration.go b/aws-source/adapters/efs-replication-configuration.go index fb151deb..e48c0211 100644 --- a/aws-source/adapters/efs-replication-configuration.go +++ b/aws-source/adapters/efs-replication-configuration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeReplicationConfigurationsInput, output *efs.DescribeReplicationConfigurationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-replication-configuration_test.go b/aws-source/adapters/efs-replication-configuration_test.go index d56b9f64..a98bb31c 100644 --- a/aws-source/adapters/efs-replication-configuration_test.go +++ b/aws-source/adapters/efs-replication-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/efs.go b/aws-source/adapters/efs.go index 76e6b20c..b2d81f00 100644 --- a/aws-source/adapters/efs.go +++ b/aws-source/adapters/efs.go @@ -2,7 +2,7 @@ package adapters import ( "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // lifeCycleStateToHealth Converts a lifecycle state to a health state diff --git a/aws-source/adapters/eks-addon.go b/aws-source/adapters/eks-addon.go index 69da441c..2f73e13d 100644 --- a/aws-source/adapters/eks-addon.go +++ b/aws-source/adapters/eks-addon.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func addonGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeAddonInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-addon_test.go b/aws-source/adapters/eks-addon_test.go index eeb544ea..5432416a 100644 --- a/aws-source/adapters/eks-addon_test.go +++ b/aws-source/adapters/eks-addon_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var AddonTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-cluster.go b/aws-source/adapters/eks-cluster.go index b2f4f33b..58ac7b89 100644 --- a/aws-source/adapters/eks-cluster.go +++ b/aws-source/adapters/eks-cluster.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeClusterInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-cluster_test.go b/aws-source/adapters/eks-cluster_test.go index 95aa92a0..4920a91d 100644 --- a/aws-source/adapters/eks-cluster_test.go +++ b/aws-source/adapters/eks-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var ClusterClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-fargate-profile.go b/aws-source/adapters/eks-fargate-profile.go index 4db6d7f3..4eae17e7 100644 --- a/aws-source/adapters/eks-fargate-profile.go +++ b/aws-source/adapters/eks-fargate-profile.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeFargateProfileInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-fargate-profile_test.go b/aws-source/adapters/eks-fargate-profile_test.go index ab107381..3f830c73 100644 --- a/aws-source/adapters/eks-fargate-profile_test.go +++ b/aws-source/adapters/eks-fargate-profile_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var FargateTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-nodegroup.go b/aws-source/adapters/eks-nodegroup.go index c340f621..c79f7097 100644 --- a/aws-source/adapters/eks-nodegroup.go +++ b/aws-source/adapters/eks-nodegroup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeNodegroupInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-nodegroup_test.go b/aws-source/adapters/eks-nodegroup_test.go index 2a64ba1d..048db1b4 100644 --- a/aws-source/adapters/eks-nodegroup_test.go +++ b/aws-source/adapters/eks-nodegroup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var NodeGroupClient = EKSTestClient{ diff --git a/aws-source/adapters/elb-instance-health.go b/aws-source/adapters/elb-instance-health.go index 096cc1a2..ccf8800d 100644 --- a/aws-source/adapters/elb-instance-health.go +++ b/aws-source/adapters/elb-instance-health.go @@ -9,8 +9,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) // InstanceHealthName Structured representation of an instance health's unique diff --git a/aws-source/adapters/elb-instance-health_test.go b/aws-source/adapters/elb-instance-health_test.go index 676a04f7..aa1418fb 100644 --- a/aws-source/adapters/elb-instance-health_test.go +++ b/aws-source/adapters/elb-instance-health_test.go @@ -7,7 +7,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestInstanceHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elb-load-balancer.go b/aws-source/adapters/elb-load-balancer.go index 1992d468..077a9523 100644 --- a/aws-source/adapters/elb-load-balancer.go +++ b/aws-source/adapters/elb-load-balancer.go @@ -6,8 +6,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type elbClient interface { diff --git a/aws-source/adapters/elb-load-balancer_test.go b/aws-source/adapters/elb-load-balancer_test.go index 3700f1e9..6060fe6d 100644 --- a/aws-source/adapters/elb-load-balancer_test.go +++ b/aws-source/adapters/elb-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) type mockElbClient struct{} diff --git a/aws-source/adapters/elbv2-listener.go b/aws-source/adapters/elbv2-listener.go index 6047d67c..483d2de0 100644 --- a/aws-source/adapters/elbv2-listener.go +++ b/aws-source/adapters/elbv2-listener.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeListenersInput, output *elbv2.DescribeListenersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-listener_test.go b/aws-source/adapters/elbv2-listener_test.go index e370da2c..c30fa8ee 100644 --- a/aws-source/adapters/elbv2-listener_test.go +++ b/aws-source/adapters/elbv2-listener_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestListenerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-load-balancer.go b/aws-source/adapters/elbv2-load-balancer.go index 59967eb2..d330d0aa 100644 --- a/aws-source/adapters/elbv2-load-balancer.go +++ b/aws-source/adapters/elbv2-load-balancer.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeLoadBalancersInput, output *elbv2.DescribeLoadBalancersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-load-balancer_test.go b/aws-source/adapters/elbv2-load-balancer_test.go index 05dd7c62..4a5ee666 100644 --- a/aws-source/adapters/elbv2-load-balancer_test.go +++ b/aws-source/adapters/elbv2-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestLoadBalancerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-rule.go b/aws-source/adapters/elbv2-rule.go index 99aa3795..913030f8 100644 --- a/aws-source/adapters/elbv2-rule.go +++ b/aws-source/adapters/elbv2-rule.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func ruleOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeRulesInput, output *elbv2.DescribeRulesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-rule_test.go b/aws-source/adapters/elbv2-rule_test.go index 6117bb2c..9fdd508f 100644 --- a/aws-source/adapters/elbv2-rule_test.go +++ b/aws-source/adapters/elbv2-rule_test.go @@ -9,9 +9,9 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestRuleOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-group.go b/aws-source/adapters/elbv2-target-group.go index bc4fca11..9d7789d0 100644 --- a/aws-source/adapters/elbv2-target-group.go +++ b/aws-source/adapters/elbv2-target-group.go @@ -6,8 +6,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeTargetGroupsInput, output *elbv2.DescribeTargetGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-target-group_test.go b/aws-source/adapters/elbv2-target-group_test.go index 88a48ab7..b296a9d9 100644 --- a/aws-source/adapters/elbv2-target-group_test.go +++ b/aws-source/adapters/elbv2-target-group_test.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestTargetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-health.go b/aws-source/adapters/elbv2-target-health.go index 783100aa..e4c129f2 100644 --- a/aws-source/adapters/elbv2-target-health.go +++ b/aws-source/adapters/elbv2-target-health.go @@ -10,8 +10,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type TargetHealthUniqueID struct { diff --git a/aws-source/adapters/elbv2-target-health_test.go b/aws-source/adapters/elbv2-target-health_test.go index c4b8171b..72810f38 100644 --- a/aws-source/adapters/elbv2-target-health_test.go +++ b/aws-source/adapters/elbv2-target-health_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestTargetHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2.go b/aws-source/adapters/elbv2.go index a045b1af..d083a106 100644 --- a/aws-source/adapters/elbv2.go +++ b/aws-source/adapters/elbv2.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) type elbv2Client interface { diff --git a/aws-source/adapters/elbv2_test.go b/aws-source/adapters/elbv2_test.go index 10926076..7be9fd0b 100644 --- a/aws-source/adapters/elbv2_test.go +++ b/aws-source/adapters/elbv2_test.go @@ -6,7 +6,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) type mockElbv2Client struct{} diff --git a/aws-source/adapters/iam-group.go b/aws-source/adapters/iam-group.go index 99dbcea5..f8bae2f2 100644 --- a/aws-source/adapters/iam-group.go +++ b/aws-source/adapters/iam-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func groupGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.Group, error) { diff --git a/aws-source/adapters/iam-group_test.go b/aws-source/adapters/iam-group_test.go index ec6651fe..20872517 100644 --- a/aws-source/adapters/iam-group_test.go +++ b/aws-source/adapters/iam-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-instance-profile.go b/aws-source/adapters/iam-instance-profile.go index 7ca23edc..b6fff4d2 100644 --- a/aws-source/adapters/iam-instance-profile.go +++ b/aws-source/adapters/iam-instance-profile.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func instanceProfileGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.InstanceProfile, error) { diff --git a/aws-source/adapters/iam-instance-profile_test.go b/aws-source/adapters/iam-instance-profile_test.go index 092e9d58..2b86d004 100644 --- a/aws-source/adapters/iam-instance-profile_test.go +++ b/aws-source/adapters/iam-instance-profile_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestInstanceProfileItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-policy.go b/aws-source/adapters/iam-policy.go index 009cb900..16bd0713 100644 --- a/aws-source/adapters/iam-policy.go +++ b/aws-source/adapters/iam-policy.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/iam-policy_test.go b/aws-source/adapters/iam-policy_test.go index cbdf09cc..6ce52a03 100644 --- a/aws-source/adapters/iam-policy_test.go +++ b/aws-source/adapters/iam-policy_test.go @@ -9,9 +9,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *TestIAMClient) GetPolicy(ctx context.Context, params *iam.GetPolicyInput, optFns ...func(*iam.Options)) (*iam.GetPolicyOutput, error) { diff --git a/aws-source/adapters/iam-role.go b/aws-source/adapters/iam-role.go index 58d735ac..ed72506e 100644 --- a/aws-source/adapters/iam-role.go +++ b/aws-source/adapters/iam-role.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/sourcegraph/conc/iter" ) diff --git a/aws-source/adapters/iam-role_test.go b/aws-source/adapters/iam-role_test.go index 8b581863..ee92d961 100644 --- a/aws-source/adapters/iam-role_test.go +++ b/aws-source/adapters/iam-role_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *TestIAMClient) GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) { diff --git a/aws-source/adapters/iam-user.go b/aws-source/adapters/iam-user.go index c96801f2..47c49fa1 100644 --- a/aws-source/adapters/iam-user.go +++ b/aws-source/adapters/iam-user.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type UserDetails struct { diff --git a/aws-source/adapters/iam-user_test.go b/aws-source/adapters/iam-user_test.go index 3d3bbe71..bd432c76 100644 --- a/aws-source/adapters/iam-user_test.go +++ b/aws-source/adapters/iam-user_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListGroupsForUserInput, optFns ...func(*iam.Options)) (*iam.ListGroupsForUserOutput, error) { diff --git a/aws-source/adapters/iam.go b/aws-source/adapters/iam.go index 230a5011..78128364 100644 --- a/aws-source/adapters/iam.go +++ b/aws-source/adapters/iam.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) type IAMClient interface { diff --git a/aws-source/adapters/iam_test.go b/aws-source/adapters/iam_test.go index 889a91cc..f345b877 100644 --- a/aws-source/adapters/iam_test.go +++ b/aws-source/adapters/iam_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/tracing" ) // TestIAMClient Test client that returns three pages diff --git a/aws-source/adapters/integration/apigateway/apigateway_test.go b/aws-source/adapters/integration/apigateway/apigateway_test.go index 25211873..1258ad25 100644 --- a/aws-source/adapters/integration/apigateway/apigateway_test.go +++ b/aws-source/adapters/integration/apigateway/apigateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func APIGateway(t *testing.T) { diff --git a/aws-source/adapters/integration/ec2/instance_test.go b/aws-source/adapters/integration/ec2/instance_test.go index 730339ef..d40a2774 100644 --- a/aws-source/adapters/integration/ec2/instance_test.go +++ b/aws-source/adapters/integration/ec2/instance_test.go @@ -7,9 +7,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/kms/kms_test.go b/aws-source/adapters/integration/kms/kms_test.go index 3733d15c..8669d281 100644 --- a/aws-source/adapters/integration/kms/kms_test.go +++ b/aws-source/adapters/integration/kms/kms_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/networkmanager/networkmanager_test.go b/aws-source/adapters/integration/networkmanager/networkmanager_test.go index 7a2f490f..e224c985 100644 --- a/aws-source/adapters/integration/networkmanager/networkmanager_test.go +++ b/aws-source/adapters/integration/networkmanager/networkmanager_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/ssm/main_test.go b/aws-source/adapters/integration/ssm/main_test.go index db6b64ab..d8cab0c9 100644 --- a/aws-source/adapters/integration/ssm/main_test.go +++ b/aws-source/adapters/integration/ssm/main_test.go @@ -14,9 +14,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm/types" "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/tracing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) diff --git a/aws-source/adapters/integration/util.go b/aws-source/adapters/integration/util.go index e6828249..67067b05 100644 --- a/aws-source/adapters/integration/util.go +++ b/aws-source/adapters/integration/util.go @@ -13,8 +13,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/tracing" ) const ( diff --git a/aws-source/adapters/kms-alias.go b/aws-source/adapters/kms-alias.go index 0272a291..ac114c0d 100644 --- a/aws-source/adapters/kms-alias.go +++ b/aws-source/adapters/kms-alias.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func aliasOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.ListAliasesInput, output *kms.ListAliasesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/kms-alias_test.go b/aws-source/adapters/kms-alias_test.go index 4b50aa6c..de6566e1 100644 --- a/aws-source/adapters/kms-alias_test.go +++ b/aws-source/adapters/kms-alias_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestAliasOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-custom-key-store.go b/aws-source/adapters/kms-custom-key-store.go index ddaa0742..74fb2ce5 100644 --- a/aws-source/adapters/kms-custom-key-store.go +++ b/aws-source/adapters/kms-custom-key-store.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.DescribeCustomKeyStoresInput, output *kms.DescribeCustomKeyStoresOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/kms-custom-key-store_test.go b/aws-source/adapters/kms-custom-key-store_test.go index 10a420b0..1cdcfaf3 100644 --- a/aws-source/adapters/kms-custom-key-store_test.go +++ b/aws-source/adapters/kms-custom-key-store_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestCustomKeyStoreOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-grant.go b/aws-source/adapters/kms-grant.go index 66b8bd49..2e638039 100644 --- a/aws-source/adapters/kms-grant.go +++ b/aws-source/adapters/kms-grant.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/kms-grant_test.go b/aws-source/adapters/kms-grant_test.go index 3dd5a08a..bef28adc 100644 --- a/aws-source/adapters/kms-grant_test.go +++ b/aws-source/adapters/kms-grant_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key-policy.go b/aws-source/adapters/kms-key-policy.go index aa2895c7..be21fd5e 100644 --- a/aws-source/adapters/kms-key-policy.go +++ b/aws-source/adapters/kms-key-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/kms-key-policy_test.go b/aws-source/adapters/kms-key-policy_test.go index 96682d43..0aebd1c3 100644 --- a/aws-source/adapters/kms-key-policy_test.go +++ b/aws-source/adapters/kms-key-policy_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key.go b/aws-source/adapters/kms-key.go index cf7db93b..1c20b12e 100644 --- a/aws-source/adapters/kms-key.go +++ b/aws-source/adapters/kms-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type kmsClient interface { diff --git a/aws-source/adapters/kms-key_test.go b/aws-source/adapters/kms-key_test.go index 5d88422c..1e135a51 100644 --- a/aws-source/adapters/kms-key_test.go +++ b/aws-source/adapters/kms-key_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type kmsTestClient struct{} diff --git a/aws-source/adapters/lambda-event-source-mapping.go b/aws-source/adapters/lambda-event-source-mapping.go index 5a53be8a..ebb16c41 100644 --- a/aws-source/adapters/lambda-event-source-mapping.go +++ b/aws-source/adapters/lambda-event-source-mapping.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type lambdaEventSourceMappingClient interface { diff --git a/aws-source/adapters/lambda-event-source-mapping_test.go b/aws-source/adapters/lambda-event-source-mapping_test.go index e671c50d..63c170dd 100644 --- a/aws-source/adapters/lambda-event-source-mapping_test.go +++ b/aws-source/adapters/lambda-event-source-mapping_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type TestLambdaEventSourceMappingClient struct{} diff --git a/aws-source/adapters/lambda-function.go b/aws-source/adapters/lambda-function.go index 5295ed26..e63a200f 100644 --- a/aws-source/adapters/lambda-function.go +++ b/aws-source/adapters/lambda-function.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type FunctionDetails struct { diff --git a/aws-source/adapters/lambda-function_test.go b/aws-source/adapters/lambda-function_test.go index f6613dfe..239ec1d1 100644 --- a/aws-source/adapters/lambda-function_test.go +++ b/aws-source/adapters/lambda-function_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var testFuncConfig = &types.FunctionConfiguration{ diff --git a/aws-source/adapters/lambda-layer-version.go b/aws-source/adapters/lambda-layer-version.go index 522cfd52..f3d6dcdd 100644 --- a/aws-source/adapters/lambda-layer-version.go +++ b/aws-source/adapters/lambda-layer-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func layerVersionGetInputMapper(scope, query string) *lambda.GetLayerVersionInput { diff --git a/aws-source/adapters/lambda-layer-version_test.go b/aws-source/adapters/lambda-layer-version_test.go index 09c06411..02da971e 100644 --- a/aws-source/adapters/lambda-layer-version_test.go +++ b/aws-source/adapters/lambda-layer-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestLayerVersionGetInputMapper(t *testing.T) { diff --git a/aws-source/adapters/lambda-layer.go b/aws-source/adapters/lambda-layer.go index e467bf55..ebeb741b 100644 --- a/aws-source/adapters/lambda-layer.go +++ b/aws-source/adapters/lambda-layer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func layerListFunc(ctx context.Context, client *lambda.Client, scope string) ([]*types.LayersListItem, error) { diff --git a/aws-source/adapters/lambda-layer_test.go b/aws-source/adapters/lambda-layer_test.go index 71713317..a1d2e0c5 100644 --- a/aws-source/adapters/lambda-layer_test.go +++ b/aws-source/adapters/lambda-layer_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestLayerItemMapper(t *testing.T) { diff --git a/aws-source/adapters/main.go b/aws-source/adapters/main.go index 6a5baa27..08905ff3 100644 --- a/aws-source/adapters/main.go +++ b/aws-source/adapters/main.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) var Metadata = sdp.AdapterMetadataList{} diff --git a/aws-source/adapters/network-firewall-firewall-policy.go b/aws-source/adapters/network-firewall-firewall-policy.go index daf15cb1..ca914622 100644 --- a/aws-source/adapters/network-firewall-firewall-policy.go +++ b/aws-source/adapters/network-firewall-firewall-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type unifiedFirewallPolicy struct { diff --git a/aws-source/adapters/network-firewall-firewall-policy_test.go b/aws-source/adapters/network-firewall-firewall-policy_test.go index 109185c5..80728ae6 100644 --- a/aws-source/adapters/network-firewall-firewall-policy_test.go +++ b/aws-source/adapters/network-firewall-firewall-policy_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-firewall.go b/aws-source/adapters/network-firewall-firewall.go index f67e47a3..f3fb301a 100644 --- a/aws-source/adapters/network-firewall-firewall.go +++ b/aws-source/adapters/network-firewall-firewall.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type unifiedFirewall struct { diff --git a/aws-source/adapters/network-firewall-firewall_test.go b/aws-source/adapters/network-firewall-firewall_test.go index 4417e200..68bb239a 100644 --- a/aws-source/adapters/network-firewall-firewall_test.go +++ b/aws-source/adapters/network-firewall-firewall_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params *networkfirewall.DescribeFirewallInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeFirewallOutput, error) { diff --git a/aws-source/adapters/network-firewall-rule-group.go b/aws-source/adapters/network-firewall-rule-group.go index 1af444c2..d59ab06c 100644 --- a/aws-source/adapters/network-firewall-rule-group.go +++ b/aws-source/adapters/network-firewall-rule-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type unifiedRuleGroup struct { diff --git a/aws-source/adapters/network-firewall-rule-group_test.go b/aws-source/adapters/network-firewall-rule-group_test.go index d4773adc..b8dd06b5 100644 --- a/aws-source/adapters/network-firewall-rule-group_test.go +++ b/aws-source/adapters/network-firewall-rule-group_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration.go b/aws-source/adapters/network-firewall-tls-inspection-configuration.go index d2354ab8..5892d208 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type unifiedTLSInspectionConfiguration struct { diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go index bd623260..3e2ee831 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/networkfirewall.go b/aws-source/adapters/networkfirewall.go index 45a2bb8a..d23c7860 100644 --- a/aws-source/adapters/networkfirewall.go +++ b/aws-source/adapters/networkfirewall.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) type networkFirewallClient interface { diff --git a/aws-source/adapters/networkmanager-connect-attachment.go b/aws-source/adapters/networkmanager-connect-attachment.go index df3d4010..2dac9b47 100644 --- a/aws-source/adapters/networkmanager-connect-attachment.go +++ b/aws-source/adapters/networkmanager-connect-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func connectAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.ConnectAttachment, error) { diff --git a/aws-source/adapters/networkmanager-connect-attachment_test.go b/aws-source/adapters/networkmanager-connect-attachment_test.go index d8e398e5..4a3b55ba 100644 --- a/aws-source/adapters/networkmanager-connect-attachment_test.go +++ b/aws-source/adapters/networkmanager-connect-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestConnectAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer-association.go b/aws-source/adapters/networkmanager-connect-peer-association.go index 0624ac89..3a6631aa 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-connect-peer-association.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectPeerAssociationsInput, output *networkmanager.GetConnectPeerAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connect-peer-association_test.go b/aws-source/adapters/networkmanager-connect-peer-association_test.go index 8d2e1188..25c4dbda 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer.go b/aws-source/adapters/networkmanager-connect-peer.go index 99742bc7..7195086a 100644 --- a/aws-source/adapters/networkmanager-connect-peer.go +++ b/aws-source/adapters/networkmanager-connect-peer.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetConnectPeerInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connect-peer_test.go b/aws-source/adapters/networkmanager-connect-peer_test.go index 0cf3c2e3..22f2b127 100644 --- a/aws-source/adapters/networkmanager-connect-peer_test.go +++ b/aws-source/adapters/networkmanager-connect-peer_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func (n NetworkManagerTestClient) GetConnectPeer(ctx context.Context, params *networkmanager.GetConnectPeerInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetConnectPeerOutput, error) { diff --git a/aws-source/adapters/networkmanager-connection.go b/aws-source/adapters/networkmanager-connection.go index 8c3c476b..37ba42f0 100644 --- a/aws-source/adapters/networkmanager-connection.go +++ b/aws-source/adapters/networkmanager-connection.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectionsInput, output *networkmanager.GetConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connection_test.go b/aws-source/adapters/networkmanager-connection_test.go index c873431c..8e633c7b 100644 --- a/aws-source/adapters/networkmanager-connection_test.go +++ b/aws-source/adapters/networkmanager-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network-policy.go b/aws-source/adapters/networkmanager-core-network-policy.go index fd934323..46c61af1 100644 --- a/aws-source/adapters/networkmanager-core-network-policy.go +++ b/aws-source/adapters/networkmanager-core-network-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func coreNetworkPolicyGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.CoreNetworkPolicy, error) { diff --git a/aws-source/adapters/networkmanager-core-network-policy_test.go b/aws-source/adapters/networkmanager-core-network-policy_test.go index 5cbbb949..25a3667a 100644 --- a/aws-source/adapters/networkmanager-core-network-policy_test.go +++ b/aws-source/adapters/networkmanager-core-network-policy_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestCoreNetworkPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network.go b/aws-source/adapters/networkmanager-core-network.go index bd393cb8..f11fc0ca 100644 --- a/aws-source/adapters/networkmanager-core-network.go +++ b/aws-source/adapters/networkmanager-core-network.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetCoreNetworkInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-core-network_test.go b/aws-source/adapters/networkmanager-core-network_test.go index e90a7faa..88d9fede 100644 --- a/aws-source/adapters/networkmanager-core-network_test.go +++ b/aws-source/adapters/networkmanager-core-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func (n NetworkManagerTestClient) GetCoreNetwork(ctx context.Context, params *networkmanager.GetCoreNetworkInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetCoreNetworkOutput, error) { diff --git a/aws-source/adapters/networkmanager-device.go b/aws-source/adapters/networkmanager-device.go index cfbd8686..308b774c 100644 --- a/aws-source/adapters/networkmanager-device.go +++ b/aws-source/adapters/networkmanager-device.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetDevicesInput, output *networkmanager.GetDevicesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-device_test.go b/aws-source/adapters/networkmanager-device_test.go index 76f432e1..79deae1a 100644 --- a/aws-source/adapters/networkmanager-device_test.go +++ b/aws-source/adapters/networkmanager-device_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDeviceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-global-network.go b/aws-source/adapters/networkmanager-global-network.go index 7918d5b1..c561f511 100644 --- a/aws-source/adapters/networkmanager-global-network.go +++ b/aws-source/adapters/networkmanager-global-network.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, scope string, _ *networkmanager.DescribeGlobalNetworksInput, output *networkmanager.DescribeGlobalNetworksOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-global-network_test.go b/aws-source/adapters/networkmanager-global-network_test.go index 2dfd5299..02e8a8d4 100644 --- a/aws-source/adapters/networkmanager-global-network_test.go +++ b/aws-source/adapters/networkmanager-global-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestGlobalNetworkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link-association.go b/aws-source/adapters/networkmanager-link-association.go index 971d705a..df3cebc2 100644 --- a/aws-source/adapters/networkmanager-link-association.go +++ b/aws-source/adapters/networkmanager-link-association.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinkAssociationsInput, output *networkmanager.GetLinkAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-link-association_test.go b/aws-source/adapters/networkmanager-link-association_test.go index f753e9ea..f06f11bd 100644 --- a/aws-source/adapters/networkmanager-link-association_test.go +++ b/aws-source/adapters/networkmanager-link-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestLinkAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link.go b/aws-source/adapters/networkmanager-link.go index 36c51466..87c87739 100644 --- a/aws-source/adapters/networkmanager-link.go +++ b/aws-source/adapters/networkmanager-link.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinksInput, output *networkmanager.GetLinksOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-link_test.go b/aws-source/adapters/networkmanager-link_test.go index 4f3c3e60..3468dc75 100644 --- a/aws-source/adapters/networkmanager-link_test.go +++ b/aws-source/adapters/networkmanager-link_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestLinkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-network-resource-relationship.go b/aws-source/adapters/networkmanager-network-resource-relationship.go index df7961dd..24a17008 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, input *networkmanager.GetNetworkResourceRelationshipsInput, output *networkmanager.GetNetworkResourceRelationshipsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-network-resource-relationship_test.go b/aws-source/adapters/networkmanager-network-resource-relationship_test.go index 6a18cfbd..464a1716 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship_test.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestNetworkResourceRelationshipOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go index 30ad792c..961d7439 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func getSiteToSiteVpnAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.SiteToSiteVpnAttachment, error) { diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go index 19e47712..a1c0ba85 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestSiteToSiteVpnAttachmentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site.go b/aws-source/adapters/networkmanager-site.go index e8024478..19fd7b06 100644 --- a/aws-source/adapters/networkmanager-site.go +++ b/aws-source/adapters/networkmanager-site.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetSitesInput, output *networkmanager.GetSitesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-site_test.go b/aws-source/adapters/networkmanager-site_test.go index 94299f3f..4ce76b04 100644 --- a/aws-source/adapters/networkmanager-site_test.go +++ b/aws-source/adapters/networkmanager-site_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSiteOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go index 7b003117..1588ecb5 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayConnectPeerAssociationsInput, output *networkmanager.GetTransitGatewayConnectPeerAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go index f0afd62f..d63b5bd7 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestTransitGatewayConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering.go b/aws-source/adapters/networkmanager-transit-gateway-peering.go index 7e840206..5e1c140a 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func getTransitGatewayPeeringGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayPeering, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go index 3b530c94..f7552bde 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestTransitGatewayPeeringOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration.go b/aws-source/adapters/networkmanager-transit-gateway-registration.go index d0d1c3a0..7a9d9b2b 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayRegistrationsInput, output *networkmanager.GetTransitGatewayRegistrationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go index 36dca8d0..4de15a86 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestTransitGatewayRegistrationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go index fd931e1e..9653b0b4 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func getTransitGatewayRouteTableAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayRouteTableAttachment, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go index 1c36a9e8..8d775e75 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-vpc-attachment.go b/aws-source/adapters/networkmanager-vpc-attachment.go index 08e16a9b..92cdcba7 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment.go +++ b/aws-source/adapters/networkmanager-vpc-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func vpcAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.VpcAttachment, error) { diff --git a/aws-source/adapters/networkmanager-vpc-attachment_test.go b/aws-source/adapters/networkmanager-vpc-attachment_test.go index 222742e4..bdcfcdf4 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment_test.go +++ b/aws-source/adapters/networkmanager-vpc-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestVPCAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group.go b/aws-source/adapters/rds-db-cluster-parameter-group.go index ca386672..8dd1689f 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type ClusterParameterGroup struct { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group_test.go b/aws-source/adapters/rds-db-cluster-parameter-group_test.go index 6d89db4d..3c79d2ea 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group_test.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestDBClusterParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster.go b/aws-source/adapters/rds-db-cluster.go index 8cf90920..59db9fbe 100644 --- a/aws-source/adapters/rds-db-cluster.go +++ b/aws-source/adapters/rds-db-cluster.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBClustersInput, output *rds.DescribeDBClustersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/rds-db-cluster_test.go b/aws-source/adapters/rds-db-cluster_test.go index c35d57e9..7fbd2f8e 100644 --- a/aws-source/adapters/rds-db-cluster_test.go +++ b/aws-source/adapters/rds-db-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDBClusterOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-instance.go b/aws-source/adapters/rds-db-instance.go index 2b882a57..5dc63163 100644 --- a/aws-source/adapters/rds-db-instance.go +++ b/aws-source/adapters/rds-db-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func statusToHealth(status string) *sdp.Health { diff --git a/aws-source/adapters/rds-db-instance_test.go b/aws-source/adapters/rds-db-instance_test.go index 6fe35b5b..94b2773d 100644 --- a/aws-source/adapters/rds-db-instance_test.go +++ b/aws-source/adapters/rds-db-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDBInstanceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-parameter-group.go b/aws-source/adapters/rds-db-parameter-group.go index 2aeca89e..4d8e90e1 100644 --- a/aws-source/adapters/rds-db-parameter-group.go +++ b/aws-source/adapters/rds-db-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type ParameterGroup struct { diff --git a/aws-source/adapters/rds-db-parameter-group_test.go b/aws-source/adapters/rds-db-parameter-group_test.go index 3c3d10e0..d403a519 100644 --- a/aws-source/adapters/rds-db-parameter-group_test.go +++ b/aws-source/adapters/rds-db-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestDBParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-subnet-group.go b/aws-source/adapters/rds-db-subnet-group.go index 2f22822a..0cf96777 100644 --- a/aws-source/adapters/rds-db-subnet-group.go +++ b/aws-source/adapters/rds-db-subnet-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBSubnetGroupsInput, output *rds.DescribeDBSubnetGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/rds-db-subnet-group_test.go b/aws-source/adapters/rds-db-subnet-group_test.go index 4c1ac8e8..4bf62d79 100644 --- a/aws-source/adapters/rds-db-subnet-group_test.go +++ b/aws-source/adapters/rds-db-subnet-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestDBSubnetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-option-group.go b/aws-source/adapters/rds-option-group.go index 846eb6f6..5bdbe97e 100644 --- a/aws-source/adapters/rds-option-group.go +++ b/aws-source/adapters/rds-option-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func optionGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeOptionGroupsInput, output *rds.DescribeOptionGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/route53-health-check.go b/aws-source/adapters/route53-health-check.go index f743e866..9f98fafb 100644 --- a/aws-source/adapters/route53-health-check.go +++ b/aws-source/adapters/route53-health-check.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type HealthCheck struct { diff --git a/aws-source/adapters/route53-health-check_test.go b/aws-source/adapters/route53-health-check_test.go index 69024a9a..af72f1b0 100644 --- a/aws-source/adapters/route53-health-check_test.go +++ b/aws-source/adapters/route53-health-check_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestHealthCheckItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-hosted-zone.go b/aws-source/adapters/route53-hosted-zone.go index bf6f855e..63df5f28 100644 --- a/aws-source/adapters/route53-hosted-zone.go +++ b/aws-source/adapters/route53-hosted-zone.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func hostedZoneGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.HostedZone, error) { diff --git a/aws-source/adapters/route53-hosted-zone_test.go b/aws-source/adapters/route53-hosted-zone_test.go index 322c2da5..abed7dc5 100644 --- a/aws-source/adapters/route53-hosted-zone_test.go +++ b/aws-source/adapters/route53-hosted-zone_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestHostedZoneItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-resource-record-set.go b/aws-source/adapters/route53-resource-record-set.go index d6ba1d8a..cc36042b 100644 --- a/aws-source/adapters/route53-resource-record-set.go +++ b/aws-source/adapters/route53-resource-record-set.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func resourceRecordSetGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.ResourceRecordSet, error) { diff --git a/aws-source/adapters/route53-resource-record-set_test.go b/aws-source/adapters/route53-resource-record-set_test.go index 6719e02e..9559fd38 100644 --- a/aws-source/adapters/route53-resource-record-set_test.go +++ b/aws-source/adapters/route53-resource-record-set_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestResourceRecordSetItemMapper(t *testing.T) { diff --git a/aws-source/adapters/s3.go b/aws-source/adapters/s3.go index 46599ed2..9c3b21d6 100644 --- a/aws-source/adapters/s3.go +++ b/aws-source/adapters/s3.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) const CacheDuration = 10 * time.Minute diff --git a/aws-source/adapters/s3_test.go b/aws-source/adapters/s3_test.go index b3e23b51..65ae8fdd 100644 --- a/aws-source/adapters/s3_test.go +++ b/aws-source/adapters/s3_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestS3SearchImpl(t *testing.T) { diff --git a/aws-source/adapters/sns-data-protection-policy.go b/aws-source/adapters/sns-data-protection-policy.go index 6a20b0f7..7f566cb5 100644 --- a/aws-source/adapters/sns-data-protection-policy.go +++ b/aws-source/adapters/sns-data-protection-policy.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type dataProtectionPolicyClient interface { diff --git a/aws-source/adapters/sns-data-protection-policy_test.go b/aws-source/adapters/sns-data-protection-policy_test.go index a1c75dca..c9283234 100644 --- a/aws-source/adapters/sns-data-protection-policy_test.go +++ b/aws-source/adapters/sns-data-protection-policy_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type mockDataProtectionPolicyClient struct{} diff --git a/aws-source/adapters/sns-endpoint.go b/aws-source/adapters/sns-endpoint.go index 8bf5c486..afbd1c2e 100644 --- a/aws-source/adapters/sns-endpoint.go +++ b/aws-source/adapters/sns-endpoint.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type endpointClient interface { diff --git a/aws-source/adapters/sns-endpoint_test.go b/aws-source/adapters/sns-endpoint_test.go index e8ddd5b2..a71e638c 100644 --- a/aws-source/adapters/sns-endpoint_test.go +++ b/aws-source/adapters/sns-endpoint_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type mockEndpointClient struct{} diff --git a/aws-source/adapters/sns-platform-application.go b/aws-source/adapters/sns-platform-application.go index 90354de1..3750a65d 100644 --- a/aws-source/adapters/sns-platform-application.go +++ b/aws-source/adapters/sns-platform-application.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type platformApplicationClient interface { diff --git a/aws-source/adapters/sns-platform-application_test.go b/aws-source/adapters/sns-platform-application_test.go index 3bc4d2a3..a493b98c 100644 --- a/aws-source/adapters/sns-platform-application_test.go +++ b/aws-source/adapters/sns-platform-application_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type mockPlatformApplicationClient struct{} diff --git a/aws-source/adapters/sns-subscription.go b/aws-source/adapters/sns-subscription.go index 3df89e62..7fb609ad 100644 --- a/aws-source/adapters/sns-subscription.go +++ b/aws-source/adapters/sns-subscription.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type subsCli interface { diff --git a/aws-source/adapters/sns-subscription_test.go b/aws-source/adapters/sns-subscription_test.go index f3b46f59..dc39df2c 100644 --- a/aws-source/adapters/sns-subscription_test.go +++ b/aws-source/adapters/sns-subscription_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type snsTestClient struct{} diff --git a/aws-source/adapters/sns-topic.go b/aws-source/adapters/sns-topic.go index 9bbd8d8b..0eebb622 100644 --- a/aws-source/adapters/sns-topic.go +++ b/aws-source/adapters/sns-topic.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type topicClient interface { diff --git a/aws-source/adapters/sns-topic_test.go b/aws-source/adapters/sns-topic_test.go index 484fd227..e1a4f216 100644 --- a/aws-source/adapters/sns-topic_test.go +++ b/aws-source/adapters/sns-topic_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) type testTopicClient struct{} diff --git a/aws-source/adapters/sqs-queue.go b/aws-source/adapters/sqs-queue.go index a6eccbe2..d1f8713e 100644 --- a/aws-source/adapters/sqs-queue.go +++ b/aws-source/adapters/sqs-queue.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type sqsClient interface { diff --git a/aws-source/adapters/sqs-queue_test.go b/aws-source/adapters/sqs-queue_test.go index 54672448..42de525d 100644 --- a/aws-source/adapters/sqs-queue_test.go +++ b/aws-source/adapters/sqs-queue_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type testClient struct{} diff --git a/aws-source/adapters/ssm-parameter.go b/aws-source/adapters/ssm-parameter.go index ea8b5a32..61e5e3ab 100644 --- a/aws-source/adapters/ssm-parameter.go +++ b/aws-source/adapters/ssm-parameter.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/ssm-parameter_test.go b/aws-source/adapters/ssm-parameter_test.go index 2c01d3b7..6d199c9f 100644 --- a/aws-source/adapters/ssm-parameter_test.go +++ b/aws-source/adapters/ssm-parameter_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" ) type mockSSMClient struct{} diff --git a/aws-source/build/package/Dockerfile b/aws-source/build/package/Dockerfile index 204bdfa6..a5b103da 100644 --- a/aws-source/build/package/Dockerfile +++ b/aws-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/aws-source/cmd/root.go b/aws-source/cmd/root.go index 0a9a4f11..07231afd 100644 --- a/aws-source/cmd/root.go +++ b/aws-source/cmd/root.go @@ -10,9 +10,9 @@ import ( "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/aws-source/proc" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/logging" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/logging" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/aws-source/proc/proc.go b/aws-source/proc/proc.go index c049e7c4..e8881fe4 100644 --- a/aws-source/proc/proc.go +++ b/aws-source/proc/proc.go @@ -41,8 +41,8 @@ import ( stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" diff --git a/aws-source/proc/proc_test.go b/aws-source/proc/proc_test.go index 630f050c..097e6020 100644 --- a/aws-source/proc/proc_test.go +++ b/aws-source/proc/proc_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/aws/smithy-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cmd/auth_client.go b/cmd/auth_client.go index 27e2fddd..2efef5be 100644 --- a/cmd/auth_client.go +++ b/cmd/auth_client.go @@ -7,10 +7,10 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/auth" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpconnect" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" ) diff --git a/cmd/auth_client_test.go b/cmd/auth_client_test.go index 8a18e238..7fa130b6 100644 --- a/cmd/auth_client_test.go +++ b/cmd/auth_client_test.go @@ -14,8 +14,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/auth" + "github.com/overmindtech/workspace/tracing" ) // testProxyServer is a simple HTTP proxy server for testing diff --git a/cmd/bookmarks_create_bookmark.go b/cmd/bookmarks_create_bookmark.go index 4a2cf333..4717c8bc 100644 --- a/cmd/bookmarks_create_bookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_affected_bookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go index 8b69df2e..c0523149 100644 --- a/cmd/bookmarks_get_affected_bookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -5,7 +5,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_bookmark.go b/cmd/bookmarks_get_bookmark.go index 142cd33c..62e8646e 100644 --- a/cmd/bookmarks_get_bookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index 9d5ca4df..a48e4d96 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -4,7 +4,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index 4237e4c6..bb370c26 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -8,7 +8,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_get_change_test.go b/cmd/changes_get_change_test.go index defabcef..6f201b3f 100644 --- a/cmd/changes_get_change_test.go +++ b/cmd/changes_get_change_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) func TestValidateChangeStatus(t *testing.T) { diff --git a/cmd/changes_get_signals.go b/cmd/changes_get_signals.go index 52d7420c..6f7e1311 100644 --- a/cmd/changes_get_signals.go +++ b/cmd/changes_get_signals.go @@ -6,7 +6,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_list_changes.go b/cmd/changes_list_changes.go index 073ff8bc..6824cf08 100644 --- a/cmd/changes_list_changes.go +++ b/cmd/changes_list_changes.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 7d915871..324cad4b 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -5,7 +5,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 1af78f20..94405321 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -12,7 +12,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_submit_signal.go b/cmd/changes_submit_signal.go index a218eae6..dafd6642 100644 --- a/cmd/changes_submit_signal.go +++ b/cmd/changes_submit_signal.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/explore.go b/cmd/explore.go index ed2e3550..8164042f 100644 --- a/cmd/explore.go +++ b/cmd/explore.go @@ -18,12 +18,12 @@ import ( "github.com/overmindtech/pterm" "github.com/overmindtech/cli/aws-source/proc" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" azureproc "github.com/overmindtech/cli/sources/azure/proc" gcpproc "github.com/overmindtech/cli/sources/gcp/proc" stdlibSource "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" diff --git a/cmd/flags.go b/cmd/flags.go index 7f6318ed..357625fe 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/spf13/cobra" "github.com/spf13/viper" ) diff --git a/cmd/flags_test.go b/cmd/flags_test.go index 14e7e14b..ad30a99a 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/spf13/viper" ) diff --git a/cmd/integrations_tfc.go b/cmd/integrations_tfc.go index 7d2b4513..babd2a21 100644 --- a/cmd/integrations_tfc.go +++ b/cmd/integrations_tfc.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/cmd/invites_crud.go b/cmd/invites_crud.go index 40963e47..8800ec69 100644 --- a/cmd/invites_crud.go +++ b/cmd/invites_crud.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/jedib0t/go-pretty/v6/table" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/pterm.go b/cmd/pterm.go index abb1eec7..de9c9881 100644 --- a/cmd/pterm.go +++ b/cmd/pterm.go @@ -15,11 +15,11 @@ import ( "connectrpc.com/connect" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/auth" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpconnect" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "github.com/spf13/cobra" diff --git a/cmd/request.go b/cmd/request.go index 2926239e..3f0d5b98 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -4,8 +4,8 @@ import ( "context" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpws" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_load.go b/cmd/request_load.go index 2bbd779c..16de26d6 100644 --- a/cmd/request_load.go +++ b/cmd/request_load.go @@ -6,9 +6,9 @@ import ( "os" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpws" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_query.go b/cmd/request_query.go index af84d075..43887b7a 100644 --- a/cmd/request_query.go +++ b/cmd/request_query.go @@ -7,9 +7,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpws" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/root.go b/cmd/root.go index a262b11b..f02798a0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,9 +22,9 @@ import ( josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/auth" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/cmd/root_test.go b/cmd/root_test.go index 4f926537..c0589e02 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/auth" + "github.com/overmindtech/workspace/auth" "golang.org/x/oauth2" ) diff --git a/cmd/snapshots_create.go b/cmd/snapshots_create.go index 3e965547..03244504 100644 --- a/cmd/snapshots_create.go +++ b/cmd/snapshots_create.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpws" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdp-go/sdpws" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/snapshots_get_snapshot.go b/cmd/snapshots_get_snapshot.go index ce90080b..3778ed75 100644 --- a/cmd/snapshots_get_snapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 0617c2bf..02266d15 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -11,7 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index fae484b7..ad4643c6 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -17,7 +17,7 @@ import ( "github.com/muesli/reflow/wordwrap" "github.com/overmindtech/pterm" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/discovery/adapter.go b/discovery/adapter.go deleted file mode 100644 index 5d0b473d..00000000 --- a/discovery/adapter.go +++ /dev/null @@ -1,205 +0,0 @@ -package discovery - -import ( - "context" - "slices" - "sync" - - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" -) - -// Adapter is capable of finding information about items -// -// Adapters must implement all of the methods to satisfy this interface in order -// to be able to used as an SDP adapter. Note that the `context.Context` value -// that is passed to the Get(), List() and Search() (optional) methods needs to -// handled by each adapter individually. Adapter authors should make an effort -// ensure that expensive operations that the adapter undertakes can be cancelled -// if the context `ctx` is cancelled -type Adapter interface { - // Type The type of items that this adapter is capable of finding - Type() string - - // Descriptive name for the adapter, used in logging and metadata - Name() string - - // List of scopes that this adapter is capable of find items for. If the - // adapter supports all scopes the special value "*" - // should be used - Scopes() []string - - // Get Get a single item with a given scope and query. The item returned - // should have a UniqueAttributeValue that matches the `query` parameter. - Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) - - // A struct that contains information about the adapter, it is used by the api-server to determine the capabilities of the adapter - // It is mandatory for all adapters to implement this method - Metadata() *sdp.AdapterMetadata -} - -// An adapter that support the List method. This was previously part of the -// Adapter interface however it was split out to allow for the transition to -// streaming responses -type ListableAdapter interface { - Adapter - - // List Lists all items in a given scope - List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) -} - -// ListStreamableAdapter supports streaming for the List queries. -type ListStreamableAdapter interface { - Adapter - ListStream(ctx context.Context, scope string, ignoreCache bool, stream QueryResultStream) -} - -// SearchStreamableAdapter supports streaming for the Search queries. -type SearchStreamableAdapter interface { - Adapter - SearchStream(ctx context.Context, scope string, query string, ignoreCache bool, stream QueryResultStream) -} - -// CachingAdapter Is an adapter of items that supports caching -type CachingAdapter interface { - Adapter - Cache() sdpcache.Cache -} - -// SearchableAdapter Is an adapter of items that supports searching -type SearchableAdapter interface { - Adapter - // Search executes a specific search and returns zero or many items as a - // result (and optionally an error). The specific format of the query that - // needs to be provided to Search is dependant on the adapter itself as each - // adapter will respond to searches differently - Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) -} - -// HiddenAdapter adapters that define a `Hidden()` method are able to tell whether -// or not the items they produce should be marked as hidden within the metadata. -// Hidden items will not be shown in GUIs or stored in databases and are used -// for gathering data as part of other processes such as remotely executed -// secondary adapters -type HiddenAdapter interface { - Hidden() bool -} - -// WildcardScopeAdapter is an optional interface that adapters can implement -// to declare they can handle "*" wildcard scopes efficiently for LIST queries -// (e.g., using GCP's aggregatedList API). When an adapter implements this -// interface and returns true from SupportsWildcardScope(), the engine will -// pass wildcard scopes directly to the adapter instead of expanding them to -// all configured scopes—but only for LIST queries. -// -// For GET and SEARCH, the engine always expands wildcard scope so that -// multiple results can be returned when a resource exists in multiple scopes. -// Future work may extend this optimization to SEARCH once adapters support it. -type WildcardScopeAdapter interface { - Adapter - SupportsWildcardScope() bool -} - -// QueryResultStream is a stream of items and errors that are returned from a -// query. Adapters should send items to the stream as soon as they are -// discovered using the `SendItem` method and should send any errors that occur -// using the `SendError` method. These errors will be considered non-fatal. If -// the process encounters a fatal error it should return an error to the caller -// rather then sending one on the stream. -// -// Note that this interface does not have a `Close()` method. Clients of this -// interface are specific functions that get passed in an instance implementing -// this interface. The expectation is that those clients do not return until all -// calls into the stream have finished. -type QueryResultStream interface { - // SendItem sends an item to the stream. This method is thread-safe, but the - // ordering vs SendError is only guaranteed for non-overlapping calls. - SendItem(item *sdp.Item) - // SendError sends an Error to the stream. This method is thread-safe, but - // the ordering vs SendItem is only guaranteed for non-overlapping calls. - SendError(err error) -} - -// QueryResultStream is a stream of items and errors that are returned from a -// query. Adapters should send items to the stream as soon as they are -// discovered using the `SendItem` method and should send any errors that occur -// using the `SendError` method. These errors will be considered non-fatal. If -// the process encounters a fatal error it should return an error to the caller -// rather then sending one on the stream -type QueryResultStreamWithHandlers struct { - itemHandler ItemHandler - errHandler ErrHandler -} - -// assert interface implementation -var _ QueryResultStream = (*QueryResultStreamWithHandlers)(nil) - -// ItemHandler is a function that can be used to handle items as they are -// received from a QueryResultStream -type ItemHandler func(item *sdp.Item) - -// ErrHandler is a function that can be used to handle errors as they are -// received from a QueryResultStream -type ErrHandler func(err error) - -// NewQueryResultStream creates a new QueryResultStream that calls the provided -// handlers when items and errors are received. Note that the handlers are -// called asynchronously and need to provide for their own thread safety. -func NewQueryResultStream(itemHandler ItemHandler, errHandler ErrHandler) *QueryResultStreamWithHandlers { - stream := &QueryResultStreamWithHandlers{ - itemHandler: itemHandler, - errHandler: errHandler, - } - - return stream -} - -// SendItem sends an item to the stream -func (qrs *QueryResultStreamWithHandlers) SendItem(item *sdp.Item) { - qrs.itemHandler(item) -} - -// SendError sends an error to the stream -func (qrs *QueryResultStreamWithHandlers) SendError(err error) { - qrs.errHandler(err) -} - -type RecordingQueryResultStream struct { - streamMu sync.Mutex - items []*sdp.Item - errs []error -} - -// assert interface implementation -var _ QueryResultStream = (*RecordingQueryResultStream)(nil) - -func NewRecordingQueryResultStream() *RecordingQueryResultStream { - return &RecordingQueryResultStream{ - items: []*sdp.Item{}, - errs: []error{}, - } -} - -func (r *RecordingQueryResultStream) SendItem(item *sdp.Item) { - r.streamMu.Lock() - defer r.streamMu.Unlock() - r.items = append(r.items, item) -} - -func (r *RecordingQueryResultStream) GetItems() []*sdp.Item { - r.streamMu.Lock() - defer r.streamMu.Unlock() - return slices.Clone(r.items) -} - -func (r *RecordingQueryResultStream) SendError(err error) { - r.streamMu.Lock() - defer r.streamMu.Unlock() - r.errs = append(r.errs, err) -} - -func (r *RecordingQueryResultStream) GetErrors() []error { - r.streamMu.Lock() - defer r.streamMu.Unlock() - return slices.Clone(r.errs) -} diff --git a/discovery/adapter_test.go b/discovery/adapter_test.go deleted file mode 100644 index 34011567..00000000 --- a/discovery/adapter_test.go +++ /dev/null @@ -1,736 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" -) - -func TestEngineAddAdapters(t *testing.T) { - ec := EngineConfig{} - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - adapter := TestAdapter{} - - if err := e.AddAdapters(&adapter); err != nil { - t.Fatalf("Error adding adapter: %v", err) - } - - if x := len(e.sh.Adapters()); x != 1 { - t.Fatalf("Expected 1 adapters, got %v", x) - } -} - -func TestGet(t *testing.T) { - adapter := TestAdapter{ - ReturnName: "orange", - ReturnType: "person", - ReturnScopes: []string{ - "test", - "empty", - }, - cache: sdpcache.NewMemoryCache(), - } - - e := newStartedEngine(t, "TestGet", nil, nil, &adapter) - - t.Run("Basic test", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Query: "three", - Method: sdp.QueryMethod_GET, - }) - if err != nil { - t.Fatal(err) - } - - if x := len(adapter.GetCalls); x != 1 { - t.Fatalf("Expected 1 get call, got %v", x) - } - - firstCall := adapter.GetCalls[0] - - if firstCall[0] != "test" || firstCall[1] != "three" { - t.Fatalf("First get call parameters unexpected: %v", firstCall) - } - }) - - t.Run("not found error", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - items, edges, errs, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "empty", - Query: "three", - Method: sdp.QueryMethod_GET, - }) - if err != nil { - t.Fatal(err) - } - - if len(errs) == 1 { - if errs[0].GetErrorType() != sdp.QueryError_NOTFOUND { - t.Errorf("expected ErrorType to be %v, got %v", sdp.QueryError_NOTFOUND, errs[0].GetErrorType()) - } - if errs[0].GetErrorString() != "no items found" { - t.Errorf("expected ErrorString to be '%v', got '%v'", "no items found", errs[0].GetErrorString()) - } - if errs[0].GetScope() != "empty" { - t.Errorf("expected Scope to be '%v', got '%v'", "empty", errs[0].GetScope()) - } - if errs[0].GetSourceName() != "testAdapter-orange" { - t.Errorf("expected Adapter name to be '%v', got '%v'", "testAdapter-orange", errs[0].GetSourceName()) - } - if errs[0].GetItemType() != "person" { - t.Errorf("expected ItemType to be '%v', got '%v'", "person", errs[0].GetItemType()) - } - if errs[0].GetResponderName() != "TestGet" { - t.Errorf("expected ResponderName to be '%v', got '%v'", "TestGet", errs[0].GetResponderName()) - } - } else { - t.Errorf("expected 1 error, got %v", len(errs)) - } - - if len(items) != 0 { - t.Errorf("expected 0 items, got %v: %v", len(items), items) - } - if len(edges) != 0 { - t.Errorf("expected 0 edges, got %v: %v", len(edges), edges) - } - }) - - t.Run("Test caching", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var list1 []*sdp.Item - var item2 []*sdp.Item - var item3 []*sdp.Item - var err error - - req := sdp.Query{ - Type: "person", - Scope: "test", - Query: "Dylan", - Method: sdp.QueryMethod_GET, - } - - list1, _, _, err = e.executeQuerySync(context.Background(), &req) - if err != nil { - t.Error(err) - } - - time.Sleep(10 * time.Millisecond) - item2, _, _, err = e.executeQuerySync(context.Background(), &req) - if err != nil { - t.Error(err) - } - - if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != item2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { - t.Errorf("Get queries 10ms apart had different timestamps, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue(), item2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue()) - } - - time.Sleep(10 * time.Millisecond) - - item3, _, _, err = e.executeQuerySync(context.Background(), &req) - if err != nil { - t.Error(err) - } - - if item2[0].GetMetadata().GetTimestamp().String() == item3[0].GetMetadata().GetTimestamp().String() { - t.Error("Get queries after purging had the same timestamps, cache not expiring") - } - }) - - t.Run("Test Get() caching errors", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - req := sdp.Query{ - Type: "person", - Scope: "empty", - Query: "query", - Method: sdp.QueryMethod_GET, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), &req) - if err != nil { - t.Fatal(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - _, _, errs, err = e.executeQuerySync(context.Background(), &req) - if err != nil { - t.Fatal(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.GetCalls); l != 1 { - t.Errorf("Expected 1 Get call due to caching og NOTFOUND errors, got %v", l) - } - }) - - t.Run("Hidden items", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - adapter.IsHidden = true - - t.Run("Get", func(t *testing.T) { - item, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Query: "three", - Method: sdp.QueryMethod_GET, - }) - if err != nil { - t.Fatal(err) - } - - if !item[0].GetMetadata().GetHidden() { - t.Fatal("Item was not marked as hidden in metadata") - } - }) - - t.Run("List", func(t *testing.T) { - items, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Method: sdp.QueryMethod_LIST, - }) - if err != nil { - t.Fatal(err) - } - - if !items[0].GetMetadata().GetHidden() { - t.Fatal("Item was not marked as hidden in metadata") - } - }) - - t.Run("Search", func(t *testing.T) { - items, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Query: "three", - Method: sdp.QueryMethod_SEARCH, - }) - if err != nil { - t.Fatal(err) - } - - if !items[0].GetMetadata().GetHidden() { - t.Fatal("Item was not marked as hidden in metadata") - } - }) - }) -} - -func TestList(t *testing.T) { - adapter := TestAdapter{} - adapter.cache = sdpcache.NewMemoryCache() - - e := newStartedEngine(t, "TestList", nil, nil, &adapter) - - _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Method: sdp.QueryMethod_LIST, - }) - if err != nil { - t.Fatal(err) - } - - if x := len(adapter.ListCalls); x != 1 { - t.Fatalf("Expected 1 find call, got %v", x) - } - - firstCall := adapter.ListCalls[0] - - if firstCall[0] != "test" { - t.Fatalf("First find call parameters unexpected: %v", firstCall) - } -} - -func TestSearch(t *testing.T) { - adapter := TestAdapter{} - adapter.cache = sdpcache.NewMemoryCache() - - e := newStartedEngine(t, "TestSearch", nil, nil, &adapter) - - _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ - Type: "person", - Scope: "test", - Query: "query", - Method: sdp.QueryMethod_SEARCH, - }) - if err != nil { - t.Fatal(err) - } - - if x := len(adapter.SearchCalls); x != 1 { - t.Fatalf("Expected 1 Search call, got %v", x) - } - - firstCall := adapter.SearchCalls[0] - - if firstCall[0] != "test" || firstCall[1] != "query" { - t.Fatalf("First Search call parameters unexpected: %v", firstCall) - } -} - -func TestListSearchCaching(t *testing.T) { - adapter := TestAdapter{ - ReturnScopes: []string{ - "test", - "empty", - "error", - }, - cache: sdpcache.NewMemoryCache(), - } - - e := newStartedEngine(t, "TestListSearchCaching", nil, nil, &adapter) - - t.Run("caching with successful list", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var list1 []*sdp.Item - var list2 []*sdp.Item - var list3 []*sdp.Item - var err error - q := sdp.Query{ - Type: "person", - Scope: "test", - Method: sdp.QueryMethod_LIST, - } - - list1, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - time.Sleep(10 * time.Millisecond) - - list2, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Fatal(err) - } - - if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { - t.Errorf("List queries had different generations, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) - } - - time.Sleep(10 * time.Millisecond) - - list3, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Fatal(err) - } - - if list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"] == list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"] { - t.Errorf("List queries after purging had the same generation, caching not working. %v == %v", list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) - } - }) - - t.Run("empty list", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var err error - q := sdp.Query{ - Type: "person", - Scope: "empty", - Method: sdp.QueryMethod_LIST, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Fatal(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - time.Sleep(10 * time.Millisecond) - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Fatal(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.ListCalls); l != 1 { - t.Errorf("Expected only 1 list call, got %v, cache not working: %v", l, adapter.ListCalls) - } - - time.Sleep(200 * time.Millisecond) - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Fatal(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.ListCalls); l != 2 { - t.Errorf("Expected 2 list calls, got %v, cache not clearing: %v", l, adapter.ListCalls) - } - }) - - t.Run("caching with successful search", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var list1 []*sdp.Item - var list2 []*sdp.Item - var list3 []*sdp.Item - var err error - q := sdp.Query{ - Type: "person", - Scope: "test", - Query: "query", - Method: sdp.QueryMethod_SEARCH, - } - - list1, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - time.Sleep(10 * time.Millisecond) - - list2, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { - t.Errorf("List queries had different generations, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) - } - - time.Sleep(200 * time.Millisecond) - - list3, _, _, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() == list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { - t.Errorf("List queries 200ms apart had the same generations, caching not working. %v == %v", list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) - } - }) - - t.Run("empty search", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var err error - q := sdp.Query{ - Type: "person", - Scope: "empty", - Query: "query", - Method: sdp.QueryMethod_SEARCH, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - time.Sleep(10 * time.Millisecond) - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - time.Sleep(200 * time.Millisecond) - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.SearchCalls); l != 2 { - t.Errorf("Expected 2 find calls, got %v, cache not clearing", l) - } - }) - - t.Run("non-caching of OTHER errors", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - q := sdp.Query{ - Type: "person", - Scope: "error", - Query: "query", - Method: sdp.QueryMethod_GET, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.GetCalls); l != 2 { - t.Errorf("Expected 2 get calls, got %v, OTHER errors should not be cached", l) - } - }) - - t.Run("non-caching when ignoreCache is specified", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - q := sdp.Query{ - Type: "person", - Scope: "error", - Query: "query", - Method: sdp.QueryMethod_GET, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - q.Method = sdp.QueryMethod_LIST - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - q.Method = sdp.QueryMethod_SEARCH - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - _, _, errs, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - if len(errs) != 1 { - t.Fatalf("Expected 1 error, got %v", len(errs)) - } - - if l := len(adapter.GetCalls); l != 2 { - t.Errorf("Expected 2 get calls, got %v", l) - } - - if l := len(adapter.ListCalls); l != 2 { - t.Errorf("Expected 2 List calls, got %v", l) - } - - if l := len(adapter.SearchCalls); l != 2 { - t.Errorf("Expected 2 Search calls, got %v", l) - } - }) -} - -func TestSearchGetCaching(t *testing.T) { - // We want to be sure that if an item has been found via a search and - // cached, the cache will be hit if a Get is run for that particular item - - adapter := TestAdapter{ - ReturnScopes: []string{ - "test", - }, - cache: sdpcache.NewMemoryCache(), - } - - e := newStartedEngine(t, "TestSearchGetCaching", nil, nil, &adapter) - - t.Run("caching with successful search", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - var searchResult []*sdp.Item - var searchErrors []*sdp.QueryError - var getResult []*sdp.Item - var getErrors []*sdp.QueryError - var err error - q := sdp.Query{ - Type: "person", - Scope: "test", - Query: "Dylan", - Method: sdp.QueryMethod_SEARCH, - } - - t.Logf("Searching for %v", q.GetQuery()) - searchResult, _, searchErrors, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if len(searchErrors) != 0 { - for _, err := range searchErrors { - t.Error(err) - } - } - - if len(searchResult) == 0 { - t.Fatal("Got no results") - } - - if len(searchResult) > 1 { - t.Fatalf("Got too many results: %v", searchResult) - } - - time.Sleep(10 * time.Millisecond) - - // Do a get query for that same item - q.Method = sdp.QueryMethod_GET - q.Query = searchResult[0].UniqueAttributeValue() - - t.Logf("Getting %v from cache", q.GetQuery()) - getResult, _, getErrors, err = e.executeQuerySync(context.Background(), &q) - if err != nil { - t.Error(err) - } - - if len(getErrors) != 0 { - for _, err := range getErrors { - t.Error(err) - } - } - - if len(getResult) == 0 { - t.Error("No result from GET") - } - - if searchResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != getResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { - t.Errorf("Search and Get queries had different generations, caching not working. %v != %v", searchResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"], getResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) - } - }) -} - -func TestNewQueryResultStream(t *testing.T) { - items := make(chan *sdp.Item, 10) - errs := make(chan error, 10) - - itemHandler := func(item *sdp.Item) { - time.Sleep(10 * time.Millisecond) - items <- item - } - - errHandler := func(err error) { - time.Sleep(10 * time.Millisecond) - errs <- err - } - - stream := NewQueryResultStream(itemHandler, errHandler) - - // Test Initialization - if stream == nil { - t.Fatal("Expected stream to be initialized, got nil") - } - if stream.itemHandler == nil || stream.errHandler == nil { - t.Fatal("Expected handlers to be set") - } - - // Test SendItem - testItem := &sdp.Item{} - stream.SendItem(testItem) - - // Due to the fact that the handlers are executed in a goroutine it - // essentially gives us a buffered channel with a buffer depth of 1 since - // the item can be pulled off the internal items channel immediately then - // wait on the handler in parallel. That's what allows this test to work - // without extra synchronization - if x := <-items; x != testItem { - t.Fatalf("Expected item to be %v, got %v", testItem, x) - } - - // Test SendError - testErr := errors.New("test error") - stream.SendError(testErr) - - if x := <-errs; x.Error() != testErr.Error() { - t.Fatalf("Expected error to be %v, got %v", testErr, x) - } -} diff --git a/discovery/adapterhost.go b/discovery/adapterhost.go deleted file mode 100644 index 9d34eee4..00000000 --- a/discovery/adapterhost.go +++ /dev/null @@ -1,203 +0,0 @@ -package discovery - -import ( - "errors" - "fmt" - "strings" - "sync" - - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" - "google.golang.org/protobuf/proto" -) - -// AdapterHost This struct holds references to all Adapters in a process -// and provides utility functions to work with them. Methods of this -// struct are safe to call concurrently. -type AdapterHost struct { - // Map of types to all adapters for that type - adapters []Adapter - // Index for O(1) duplicate detection: map[type]map[scope]exists - adapterIndex map[string]map[string]bool - mutex sync.RWMutex -} - -func NewAdapterHost() *AdapterHost { - sh := &AdapterHost{ - adapters: make([]Adapter, 0), - adapterIndex: make(map[string]map[string]bool), - } - - return sh -} - -var ErrAdapterAlreadyExists = errors.New("adapter already exists") - -// AddAdapters Adds an adapter to this engine -func (sh *AdapterHost) AddAdapters(adapters ...Adapter) error { - sh.mutex.Lock() - defer sh.mutex.Unlock() - - for _, newAdapter := range adapters { - newType := newAdapter.Type() - newScopes := newAdapter.Scopes() - - // Check for overlapping scopes using O(1) index lookup instead of O(n) scan - if scopeMap, exists := sh.adapterIndex[newType]; exists { - for _, newScope := range newScopes { - if scopeMap[newScope] { - log.Errorf("Error: Adapter with type %s and overlapping scope %s already exists", - newType, newScope) - return fmt.Errorf("adapter with type %s and overlapping scopes already exists", newType) - } - } - } - - // Add to index - if sh.adapterIndex[newType] == nil { - sh.adapterIndex[newType] = make(map[string]bool) - } - for _, scope := range newScopes { - sh.adapterIndex[newType][scope] = true - } - - // Add to adapters list - sh.adapters = append(sh.adapters, newAdapter) - } - - return nil -} - -// Adapters Returns a slice of all known adapters -func (sh *AdapterHost) Adapters() []Adapter { - sh.mutex.RLock() - defer sh.mutex.RUnlock() - - adapters := make([]Adapter, 0) - - adapters = append(adapters, sh.adapters...) - - return adapters -} - -// VisibleAdapters Returns a slice of all known adapters excluding hidden ones -func (sh *AdapterHost) VisibleAdapters() []Adapter { - allAdapters := sh.Adapters() - result := make([]Adapter, 0) - - // Add all adapters unless they are hidden - for _, adapter := range allAdapters { - if hs, ok := adapter.(HiddenAdapter); ok { - if hs.Hidden() { - // If the adapter is hidden, continue without adding it - continue - } - } - - result = append(result, adapter) - } - - return result -} - -// AdapterByType Returns the adapters for a given type -func (sh *AdapterHost) AdaptersByType(typ string) []Adapter { - sh.mutex.RLock() - defer sh.mutex.RUnlock() - - adapters := make([]Adapter, 0) - - for _, adapter := range sh.adapters { - if adapter.Type() == typ { - adapters = append(adapters, adapter) - } - } - - return adapters -} - -// ExpandQuery Expands queries with wildcards to no longer contain wildcards. -// Meaning that if we support 5 types, and a query comes in with a wildcard -// type, this function will expand that query into 5 queries, one for each -// type. -// -// The same goes for scopes, if we have a query with a wildcard scope, and -// a single adapter that supports 5 scopes, we will end up with 5 queries. The -// exception to this is if we have an adapter that supports all scopes -// (implements WildcardScopeAdapter) and the query method is LIST. In that -// case we pass the wildcard scope directly to the adapter. For GET and -// SEARCH, we always expand so multiple results can be returned. -// -// This functions returns a map of queries with the adapters that they should be -// run against -func (sh *AdapterHost) ExpandQuery(q *sdp.Query) map[*sdp.Query]Adapter { - var checkAdapters []Adapter - - if IsWildcard(q.GetType()) { - // If the query has a wildcard type, all non-hidden adapters might try - // to respond - checkAdapters = sh.VisibleAdapters() - } else { - // If the type is specific, pull just adapters for that type - checkAdapters = append(checkAdapters, sh.AdaptersByType(q.GetType())...) - } - - expandedQueries := make(map[*sdp.Query]Adapter) - - for _, adapter := range checkAdapters { - // is the adapter is hidden - isHidden := false - if hs, ok := adapter.(HiddenAdapter); ok { - isHidden = hs.Hidden() - } - - // Check if adapter supports wildcard scopes - supportsWildcard := false - if ws, ok := adapter.(WildcardScopeAdapter); ok { - supportsWildcard = ws.SupportsWildcardScope() - } - - // If query has wildcard scope and adapter supports wildcards, - // create ONE query with wildcard scope (no expansion). - // Only for LIST: GET and SEARCH must expand so we can return - // multiple results when a resource exists in multiple scopes. - if supportsWildcard && IsWildcard(q.GetScope()) && !isHidden && q.GetMethod() == sdp.QueryMethod_LIST { - dest := proto.Clone(q).(*sdp.Query) - dest.Type = adapter.Type() // specialise the query to the adapter type - expandedQueries[dest] = adapter - continue // Skip normal scope expansion loop - } - - for _, adapterScope := range adapter.Scopes() { - // Create a new query if: - // - // * The adapter supports all scopes, or - // * The query scope is a wildcard (and the adapter is not hidden), or - // * The query scope substring matches adapter scope - if IsWildcard(adapterScope) || (IsWildcard(q.GetScope()) && !isHidden) || strings.Contains(adapterScope, q.GetScope()) { - dest := proto.Clone(q).(*sdp.Query) - - dest.Type = adapter.Type() - - // Choose the more specific scope - if IsWildcard(adapterScope) { - dest.Scope = q.GetScope() - } else { - dest.Scope = adapterScope - } - - expandedQueries[dest] = adapter - } - } - } - - return expandedQueries -} - -// ClearAllAdapters Removes all adapters from the engine -func (sh *AdapterHost) ClearAllAdapters() { - sh.mutex.Lock() - sh.adapters = make([]Adapter, 0) - sh.adapterIndex = make(map[string]map[string]bool) - sh.mutex.Unlock() -} diff --git a/discovery/adapterhost_bench_test.go b/discovery/adapterhost_bench_test.go deleted file mode 100644 index fc6b09da..00000000 --- a/discovery/adapterhost_bench_test.go +++ /dev/null @@ -1,822 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - "os" - "runtime" - "runtime/pprof" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" - "github.com/sourcegraph/conc/pool" -) - -// BenchmarkAddAdapters_GCPScenario simulates the real-world GCP organization scenario -// where we have many projects, regions, and zones creating thousands of adapters -func BenchmarkAddAdapters_GCPScenario(b *testing.B) { - scenarios := []struct { - name string - projects int - regions int - zones int - adapterTypes int // Simplified: different adapter types per scope level - }{ - {"Small_5proj", 5, 5, 10, 20}, - {"Medium_23proj", 23, 35, 135, 88}, // Current failing scenario - {"Large_100proj", 100, 35, 135, 88}, // Enterprise scenario - {"VeryLarge_500proj", 500, 35, 135, 88}, // Large enterprise - } - - for _, sc := range scenarios { - b.Run(sc.name, func(b *testing.B) { - b.ResetTimer() - for range b.N { - b.StopTimer() - adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.adapterTypes) - sh := NewAdapterHost() - b.StartTimer() - - start := time.Now() - err := sh.AddAdapters(adapters...) - elapsed := time.Since(start) - - b.StopTimer() - if err != nil { - b.Fatalf("Failed to add adapters: %v", err) - } - - totalAdapters := len(adapters) - b.ReportMetric(float64(totalAdapters), "adapters") - b.ReportMetric(elapsed.Seconds(), "seconds") - b.ReportMetric(float64(totalAdapters)/elapsed.Seconds(), "adapters/sec") - } - }) - } -} - -// BenchmarkAddAdapters_Scaling tests at different scales to demonstrate O(n²) behavior -func BenchmarkAddAdapters_Scaling(b *testing.B) { - sizes := []int{100, 500, 1000, 5000, 10000, 25000} - - for _, size := range sizes { - b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) { - b.ResetTimer() - for range b.N { - b.StopTimer() - adapters := generateSimpleAdapters(size) - sh := NewAdapterHost() - b.StartTimer() - - start := time.Now() - err := sh.AddAdapters(adapters...) - elapsed := time.Since(start) - - b.StopTimer() - if err != nil { - b.Fatalf("Failed to add adapters: %v", err) - } - - b.ReportMetric(elapsed.Seconds(), "seconds") - b.ReportMetric(float64(size)/elapsed.Seconds(), "adapters/sec") - } - }) - } -} - -// BenchmarkAddAdapters_IncrementalAdd simulates adding adapters one project at a time -// This is closer to how it might be used in practice -func BenchmarkAddAdapters_IncrementalAdd(b *testing.B) { - projects := 100 - regionsPerProject := 35 - zonesPerProject := 135 - typesPerScope := 30 - - b.ResetTimer() - for range b.N { - b.StopTimer() - sh := NewAdapterHost() - b.StartTimer() - - start := time.Now() - - // Add adapters project by project (like we do in the real code) - for p := range projects { - projectAdapters := generateProjectAdapters(p, regionsPerProject, zonesPerProject, typesPerScope) - err := sh.AddAdapters(projectAdapters...) - if err != nil { - b.Fatalf("Failed to add adapters for project %d: %v", p, err) - } - } - - elapsed := time.Since(start) - b.StopTimer() - - totalAdapters := len(sh.Adapters()) - b.ReportMetric(float64(totalAdapters), "total_adapters") - b.ReportMetric(elapsed.Seconds(), "seconds") - b.ReportMetric(float64(totalAdapters)/elapsed.Seconds(), "adapters/sec") - } -} - -// generateGCPLikeAdapters creates adapters that mimic the GCP source structure: -// - Project-level adapters (one per project per type) -// - Regional adapters (one per project per region per type) -// - Zonal adapters (one per project per zone per type) -func generateGCPLikeAdapters(projects, regions, zones, typesPerScope int) []Adapter { - projectTypes := typesPerScope / 3 - regionalTypes := typesPerScope / 3 - zonalTypes := typesPerScope / 3 - - totalAdapters := (projects * projectTypes) + - (projects * regions * regionalTypes) + - (projects * zones * zonalTypes) - - adapters := make([]Adapter, 0, totalAdapters) - - for p := range projects { - projectID := fmt.Sprintf("project-%d", p) - - // Project-level adapters - for t := range projectTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{projectID}, - ReturnType: fmt.Sprintf("gcp-project-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), - }) - } - - // Regional adapters - for r := range regions { - scope := fmt.Sprintf("%s.region-%d", projectID, r) - for t := range regionalTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }) - } - } - - // Zonal adapters - for z := range zones { - scope := fmt.Sprintf("%s.zone-%d", projectID, z) - for t := range zonalTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }) - } - } - } - - return adapters -} - -// generateProjectAdapters creates all adapters for a single project -func generateProjectAdapters(projectNum, regions, zones, typesPerScope int) []Adapter { - projectTypes := typesPerScope / 3 - regionalTypes := typesPerScope / 3 - zonalTypes := typesPerScope / 3 - - totalAdapters := projectTypes + (regions * regionalTypes) + (zones * zonalTypes) - adapters := make([]Adapter, 0, totalAdapters) - - projectID := fmt.Sprintf("project-%d", projectNum) - - // Project-level adapters - for t := range projectTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{projectID}, - ReturnType: fmt.Sprintf("gcp-project-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), - }) - } - - // Regional adapters - for r := range regions { - scope := fmt.Sprintf("%s.region-%d", projectID, r) - for t := range regionalTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }) - } - } - - // Zonal adapters - for z := range zones { - scope := fmt.Sprintf("%s.zone-%d", projectID, z) - for t := range zonalTypes { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }) - } - } - - return adapters -} - -// generateSimpleAdapters creates n unique adapters for simple scaling tests -func generateSimpleAdapters(n int) []Adapter { - adapters := make([]Adapter, 0, n) - for i := range n { - adapters = append(adapters, &TestAdapter{ - ReturnScopes: []string{fmt.Sprintf("scope-%d", i)}, - ReturnType: fmt.Sprintf("type-%d", i%100), // Reuse 100 types - ReturnName: fmt.Sprintf("adapter-%d", i), - }) - } - return adapters -} - -// BenchmarkListAdapter is a test adapter that returns 10 items per LIST query -// instead of the default 1 item. This is used for memory benchmarks to simulate -// realistic query execution patterns. -type BenchmarkListAdapter struct { - TestAdapter - itemsPerList int // Number of items to return per LIST query -} - -// List returns exactly 10 items (or itemsPerList if set) for each LIST query -func (b *BenchmarkListAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { - // Use the embedded TestAdapter's List method logic but return multiple items - // We'll call the parent's cache lookup, but then generate multiple items - itemsPerList := b.itemsPerList - if itemsPerList == 0 { - itemsPerList = 10 // Default to 10 items - } - - cacheHit, ck, cachedItems, qErr, done := b.cache.Lookup(ctx, b.Name(), sdp.QueryMethod_LIST, scope, b.Type(), "", ignoreCache) - defer done() - if qErr != nil { - return nil, qErr - } - if cacheHit { - // If we have cached items, return them (they should already be 10 items from previous call) - return cachedItems, nil - } - - // Track the call - b.ListCalls = append(b.ListCalls, []string{scope}) - - switch scope { - case "empty": - err := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no items found", - Scope: scope, - } - b.cache.StoreError(ctx, err, b.DefaultCacheDuration(), ck) - return nil, err - case "error": - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "Error for testing", - Scope: scope, - } - default: - // Generate exactly itemsPerList items - items := make([]*sdp.Item, 0, itemsPerList) - for i := range itemsPerList { - item := b.NewTestItem(scope, fmt.Sprintf("item-%d", i)) - items = append(items, item) - b.cache.StoreItem(ctx, item, b.DefaultCacheDuration(), ck) - } - return items, nil - } -} - -// generateBenchmarkGCPLikeAdapters creates adapters that mimic the GCP source structure -// but use BenchmarkListAdapter which returns 10 items per LIST query -func generateBenchmarkGCPLikeAdapters(projects, regions, zones, typesPerScope int) []Adapter { - projectTypes := typesPerScope / 3 - regionalTypes := typesPerScope / 3 - zonalTypes := typesPerScope / 3 - - totalAdapters := (projects * projectTypes) + - (projects * regions * regionalTypes) + - (projects * zones * zonalTypes) - - adapters := make([]Adapter, 0, totalAdapters) - - for p := range projects { - projectID := fmt.Sprintf("project-%d", p) - - // Project-level adapters - for t := range projectTypes { - adapters = append(adapters, &BenchmarkListAdapter{ - TestAdapter: TestAdapter{ - ReturnScopes: []string{projectID}, - ReturnType: fmt.Sprintf("gcp-project-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), - }, - itemsPerList: 10, - }) - } - - // Regional adapters - for r := range regions { - scope := fmt.Sprintf("%s.region-%d", projectID, r) - for t := range regionalTypes { - adapters = append(adapters, &BenchmarkListAdapter{ - TestAdapter: TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }, - itemsPerList: 10, - }) - } - } - - // Zonal adapters - for z := range zones { - scope := fmt.Sprintf("%s.zone-%d", projectID, z) - for t := range zonalTypes { - adapters = append(adapters, &BenchmarkListAdapter{ - TestAdapter: TestAdapter{ - ReturnScopes: []string{scope}, - ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), - ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), - }, - itemsPerList: 10, - }) - } - } - } - - return adapters -} - -// newBenchmarkEngine creates an Engine for benchmarks without requiring NATS connection -// The execution pools are manually initialized so queries can be executed without Start() -func newBenchmarkEngine(adapters ...Adapter) (*Engine, error) { - ec := &EngineConfig{ - MaxParallelExecutions: 2000, - SourceName: "benchmark-engine", - NATSQueueName: "", - Unauthenticated: true, - // No NATSOptions - we don't need NATS for benchmarks - } - - e, err := NewEngine(ec) - if err != nil { - return nil, fmt.Errorf("error creating engine: %w", err) - } - - // Manually initialize execution pools (normally done in Start()) - // This allows us to use ExecuteQuery without connecting to NATS - e.listExecutionPool = pool.New().WithMaxGoroutines(ec.MaxParallelExecutions) - e.getExecutionPool = pool.New().WithMaxGoroutines(ec.MaxParallelExecutions) - - if err := e.AddAdapters(adapters...); err != nil { - return nil, fmt.Errorf("error adding adapters: %w", err) - } - - return e, nil -} - -// TestAddAdapters_LargeScale is a regular test (not benchmark) that validates -// the system can handle a realistic large-scale scenario -func TestAddAdapters_LargeScale(t *testing.T) { - if testing.Short() { - t.Skip("Skipping large-scale test in short mode") - } - - scenarios := []struct { - name string - projects int - regions int - zones int - types int - timeout time.Duration - }{ - {"23_projects", 23, 35, 135, 88, 30 * time.Second}, - {"100_projects", 100, 35, 135, 88, 5 * time.Minute}, - } - - for _, sc := range scenarios { - t.Run(sc.name, func(t *testing.T) { - adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) - sh := NewAdapterHost() - - t.Logf("Testing with %d adapters", len(adapters)) - - done := make(chan error, 1) - go func() { - done <- sh.AddAdapters(adapters...) - }() - - select { - case err := <-done: - if err != nil { - t.Fatalf("Failed to add adapters: %v", err) - } - t.Logf("Successfully added %d adapters", len(sh.Adapters())) - case <-time.After(sc.timeout): - t.Fatalf("AddAdapters timed out after %v (likely O(n²) issue)", sc.timeout) - } - }) - } -} - -// TestMemoryFootprint_EnterpriseScale measures actual memory usage at enterprise scale -// This provides accurate memory consumption data for capacity planning -func TestMemoryFootprint_EnterpriseScale(t *testing.T) { - if testing.Short() { - t.Skip("Skipping memory footprint test in short mode") - } - - scenarios := []struct { - name string - projects int - regions int - zones int - types int - }{ - {"23_projects", 23, 35, 135, 88}, - {"100_projects", 100, 35, 135, 88}, - {"500_projects", 500, 35, 135, 88}, - } - - for _, sc := range scenarios { - t.Run(sc.name, func(t *testing.T) { - // Force GC and get baseline - runtime.GC() - var m1 runtime.MemStats - runtime.ReadMemStats(&m1) - - // Create adapters - adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) - sh := NewAdapterHost() - err := sh.AddAdapters(adapters...) - if err != nil { - t.Fatal(err) - } - - // Get memory stats immediately (don't GC, we want to see actual usage) - var m2 runtime.MemStats - runtime.ReadMemStats(&m2) - - // Calculate memory used - use TotalAlloc which is monotonically increasing - totalAllocated := m2.TotalAlloc - m1.TotalAlloc - currentHeap := m2.HeapAlloc - memUsedMB := float64(totalAllocated) / (1024 * 1024) - heapUsedMB := float64(currentHeap) / (1024 * 1024) - bytesPerAdapter := float64(totalAllocated) / float64(len(adapters)) - sysMemMB := float64(m2.Sys) / (1024 * 1024) - - // Log detailed stats - t.Logf("=== Memory Footprint Analysis ===") - t.Logf("Adapters created: %d", len(adapters)) - t.Logf("Total allocated: %d bytes (%.2f MB)", totalAllocated, memUsedMB) - t.Logf("Current heap usage: %d bytes (%.2f MB)", currentHeap, heapUsedMB) - t.Logf("Bytes per adapter: %.2f", bytesPerAdapter) - t.Logf("Heap objects: %d", m2.HeapObjects) - t.Logf("System memory (from OS): %.2f MB", sysMemMB) - t.Logf("Number of GC cycles: %d", m2.NumGC-m1.NumGC) - - // Project memory usage for larger scales based on heap usage - if sc.projects == 500 { - mem1000 := (heapUsedMB / 500) * 1000 - mem5000 := (heapUsedMB / 500) * 5000 - t.Logf("\n=== Projected Heap Memory Usage ===") - t.Logf("1,000 projects: ~%.0f MB (~%.1f GB)", mem1000, mem1000/1024) - t.Logf("5,000 projects: ~%.0f MB (~%.1f GB)", mem5000, mem5000/1024) - } - }) - } -} - -// TestMemoryFootprint_WithListQueries measures memory usage when actually executing -// LIST queries against adapters, not just adding them. This simulates real-world -// usage where queries are executed and items are returned and cached. -// -// Memory Profiling: -// -// To generate memory profiles for analysis: -// -// 1. Generate memory profile: -// go test -run TestMemoryFootprint_WithListQueries/35_projects \ -// -memprofile=mem_35_projects.pprof ./discovery/... -// -// 2. Analyze the profile: -// go tool pprof mem_35_projects.pprof -// # Then use: top, list , web, etc. -// -// 3. Or use web UI: -// go tool pprof -http=:8080 mem_35_projects.pprof -// # Then open http://localhost:8080 in browser -// -// For heap profiles at specific points (after adapters, after queries): -// HEAP_PROFILE=heap go test -run TestMemoryFootprint_WithListQueries/35_projects -v ./discovery/... -func TestMemoryFootprint_WithListQueries(t *testing.T) { - if testing.Short() { - t.Skip("Skipping memory footprint test with list queries in short mode") - } - - scenarios := []struct { - name string - projects int - regions int - zones int - types int - timeout time.Duration - }{ - {"35_projects", 35, 35, 135, 88, 5 * time.Minute}, - } - - for _, sc := range scenarios { - t.Run(sc.name, func(t *testing.T) { - // Force GC and get baseline - runtime.GC() - var m1 runtime.MemStats - runtime.ReadMemStats(&m1) - - // Create adapters using BenchmarkListAdapter (returns 10 items per query) - adapters := generateBenchmarkGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) - engine, err := newBenchmarkEngine(adapters...) - if err != nil { - t.Fatalf("Failed to create engine: %v", err) - } - - // Get memory stats after adding adapters (before queries) - var m2 runtime.MemStats - runtime.ReadMemStats(&m2) - - // Write heap profile after adapters if requested - if heapProfile := os.Getenv("HEAP_PROFILE"); heapProfile != "" { - f, err := os.Create(fmt.Sprintf("%s_%s_%d_projects_after_adapters.pprof", heapProfile, sc.name, sc.projects)) - if err == nil { - defer f.Close() - runtime.GC() - if err := pprof.WriteHeapProfile(f); err != nil { - t.Logf("Failed to write heap profile: %v", err) - } else { - t.Logf("Heap profile (after adapters) written to: %s", f.Name()) - } - } - } - - // Execute LIST queries for each unique adapter type - // This will expand to all matching scopes via ExpandQuery - ctx, cancel := context.WithTimeout(context.Background(), sc.timeout) - defer cancel() - - // Collect unique adapter types - typeSet := make(map[string]bool) - for _, adapter := range adapters { - typeSet[adapter.Type()] = true - } - - // Execute LIST queries for each unique adapter type across all scopes - // This will expand to all matching scopes via ExpandQuery - totalItems := 0 - totalErrors := 0 - - // Execute one LIST query per adapter type (will expand to all scopes) - for adapterType := range typeSet { - query := &sdp.Query{ - Type: adapterType, - Scope: "*", // Wildcard to match all scopes - Method: sdp.QueryMethod_LIST, - } - - items, _, errs, err := engine.executeQuerySync(ctx, query) - if err != nil { - t.Logf("Query execution error for type %s: %v", adapterType, err) - } - - totalItems += len(items) - totalErrors += len(errs) - } - - // Get final memory stats after queries - var m3 runtime.MemStats - runtime.ReadMemStats(&m3) - - // Write heap profile if requested via environment variable - if heapProfile := os.Getenv("HEAP_PROFILE"); heapProfile != "" { - f, err := os.Create(fmt.Sprintf("%s_%s_%d_projects.pprof", heapProfile, sc.name, sc.projects)) - if err != nil { - t.Logf("Failed to create heap profile: %v", err) - } else { - defer f.Close() - runtime.GC() // Get accurate picture - if err := pprof.WriteHeapProfile(f); err != nil { - t.Logf("Failed to write heap profile: %v", err) - } else { - t.Logf("Heap profile written to: %s", f.Name()) - } - } - } - - // Calculate memory deltas - allocAfterAdapters := m2.TotalAlloc - m1.TotalAlloc - allocAfterQueries := m3.TotalAlloc - m2.TotalAlloc - totalAllocated := m3.TotalAlloc - m1.TotalAlloc - - heapAfterAdapters := m2.HeapAlloc - heapAfterQueries := m3.HeapAlloc - - // Convert to MB - allocAfterAdaptersMB := float64(allocAfterAdapters) / (1024 * 1024) - allocAfterQueriesMB := float64(allocAfterQueries) / (1024 * 1024) - totalAllocatedMB := float64(totalAllocated) / (1024 * 1024) - heapAfterAdaptersMB := float64(heapAfterAdapters) / (1024 * 1024) - heapAfterQueriesMB := float64(heapAfterQueries) / (1024 * 1024) - - // Calculate per-item and per-adapter metrics - bytesPerAdapter := float64(totalAllocated) / float64(len(adapters)) - bytesPerItem := float64(allocAfterQueries) / float64(totalItems) - bytesPerProject := float64(totalAllocated) / float64(sc.projects) - - // Log detailed stats - t.Logf("=== Memory Footprint Analysis with List Queries ===") - t.Logf("Adapters created: %d", len(adapters)) - t.Logf("Adapter types queried: %d", len(typeSet)) - t.Logf("Total items returned: %d", totalItems) - t.Logf("Total errors: %d", totalErrors) - t.Logf("\n=== Memory After Adding Adapters ===") - t.Logf("Total allocated: %d bytes (%.2f MB)", allocAfterAdapters, allocAfterAdaptersMB) - t.Logf("Heap usage: %d bytes (%.2f MB)", heapAfterAdapters, heapAfterAdaptersMB) - t.Logf("\n=== Memory After Executing Queries ===") - t.Logf("Additional allocated: %d bytes (%.2f MB)", allocAfterQueries, allocAfterQueriesMB) - t.Logf("Heap usage: %d bytes (%.2f MB)", heapAfterQueries, heapAfterQueriesMB) - t.Logf("\n=== Total Memory Usage ===") - t.Logf("Total allocated: %d bytes (%.2f MB)", totalAllocated, totalAllocatedMB) - t.Logf("Bytes per adapter: %.2f", bytesPerAdapter) - t.Logf("Bytes per item returned: %.2f", bytesPerItem) - t.Logf("Bytes per project: %.2f", bytesPerProject) - t.Logf("Heap objects: %d", m3.HeapObjects) - t.Logf("System memory (from OS): %.2f MB", float64(m3.Sys)/(1024*1024)) - t.Logf("Number of GC cycles: %d", m3.NumGC-m1.NumGC) - - // Project memory usage for larger scales - if sc.projects >= 100 { - mem1000 := (heapAfterQueriesMB / float64(sc.projects)) * 1000 - mem5000 := (heapAfterQueriesMB / float64(sc.projects)) * 5000 - t.Logf("\n=== Projected Heap Memory Usage (with queries) ===") - t.Logf("1,000 projects: ~%.0f MB (~%.1f GB)", mem1000, mem1000/1024) - t.Logf("5,000 projects: ~%.0f MB (~%.1f GB)", mem5000, mem5000/1024) - } - }) - } -} - -// BenchmarkMemoryFootprint_WithStats measures memory with runtime.MemStats -func BenchmarkMemoryFootprint_WithStats(b *testing.B) { - scenarios := []struct { - name string - projects int - regions int - zones int - types int - }{ - {"Small_23proj", 23, 35, 135, 88}, - {"Medium_100proj", 100, 35, 135, 88}, - {"Large_500proj", 500, 35, 135, 88}, - } - - for _, sc := range scenarios { - b.Run(sc.name, func(b *testing.B) { - for range b.N { - b.StopTimer() - - // Get baseline memory - runtime.GC() - var m1 runtime.MemStats - runtime.ReadMemStats(&m1) - - b.StartTimer() - - // Create and add adapters - adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) - sh := NewAdapterHost() - err := sh.AddAdapters(adapters...) - - b.StopTimer() - - if err != nil { - b.Fatal(err) - } - - // Measure final memory (no GC to see actual usage) - var m2 runtime.MemStats - runtime.ReadMemStats(&m2) - - totalAllocated := m2.TotalAlloc - m1.TotalAlloc - heapUsed := m2.HeapAlloc - memUsedMB := float64(totalAllocated) / (1024 * 1024) - heapUsedMB := float64(heapUsed) / (1024 * 1024) - - b.ReportMetric(float64(len(adapters)), "adapters") - b.ReportMetric(memUsedMB, "total_alloc_MB") - b.ReportMetric(heapUsedMB, "heap_MB") - b.ReportMetric(float64(totalAllocated)/float64(len(adapters)), "bytes/adapter") - b.ReportMetric(float64(m2.HeapObjects), "heap_objects") - b.ReportMetric(float64(m2.Sys)/(1024*1024), "sys_memory_MB") - } - }) - } -} - -// BenchmarkMemoryFootprint_WithListQueries measures memory usage when executing -// LIST queries against adapters that return 10 items each. This provides realistic -// memory consumption data for capacity planning when queries are actually executed. -func BenchmarkMemoryFootprint_WithListQueries(b *testing.B) { - scenarios := []struct { - name string - projects int - regions int - zones int - types int - }{ - {"Medium_35proj", 35, 35, 135, 88}, - } - - for _, sc := range scenarios { - b.Run(sc.name, func(b *testing.B) { - for range b.N { - b.StopTimer() - - // Get baseline memory - runtime.GC() - var m1 runtime.MemStats - runtime.ReadMemStats(&m1) - - // Create adapters using BenchmarkListAdapter (returns 10 items per query) - adapters := generateBenchmarkGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) - engine, err := newBenchmarkEngine(adapters...) - if err != nil { - b.Fatalf("Failed to create engine: %v", err) - } - - // Get memory after adding adapters - var m2 runtime.MemStats - runtime.ReadMemStats(&m2) - - b.StartTimer() - - // Execute LIST queries for each unique adapter type - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - // Collect unique adapter types - typeSet := make(map[string]bool) - for _, adapter := range adapters { - typeSet[adapter.Type()] = true - } - - totalItems := 0 - for adapterType := range typeSet { - query := &sdp.Query{ - Type: adapterType, - Scope: "*", // Wildcard to match all scopes - Method: sdp.QueryMethod_LIST, - } - - items, _, _, err := engine.executeQuerySync(ctx, query) - if err != nil { - // Log but don't fail - some queries might timeout in benchmarks - b.Logf("Query execution error for type %s: %v", adapterType, err) - } - totalItems += len(items) - } - - b.StopTimer() - - // Measure final memory after queries - var m3 runtime.MemStats - runtime.ReadMemStats(&m3) - - allocAfterAdapters := m2.TotalAlloc - m1.TotalAlloc - allocAfterQueries := m3.TotalAlloc - m2.TotalAlloc - totalAllocated := m3.TotalAlloc - m1.TotalAlloc - heapAfterQueries := m3.HeapAlloc - - allocAfterAdaptersMB := float64(allocAfterAdapters) / (1024 * 1024) - allocAfterQueriesMB := float64(allocAfterQueries) / (1024 * 1024) - totalAllocatedMB := float64(totalAllocated) / (1024 * 1024) - heapAfterQueriesMB := float64(heapAfterQueries) / (1024 * 1024) - - b.ReportMetric(float64(len(adapters)), "adapters") - b.ReportMetric(float64(totalItems), "items_returned") - b.ReportMetric(allocAfterAdaptersMB, "alloc_after_adapters_MB") - b.ReportMetric(allocAfterQueriesMB, "alloc_after_queries_MB") - b.ReportMetric(totalAllocatedMB, "total_alloc_MB") - b.ReportMetric(heapAfterQueriesMB, "heap_MB") - b.ReportMetric(float64(totalAllocated)/float64(len(adapters)), "bytes/adapter") - b.ReportMetric(float64(allocAfterQueries)/float64(totalItems), "bytes/item") - b.ReportMetric(float64(m3.HeapObjects), "heap_objects") - b.ReportMetric(float64(m3.Sys)/(1024*1024), "sys_memory_MB") - } - }) - } -} diff --git a/discovery/adapterhost_test.go b/discovery/adapterhost_test.go deleted file mode 100644 index c706954f..00000000 --- a/discovery/adapterhost_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package discovery - -import ( - "testing" - - "github.com/overmindtech/cli/sdp-go" -) - -func TestAdapterHostExpandQuery(t *testing.T) { - sh := NewAdapterHost() - - err := sh.AddAdapters( - &TestAdapter{ - ReturnScopes: []string{"test"}, - ReturnType: "person", - ReturnName: "person", - }, - &TestAdapter{ - ReturnScopes: []string{"test"}, - ReturnType: "fish", - ReturnName: "fish", - }, - &TestAdapter{ - ReturnScopes: []string{ - "multiA", - "multiB", - }, - ReturnType: "chair", - ReturnName: "chair", - }, - &TestAdapter{ - ReturnScopes: []string{"test"}, - ReturnType: "hidden_person", - IsHidden: true, - ReturnName: "hidden_person", - }, - ) - if err != nil { - t.Fatal(err) - } - - t.Run("Right type wrong scope", func(t *testing.T) { - req := sdp.Query{ - Type: "person", - Scope: "wrong", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 0 { - t.Fatalf("Expected 0 queries, got %v", len(m)) - } - }) - - t.Run("Right scope wrong type", func(t *testing.T) { - req := sdp.Query{ - Type: "wrong", - Scope: "test", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 0 { - t.Fatalf("Expected 0 queries, got %v", len(m)) - } - }) - - t.Run("Right both", func(t *testing.T) { - req := sdp.Query{ - Type: "person", - Scope: "test", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 1 { - t.Fatalf("Expected 1 query, got %v", len(m)) - } - }) - - t.Run("Multi-scope", func(t *testing.T) { - req := sdp.Query{ - Type: "chair", - Scope: "multiB", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 1 { - t.Fatalf("Expected 1 query, got %v", len(m)) - } - }) - - t.Run("Wildcard scope", func(t *testing.T) { - req := sdp.Query{ - Type: "person", - Scope: sdp.WILDCARD, - } - - m := sh.ExpandQuery(&req) - - if len(m) != 1 { - t.Fatalf("Expected 1 query, got %v", len(m)) - } - - req = sdp.Query{ - Type: "chair", - Scope: sdp.WILDCARD, - } - - m = sh.ExpandQuery(&req) - - if len(m) != 2 { - t.Fatalf("Expected 2 queries, got %v", len(m)) - } - }) - - t.Run("Wildcard type", func(t *testing.T) { - req := sdp.Query{ - Type: sdp.WILDCARD, - Scope: "test", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 2 { - t.Fatalf("Expected 2 adapters, got %v", len(m)) - } - }) - - t.Run("Wildcard both", func(t *testing.T) { - req := sdp.Query{ - Type: sdp.WILDCARD, - Scope: sdp.WILDCARD, - } - - m := sh.ExpandQuery(&req) - - if len(m) != 4 { - t.Fatalf("Expected 4 adapters, got %v", len(m)) - } - }) - - t.Run("substring match", func(t *testing.T) { - req := sdp.Query{ - Type: sdp.WILDCARD, - Scope: "multi", - } - - m := sh.ExpandQuery(&req) - - if len(m) != 2 { - t.Fatalf("Expected 2 queries, got %v", len(m)) - } - }) - - t.Run("Listing hidden adapter with wildcard scope", func(t *testing.T) { - req := sdp.Query{ - Type: "hidden_person", - Scope: sdp.WILDCARD, - } - if x := len(sh.ExpandQuery(&req)); x != 0 { - t.Errorf("expected to find 0 adapters, found %v", x) - } - - req = sdp.Query{ - Type: "hidden_person", - Scope: "test", - } - if x := len(sh.ExpandQuery(&req)); x != 1 { - t.Errorf("expected to find 1 adapter, found %v", x) - } - }) -} - -func TestAdapterHostAddAdapters(t *testing.T) { - sh := NewAdapterHost() - - adapter := TestAdapter{} - - err := sh.AddAdapters(&adapter) - if err != nil { - t.Fatal(err) - } - - if x := len(sh.Adapters()); x != 1 { - t.Fatalf("Expected 1 adapters, got %v", x) - } -} - -func TestAdapterHostExpandQuery_WildcardScope(t *testing.T) { - sh := NewAdapterHost() - - // Add regular adapter without wildcard support - regularAdapter := &TestAdapter{ - ReturnScopes: []string{"project.zone-a", "project.zone-b"}, - ReturnType: "regular-type", - ReturnName: "regular", - } - - // Add wildcard-supporting adapter - wildcardAdapter := &TestWildcardAdapter{ - TestAdapter: TestAdapter{ - ReturnScopes: []string{"project.zone-a", "project.zone-b"}, - ReturnType: "wildcard-type", - ReturnName: "wildcard", - }, - supportsWildcard: true, - } - - err := sh.AddAdapters(regularAdapter, wildcardAdapter) - if err != nil { - t.Fatal(err) - } - - t.Run("Regular adapter with wildcard scope expands to all scopes", func(t *testing.T) { - req := sdp.Query{ - Type: "regular-type", - Scope: sdp.WILDCARD, - } - - expanded := sh.ExpandQuery(&req) - - // Should expand to 2 queries (one per zone) - if len(expanded) != 2 { - t.Fatalf("Expected 2 expanded queries for regular adapter, got %v", len(expanded)) - } - - // Check that scopes are specific, not wildcard - for q := range expanded { - if q.GetScope() == sdp.WILDCARD { - t.Errorf("Expected specific scope, got wildcard") - } - } - }) - - t.Run("Wildcard-supporting adapter with wildcard scope does not expand for LIST", func(t *testing.T) { - req := sdp.Query{ - Type: "wildcard-type", - Method: sdp.QueryMethod_LIST, - Scope: sdp.WILDCARD, - } - - expanded := sh.ExpandQuery(&req) - - // Should NOT expand - just 1 query with wildcard scope - if len(expanded) != 1 { - t.Fatalf("Expected 1 query for wildcard adapter, got %v", len(expanded)) - } - - // Check that scope is still wildcard - for q := range expanded { - if q.GetScope() != sdp.WILDCARD { - t.Errorf("Expected wildcard scope to be preserved, got %v", q.GetScope()) - } - } - }) - - t.Run("Wildcard-supporting adapter with wildcard scope expands for GET", func(t *testing.T) { - req := sdp.Query{ - Type: "wildcard-type", - Method: sdp.QueryMethod_GET, - Scope: sdp.WILDCARD, - } - - expanded := sh.ExpandQuery(&req) - - // Should expand to 2 queries (one per scope) for GET - if len(expanded) != 2 { - t.Fatalf("Expected 2 expanded queries for wildcard adapter with GET, got %v", len(expanded)) - } - - // Check that scopes are specific, not wildcard - for q := range expanded { - if q.GetScope() == sdp.WILDCARD { - t.Errorf("Expected specific scope for GET, got wildcard") - } - } - }) - - t.Run("Wildcard-supporting adapter with wildcard scope expands for SEARCH", func(t *testing.T) { - req := sdp.Query{ - Type: "wildcard-type", - Method: sdp.QueryMethod_SEARCH, - Scope: sdp.WILDCARD, - } - - expanded := sh.ExpandQuery(&req) - - // Should expand to 2 queries (one per scope) for SEARCH - if len(expanded) != 2 { - t.Fatalf("Expected 2 expanded queries for wildcard adapter with SEARCH, got %v", len(expanded)) - } - - // Check that scopes are specific, not wildcard - for q := range expanded { - if q.GetScope() == sdp.WILDCARD { - t.Errorf("Expected specific scope for SEARCH, got wildcard") - } - } - }) - - t.Run("Wildcard-supporting adapter with specific scope works normally", func(t *testing.T) { - req := sdp.Query{ - Type: "wildcard-type", - Scope: "project.zone-a", - } - - expanded := sh.ExpandQuery(&req) - - // Should return 1 query with specific scope - if len(expanded) != 1 { - t.Fatalf("Expected 1 query, got %v", len(expanded)) - } - - for q := range expanded { - if q.GetScope() != "project.zone-a" { - t.Errorf("Expected scope 'project.zone-a', got %v", q.GetScope()) - } - } - }) - - t.Run("Hidden wildcard adapter with wildcard scope is not included", func(t *testing.T) { - hiddenWildcardAdapter := &TestWildcardAdapter{ - TestAdapter: TestAdapter{ - ReturnScopes: []string{"project.zone-a"}, - ReturnType: "hidden-wildcard-type", - ReturnName: "hidden-wildcard", - IsHidden: true, - }, - supportsWildcard: true, - } - - err := sh.AddAdapters(hiddenWildcardAdapter) - if err != nil { - t.Fatal(err) - } - - req := sdp.Query{ - Type: "hidden-wildcard-type", - Scope: sdp.WILDCARD, - } - - expanded := sh.ExpandQuery(&req) - - // Hidden adapters should not be expanded for wildcard scopes - if len(expanded) != 0 { - t.Fatalf("Expected 0 queries for hidden wildcard adapter, got %v", len(expanded)) - } - }) -} - -// TestWildcardAdapter extends TestAdapter to implement WildcardScopeAdapter -type TestWildcardAdapter struct { - TestAdapter - supportsWildcard bool -} - -// SupportsWildcardScope implements the WildcardScopeAdapter interface -func (t *TestWildcardAdapter) SupportsWildcardScope() bool { - return t.supportsWildcard -} diff --git a/discovery/cmd.go b/discovery/cmd.go deleted file mode 100644 index 27b2f863..00000000 --- a/discovery/cmd.go +++ /dev/null @@ -1,326 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "fmt" - "net" - "net/http" - "os" - "runtime" - "time" - - "github.com/getsentry/sentry-go" - "github.com/google/uuid" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdp-go/sdpconnect" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "golang.org/x/oauth2" -) - -const defaultApp = "https://app.overmind.tech" - -func AddEngineFlags(command *cobra.Command) { - command.PersistentFlags().String("source-name", "", "The name of the source") - cobra.CheckErr(viper.BindEnv("source-name", "SOURCE_NAME")) - command.PersistentFlags().String("source-uuid", "", "The UUID of the source, is this is blank it will be auto-generated. This is used in heartbeats and shouldn't be supplied usually") - cobra.CheckErr(viper.BindEnv("source-uuid", "SOURCE_UUID")) - command.PersistentFlags().String("source-access-token", "", "The access token to use to authenticate the source for managed sources") - cobra.CheckErr(viper.BindEnv("source-access-token", "SOURCE_ACCESS_TOKEN")) - command.PersistentFlags().String("source-access-token-type", "", "The type of token to use to authenticate the source for managed sources") - cobra.CheckErr(viper.BindEnv("source-access-token-type", "SOURCE_ACCESS_TOKEN_TYPE")) - - command.PersistentFlags().String("api-server-service-host", "", "The host of the API server service, only if the source is managed by Overmind") - cobra.CheckErr(viper.BindEnv("api-server-service-host", "API_SERVER_SERVICE_HOST")) - command.PersistentFlags().String("api-server-service-port", "", "The port of the API server service, only if the source is managed by Overmind") - cobra.CheckErr(viper.BindEnv("api-server-service-port", "API_SERVER_SERVICE_PORT")) - command.PersistentFlags().String("nats-service-host", "", "The host of the NATS service, only if the source is managed by Overmind") - cobra.CheckErr(viper.BindEnv("nats-service-host", "NATS_SERVICE_HOST")) - command.PersistentFlags().String("nats-service-port", "", "The port of the NATS service, only if the source is managed by Overmind") - cobra.CheckErr(viper.BindEnv("nats-service-port", "NATS_SERVICE_PORT")) - - command.PersistentFlags().Bool("overmind-managed-source", false, "If you are running the source yourself or if it is managed by Overmind") - cobra.CheckErr(command.PersistentFlags().MarkHidden("overmind-managed-source")) - cobra.CheckErr(viper.BindEnv("overmind-managed-source", "OVERMIND_MANAGED_SOURCE")) - - command.PersistentFlags().String("app", defaultApp, "The URL of the Overmind app to use") - cobra.CheckErr(viper.BindEnv("app", "APP")) - command.PersistentFlags().String("api-key", "", "The API key to use to authenticate to the Overmind API") - cobra.CheckErr(viper.BindEnv("api-key", "OVM_API_KEY", "API_KEY")) - - command.PersistentFlags().String("nats-connection-name", "", "The name that the source should use to connect to NATS") - cobra.CheckErr(viper.BindEnv("nats-connection-name", "NATS_CONNECTION_NAME")) - command.PersistentFlags().Int("nats-connection-timeout", 10, "The timeout for connecting to NATS") - cobra.CheckErr(viper.BindEnv("nats-connection-timeout", "NATS_CONNECTION_TIMEOUT")) - - command.PersistentFlags().Int("max-parallel", 0, "The maximum number of parallel executions") - cobra.CheckErr(viper.BindEnv("max-parallel", "MAX_PARALLEL")) -} - -func EngineConfigFromViper(engineType, version string) (*EngineConfig, error) { - var sourceName string - hostname, err := os.Hostname() - if err != nil { - return nil, fmt.Errorf("error getting hostname: %w", err) - } - - if viper.GetString("source-name") == "" { - sourceName = fmt.Sprintf("%s-%s", engineType, hostname) - } else { - sourceName = viper.GetString("source-name") - } - - sourceUUIDString := viper.GetString("source-uuid") - var sourceUUID uuid.UUID - if sourceUUIDString == "" { - sourceUUID = uuid.New() - } else { - var err error - sourceUUID, err = uuid.Parse(sourceUUIDString) - if err != nil { - return nil, fmt.Errorf("error parsing source-uuid: %w", err) - } - } - - var managedSource sdp.SourceManaged - if viper.GetBool("overmind-managed-source") { - managedSource = sdp.SourceManaged_MANAGED - } else { - managedSource = sdp.SourceManaged_LOCAL - } - - var apiServerURL string - var natsServerURL string - appURL := viper.GetString("app") - if managedSource == sdp.SourceManaged_MANAGED { - apiServerHost := viper.GetString("api-server-service-host") - apiServerPort := viper.GetString("api-server-service-port") - if apiServerHost == "" || apiServerPort == "" { - return nil, errors.New("API_SERVER_SERVICE_HOST and API_SERVER_SERVICE_PORT (provided by k8s) must be set for managed sources") - } - apiServerURL = net.JoinHostPort(apiServerHost, apiServerPort) - if apiServerPort == "443" { - apiServerURL = "https://" + apiServerURL - } else { - apiServerURL = "http://" + apiServerURL - } - - natsServerHost := viper.GetString("nats-service-host") - natsServerPort := viper.GetString("nats-service-port") - if natsServerHost == "" || natsServerPort == "" { - return nil, errors.New("NATS_SERVICE_HOST and NATS_SERVICE_PORT (provided by k8s) must be set for managed sources") - } - natsServerURL = net.JoinHostPort(natsServerHost, natsServerPort) - // default to websocket if the port is 443; this is to allow GCP sources - // to connect to NATS from outside the EKS cluster - if natsServerPort == "443" { - natsServerURL = "wss://" + natsServerURL - } else { - natsServerURL = "nats://" + natsServerURL - } - } else { - // look up the api server url from the app url - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - oi, err := sdp.NewOvermindInstance(ctx, appURL) - if err != nil { - err = fmt.Errorf("Could not determine Overmind instance URLs from app URL %s: %w", appURL, err) - return nil, err - } - apiServerURL = oi.ApiUrl.String() - natsServerURL = oi.NatsUrl.String() - } - - // setup natsOptions - var natsConnectionName string - if viper.GetString("nats-connection-name") == "" { - natsConnectionName = hostname - } - natsOptions := auth.NATSOptions{ - NumRetries: -1, - RetryDelay: 5 * time.Second, - Servers: []string{natsServerURL}, - ConnectionName: natsConnectionName, - ConnectionTimeout: time.Duration(viper.GetInt("nats-connection-timeout")) * time.Second, - MaxReconnects: -1, - ReconnectWait: 1 * time.Second, - ReconnectJitter: 1 * time.Second, - } - - allow := os.Getenv("ALLOW_UNAUTHENTICATED") - allowUnauthenticated := allow == "true" - - // order of precedence is: - // unauthenticated overrides everything # used for local development - // if managed source, we expect a token - // if local source, we expect an api key - - if allowUnauthenticated { - log.Warn("Using unauthenticated mode as ALLOW_UNAUTHENTICATED is set") - } else { - if viper.GetBool("overmind-managed-source") { - log.Info("Running source in managed mode") - // If managed source, we expect a token - if viper.GetString("source-access-token") == "" { - return nil, errors.New("source-access-token must be set for managed sources") - } - } else if viper.GetString("api-key") == "" { - return nil, errors.New("api-key must be set for local sources") - } - } - - maxParallelExecutions := viper.GetInt("max-parallel") - if maxParallelExecutions == 0 { - maxParallelExecutions = runtime.NumCPU() * 100 // we expect most source interactions to be waiting on external services, so adding more parallelism can help - } - - return &EngineConfig{ - EngineType: engineType, - Version: version, - SourceName: sourceName, - SourceUUID: sourceUUID, - OvermindManagedSource: managedSource, - SourceAccessToken: viper.GetString("source-access-token"), - SourceAccessTokenType: viper.GetString("source-access-token-type"), - App: appURL, - APIServerURL: apiServerURL, - ApiKey: viper.GetString("api-key"), - NATSOptions: &natsOptions, - Unauthenticated: allowUnauthenticated, - MaxParallelExecutions: maxParallelExecutions, - }, nil -} - -// MapFromEngineConfig Returns the config as a map -func MapFromEngineConfig(ec *EngineConfig) map[string]any { - var apiKeyClientSecret string - if ec.ApiKey != "" { - apiKeyClientSecret = "[REDACTED]" - } - var sourceAccessToken string - if ec.SourceAccessToken != "" { - sourceAccessToken = "[REDACTED]" - } - - return map[string]interface{}{ - "engine-type": ec.EngineType, - "version": ec.Version, - "source-name": ec.SourceName, - "source-uuid": ec.SourceUUID, - "source-access-token": sourceAccessToken, - "source-access-token-type": ec.SourceAccessTokenType, - "managed-source": ec.OvermindManagedSource, - "app": ec.App, - "api-key": apiKeyClientSecret, - "api-server-url": ec.APIServerURL, - "max-parallel-executions": ec.MaxParallelExecutions, - "nats-servers": ec.NATSOptions.Servers, - "nats-connection-name": ec.NATSOptions.ConnectionName, - "nats-connection-timeout": ec.NATSConnectionTimeout, - "nats-queue-name": ec.NATSQueueName, - "unauthenticated": ec.Unauthenticated, - } -} - -// CreateClients sets up NATS TokenClient and HeartbeatOptions.ManagementClient from config. -// Each client is only created if not already set (idempotent), so callers like the CLI -// can pre-configure clients without them being overwritten. -func (ec *EngineConfig) CreateClients() error { - // If we are running in unauthenticated mode then do nothing here - if ec.Unauthenticated { - log.Warn("Using unauthenticated NATS as ALLOW_UNAUTHENTICATED is set") - if ec.NATSOptions != nil { - log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") - } - return nil - } - - // If both clients are already configured (e.g. CLI), skip entirely - if ec.NATSOptions != nil && ec.NATSOptions.TokenClient != nil && - ec.HeartbeatOptions != nil && ec.HeartbeatOptions.ManagementClient != nil { - return nil - } - - switch ec.OvermindManagedSource { - case sdp.SourceManaged_LOCAL: - log.Info("Using API Key for authentication, heartbeats will be sent") - - if ec.NATSOptions != nil && ec.NATSOptions.TokenClient == nil { - tokenClient, err := auth.NewAPIKeyClient(ec.APIServerURL, ec.ApiKey) - if err != nil { - return fmt.Errorf("error creating API key client: %w", err) - } - ec.NATSOptions.TokenClient = tokenClient - } - - if ec.HeartbeatOptions == nil { - ec.HeartbeatOptions = &HeartbeatOptions{} - } - if ec.HeartbeatOptions.ManagementClient == nil { - tokenSource := auth.NewAPIKeyTokenSource(ec.ApiKey, ec.APIServerURL) - transport := oauth2.Transport{ - Source: tokenSource, - Base: http.DefaultTransport, - } - authenticatedClient := http.Client{ - Transport: otelhttp.NewTransport(&transport), - } - ec.HeartbeatOptions.ManagementClient = sdpconnect.NewManagementServiceClient( - &authenticatedClient, - ec.APIServerURL, - ) - ec.HeartbeatOptions.Frequency = time.Second * 30 - } - - if ec.NATSOptions != nil { - log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") - } - return nil - case sdp.SourceManaged_MANAGED: - log.Info("Using static token for authentication, heartbeats will be sent") - - if ec.NATSOptions != nil && ec.NATSOptions.TokenClient == nil { - tokenClient, err := auth.NewStaticTokenClient(ec.APIServerURL, ec.SourceAccessToken, ec.SourceAccessTokenType) - if err != nil { - err = fmt.Errorf("error creating static token client: %w", err) - sentry.CaptureException(err) - return err - } - ec.NATSOptions.TokenClient = tokenClient - } - - if ec.HeartbeatOptions == nil { - ec.HeartbeatOptions = &HeartbeatOptions{} - } - if ec.HeartbeatOptions.ManagementClient == nil { - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: ec.SourceAccessToken, - TokenType: ec.SourceAccessTokenType, - }) - transport := oauth2.Transport{ - Source: tokenSource, - Base: http.DefaultTransport, - } - authenticatedClient := http.Client{ - Transport: otelhttp.NewTransport(&transport), - } - ec.HeartbeatOptions.ManagementClient = sdpconnect.NewManagementServiceClient( - &authenticatedClient, - ec.APIServerURL, - ) - ec.HeartbeatOptions.Frequency = time.Second * 30 - } - - if ec.NATSOptions != nil { - log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") - } - return nil - } - - err := fmt.Errorf("unable to setup authentication. Please check your configuration %v", ec) - return err -} diff --git a/discovery/cmd_test.go b/discovery/cmd_test.go deleted file mode 100644 index 49a433f4..00000000 --- a/discovery/cmd_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package discovery - -import ( - "os" - "runtime" - "testing" - - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// NB we do not call AddEngineFlags so we use command line flags, not environment variables -func TestEngineConfigFromViper(t *testing.T) { - tests := []struct { - name string - setupViper func() - engineType string - version string - expectedSourceName string - expectedSourceUUID uuid.UUID - expectedSourceAccessToken string - expectedSourceAccessTokenType string - expectedManagedSource sdp.SourceManaged - expectedApp string - expectedApiServerURL string - expectedApiKey string - expectedNATSUrl string - expectedMaxParallel int - expectUnauthenticated bool - expectError bool - }{ - { - name: "default values", - setupViper: func() { - viper.Set("app", "https://app.overmind.tech") - viper.Set("api-key", "api-key") - }, - engineType: "test-engine", - version: "1.0", - expectedSourceName: "test-engine-" + getHostname(t), - expectedSourceUUID: uuid.Nil, - expectedSourceAccessToken: "", - expectedSourceAccessTokenType: "", - expectedManagedSource: sdp.SourceManaged_LOCAL, - expectedApp: "https://app.overmind.tech", - expectedApiServerURL: "https://api.app.overmind.tech", - expectedNATSUrl: "wss://messages.app.overmind.tech", - expectedApiKey: "api-key", - expectedMaxParallel: runtime.NumCPU() * 100, - expectError: false, - }, - { - name: "custom values", - setupViper: func() { - viper.Set("source-name", "custom-source") - viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") - viper.Set("app", "https://df.overmind-demo.com/") - viper.Set("api-key", "custom-api-key") - viper.Set("max-parallel", 10) - }, - engineType: "test-engine", - version: "1.0", - expectedSourceName: "custom-source", - expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), - expectedSourceAccessToken: "", - expectedSourceAccessTokenType: "", - expectedManagedSource: sdp.SourceManaged_LOCAL, - expectedApp: "https://df.overmind-demo.com/", - expectedApiServerURL: "https://api.df.overmind-demo.com", - expectedNATSUrl: "wss://messages.df.overmind-demo.com", - expectedApiKey: "custom-api-key", - expectedMaxParallel: 10, - expectError: false, - }, - { - name: "invalid UUID", - setupViper: func() { - viper.Set("source-uuid", "invalid-uuid") - }, - engineType: "test-engine", - version: "1.0", - expectError: true, - }, - { - name: "managed source - nats", - setupViper: func() { - viper.Set("source-name", "custom-source") - viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") - viper.Set("source-access-token", "custom-access-token") - viper.Set("source-access-token-type", "custom-token-type") - viper.Set("overmind-managed-source", true) - viper.Set("max-parallel", 10) - - viper.Set("api-server-service-host", "api.app.overmind.tech") - viper.Set("api-server-service-port", "443") - viper.Set("nats-service-host", "messages.app.overmind.tech") - viper.Set("nats-service-port", "4222") - }, - engineType: "test-engine", - version: "1.0", - expectedSourceName: "custom-source", - expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), - expectedSourceAccessToken: "custom-access-token", - expectedSourceAccessTokenType: "custom-token-type", - expectedManagedSource: sdp.SourceManaged_MANAGED, - - expectedApiServerURL: "https://api.app.overmind.tech:443", - expectedNATSUrl: "nats://messages.app.overmind.tech:4222", - expectedMaxParallel: 10, - expectError: false, - }, - { - name: "managed source - wss", - setupViper: func() { - viper.Set("source-name", "custom-source") - viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") - viper.Set("source-access-token", "custom-access-token") - viper.Set("source-access-token-type", "custom-token-type") - viper.Set("overmind-managed-source", true) - viper.Set("max-parallel", 10) - - viper.Set("api-server-service-host", "api.app.overmind.tech") - viper.Set("api-server-service-port", "443") - viper.Set("nats-service-host", "messages.app.overmind.tech") - viper.Set("nats-service-port", "443") - }, - engineType: "test-engine", - version: "1.0", - expectedSourceName: "custom-source", - expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), - expectedSourceAccessToken: "custom-access-token", - expectedSourceAccessTokenType: "custom-token-type", - expectedManagedSource: sdp.SourceManaged_MANAGED, - - expectedApiServerURL: "https://api.app.overmind.tech:443", - expectedNATSUrl: "wss://messages.app.overmind.tech:443", - expectedMaxParallel: 10, - expectError: false, - }, - { - name: "managed source local insecure", - setupViper: func() { - viper.Set("source-name", "custom-source") - viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") - viper.Set("source-access-token", "custom-access-token") - viper.Set("source-access-token-type", "custom-token-type") - viper.Set("overmind-managed-source", true) - viper.Set("max-parallel", 10) - - viper.Set("api-server-service-host", "localhost") - viper.Set("api-server-service-port", "8080") - viper.Set("nats-service-host", "localhost") - viper.Set("nats-service-port", "4222") - }, - engineType: "test-engine", - version: "1.0", - expectedSourceName: "custom-source", - expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), - expectedSourceAccessToken: "custom-access-token", - expectedSourceAccessTokenType: "custom-token-type", - expectedManagedSource: sdp.SourceManaged_MANAGED, - - expectedApiServerURL: "http://localhost:8080", - expectedNATSUrl: "nats://localhost:4222", - expectedMaxParallel: 10, - expectError: false, - }, - { - name: "source access token and api key not set", - setupViper: func() {}, - engineType: "test-engine", - version: "1.0", - expectError: true, - }, - { - name: "fully unauthenticated", - setupViper: func() { - viper.Set("app", "https://app.overmind.tech") - viper.Set("source-name", "custom-source") - t.Setenv("ALLOW_UNAUTHENTICATED", "true") - }, - engineType: "test-engine", - version: "1.0", - expectError: false, - expectedMaxParallel: runtime.NumCPU() * 100, - expectedSourceName: "custom-source", - expectedApp: "https://app.overmind.tech", - expectedApiServerURL: "https://api.app.overmind.tech", - expectedNATSUrl: "wss://messages.app.overmind.tech", - expectUnauthenticated: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("ALLOW_UNAUTHENTICATED", "") - viper.Reset() - tt.setupViper() - engineConfig, err := EngineConfigFromViper(tt.engineType, tt.version) - if tt.expectError { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.engineType, engineConfig.EngineType) - assert.Equal(t, tt.version, engineConfig.Version) - assert.Equal(t, tt.expectedSourceName, engineConfig.SourceName) - if tt.expectedSourceUUID == uuid.Nil { - assert.NotEqual(t, uuid.Nil, engineConfig.SourceUUID) - } else { - assert.Equal(t, tt.expectedSourceUUID, engineConfig.SourceUUID) - } - assert.Equal(t, tt.expectedSourceAccessToken, engineConfig.SourceAccessToken) - assert.Equal(t, tt.expectedSourceAccessTokenType, engineConfig.SourceAccessTokenType) - assert.Equal(t, tt.expectedManagedSource, engineConfig.OvermindManagedSource) - assert.Equal(t, tt.expectedApp, engineConfig.App) - assert.Equal(t, tt.expectedApiServerURL, engineConfig.APIServerURL) - assert.Equal(t, tt.expectedNATSUrl, engineConfig.NATSOptions.Servers[0]) - assert.Equal(t, tt.expectedApiKey, engineConfig.ApiKey) - assert.Equal(t, tt.expectedMaxParallel, engineConfig.MaxParallelExecutions) - assert.Equal(t, tt.expectUnauthenticated, engineConfig.Unauthenticated) - } - }) - } -} - -func getHostname(t *testing.T) string { - hostname, err := os.Hostname() - if err != nil { - t.Fatalf("error getting hostname: %v", err) - } - return hostname -} diff --git a/discovery/doc.go b/discovery/doc.go deleted file mode 100644 index ab24ca1e..00000000 --- a/discovery/doc.go +++ /dev/null @@ -1,33 +0,0 @@ -// Package discovery provides the engine and protocol types for Overmind sources. -// Sources discover infrastructure (AWS, K8s, GCP, etc.) and respond to queries via NATS. -// -// # Startup sequence for source authors -// -// Sources should follow this canonical flow so that health probes and heartbeats -// work even when adapter initialization fails (avoiding CrashLoopBackOff): -// -// 1. EngineConfigFromViper(engineType, version) — fail: return/exit -// 2. NewEngine(engineConfig) — fail: return/exit (includes CreateClients internally) -// 3. ServeHealthProbes(port) -// 4. Start(ctx) — fail: return/exit (NATS connection required) -// 5. Validate source config — permanent config errors: SetInitError(err), then idle -// 6. Adapter init — use InitialiseAdapters (blocks until success or ctx cancelled) for retryable init, or SetInitError for single-attempt -// 7. Wait for SIGTERM, then Stop() -// -// # Error handling -// -// Fatal errors (caller must return or exit): EngineConfigFromViper, NewEngine, Start. -// The engine cannot function without a valid config, auth clients, or NATS connection. -// -// Recoverable errors (call SetInitError and keep running): source config validation -// failures (e.g. missing credentials, invalid regions) and adapter initialization -// failures that may be transient. The pod stays Running, readiness fails, and the -// error is reported via heartbeats and the API/UI. -// -// Permanent config errors (e.g. invalid API key, missing required flags) should -// be detected before calling InitialiseAdapters and reported via SetInitError — -// do not retry. Transient adapter init errors (e.g. upstream API temporarily -// unavailable) should use InitialiseAdapters, which retries with backoff. -// -// See SetInitError and InitialiseAdapters for details and examples. -package discovery diff --git a/discovery/engine.go b/discovery/engine.go deleted file mode 100644 index b9ea9e4a..00000000 --- a/discovery/engine.go +++ /dev/null @@ -1,898 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "fmt" - "net/http" - "slices" - "strings" - "sync" - "time" - - "connectrpc.com/connect" - "github.com/cenkalti/backoff/v5" - "github.com/getsentry/sentry-go" - "github.com/google/uuid" - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "github.com/sourcegraph/conc/pool" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -const ( - DefaultMaxRequestTimeout = 5 * time.Minute - DefaultConnectionWatchInterval = 3 * time.Second -) - -// The client that will be used to send heartbeats. This will usually be an -// `sdpconnect.ManagementServiceClient` -type HeartbeatClient interface { - SubmitSourceHeartbeat(context.Context, *connect.Request[sdp.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp.SubmitSourceHeartbeatResponse], error) -} - -type HeartbeatOptions struct { - // The client that will be used to send heartbeats - ManagementClient HeartbeatClient - - // ReadinessCheck is called during readiness probes to verify adapters are healthy and ready. - // This should be a lightweight, adapter-only check (do NOT include engine/liveness checks). - // Timeouts are controlled by the caller (e.g., Kubernetes probe timeout / SendHeartbeat). - // See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes - ReadinessCheck func(context.Context) error - - // How frequently to send a heartbeat - Frequency time.Duration -} - -// EngineConfig is the configuration for the engine -// it is used to configure the engine before starting it -type EngineConfig struct { - EngineType string // The type of the engine, e.g. "aws" or "kubernetes" - Version string // The version of the adapter that should be reported in the heartbeat - SourceName string // normally follows the format of "type-hostname", e.g. "stdlib-source" - SourceUUID uuid.UUID // The UUID of the source, is this is blank it will be auto-generated. This is used in heartbeats and shouldn't be supplied usually" - App string // "https://app.overmind.tech", "The URL of the Overmind app to use" - APIServerURL string // The URL of the Overmind API server to uses for the heartbeat, this is calculated - - // The 'ovm_*' API key to use to authenticate to the Overmind API. - // This and 'SourceAccessToken' are mutually exclusive - ApiKey string // The API key to use to authenticate to the Overmind API" - // Static token passed to the source to authenticate. - SourceAccessToken string // The access token to use to authenticate to the source - SourceAccessTokenType string // The type of token to use to authenticate the source for managed sources - - // NATS options - NATSOptions *auth.NATSOptions // Options for connecting to NATS - NATSConnectionTimeout int // The timeout for connecting to NATS - NATSQueueName string // The name of the queue to use when subscribing - Unauthenticated bool // Whether the source is unauthenticated - - // The options for the heartbeat. If this is nil the engine won't send - // it is not used if we are nats only or unauthenticated. this will only happen if we are running in a test environment - HeartbeatOptions *HeartbeatOptions - - // Whether this adapter is managed by Overmind. This is initially used for - // reporting so that you can tell the difference between managed adapters and - // ones you're running locally - OvermindManagedSource sdp.SourceManaged - MaxParallelExecutions int // 2_000, Max number of requests to run in parallel -} - -// Engine is the main discovery engine. This is where all of the Adapters and -// adapters are stored and is responsible for calling out to the right adapters to -// discover everything -// -// Note that an engine that does not have a connected NATS connection will -// simply not communicate over NATS -type Engine struct { - EngineConfig *EngineConfig - // The maximum request timeout. Defaults to `DefaultMaxRequestTimeout` if - // set to zero. If a client does not send a timeout, it will default to this - // value. Requests with timeouts larger than this value will have their - // timeouts overridden - MaxRequestTimeout time.Duration - - // How often to check for closed connections and try to recover - ConnectionWatchInterval time.Duration - connectionWatcher NATSWatcher - - // The configuration for the heartbeat for this engine. If this is nil the - // engine won't send heartbeats when started - - // Internal throttle used to limit MaxParallelExecutions. This reads - // MaxParallelExecutions and is populated when the engine is started. This - // pool is only used for LIST requests. Since GET requests can be blocked by - // LIST requests, they need to be handled in a different pool to avoid - // deadlocking. - listExecutionPool *pool.Pool - - // Internal throttle used to limit MaxParallelExecutions. This reads - // MaxParallelExecutions and is populated when the engine is started. This - // pool is only used for GET and SEARCH requests. Since GET requests can be - // blocked by LIST requests, they need to be handled in a different pool to - // avoid deadlocking. - getExecutionPool *pool.Pool - - // The NATS connection - natsConnection sdp.EncodedConnection - natsConnectionMutex sync.Mutex - - // All Adapters managed by this Engine - sh *AdapterHost - - // handle log requests with this adapter - logAdapter LogAdapter - logAdapterMu sync.RWMutex - - // GetListMutex used for locking out Get queries when there's a List happening - gfm GetListMutex - - // trackedQueries is used for storing queries that have a UUID so they can - // be cancelled if required - trackedQueries map[uuid.UUID]*QueryTracker - trackedQueriesMutex sync.RWMutex - - // Prevents the engine being restarted many times in parallel - restartMutex sync.Mutex - - // Context to background jobs like cache purging and heartbeats. These will - // stop when the context is cancelled - backgroundJobContext context.Context - backgroundJobCancel context.CancelFunc - heartbeatCancel context.CancelFunc - - // Heartbeat status tracking for healthz checks - lastSuccessfulHeartbeat time.Time - lastHeartbeatError error - heartbeatStatusMutex sync.RWMutex - - // initError stores configuration/credential/initialization failures that prevent - // adapters from being added to the engine. This includes: - // - AWS: AssumeRole failures, GetCallerIdentity errors, invalid credentials - // - K8s: Namespace listing failures, kubeconfig errors - // - Harness: API authentication failures, hierarchy discovery errors - // The error is surfaced via readiness checks (pod becomes 0/1 Ready) and - // heartbeats (visible in UI/API), allowing the pod to stay Running instead of - // CrashLoopBackOff so customers can diagnose and fix configuration issues. - initError error - initErrorMutex sync.RWMutex -} - -func NewEngine(engineConfig *EngineConfig) (*Engine, error) { - if err := engineConfig.CreateClients(); err != nil { - return nil, fmt.Errorf("could not create auth clients: %w", err) - } - sh := NewAdapterHost() - return &Engine{ - EngineConfig: engineConfig, - MaxRequestTimeout: DefaultMaxRequestTimeout, - ConnectionWatchInterval: DefaultConnectionWatchInterval, - sh: sh, - trackedQueries: make(map[uuid.UUID]*QueryTracker), - }, nil -} - -// TrackQuery Stores a QueryTracker in the engine so that it can be looked -// up later and cancelled if required. The UUID should be supplied as part of -// the query itself -func (e *Engine) TrackQuery(uuid uuid.UUID, qt *QueryTracker) { - e.trackedQueriesMutex.Lock() - defer e.trackedQueriesMutex.Unlock() - e.trackedQueries[uuid] = qt -} - -// GetTrackedQuery Returns the QueryTracker object for a given UUID. This -// tracker can then be used to cancel the query -func (e *Engine) GetTrackedQuery(uuid uuid.UUID) (*QueryTracker, error) { - e.trackedQueriesMutex.RLock() - defer e.trackedQueriesMutex.RUnlock() - - if qt, ok := e.trackedQueries[uuid]; ok { - return qt, nil - } else { - return nil, fmt.Errorf("tracker with UUID %x not found", uuid) - } -} - -// DeleteTrackedQuery Deletes a query from tracking -func (e *Engine) DeleteTrackedQuery(uuid [16]byte) { - e.trackedQueriesMutex.Lock() - defer e.trackedQueriesMutex.Unlock() - delete(e.trackedQueries, uuid) -} - -// AddAdapters Adds an adapter to this engine -func (e *Engine) AddAdapters(adapters ...Adapter) error { - return e.sh.AddAdapters(adapters...) -} - -// Connect Connects to NATS -func (e *Engine) connect() error { - if e.EngineConfig.NATSOptions != nil { - encodedConnection, err := e.EngineConfig.NATSOptions.Connect() - if err != nil { - return fmt.Errorf("error connecting to NATS '%+v' : %w", e.EngineConfig.NATSOptions.Servers, err) - } - - e.natsConnectionMutex.Lock() - e.natsConnection = encodedConnection - e.natsConnectionMutex.Unlock() - - // TODO: this could be replaced by setting the various callbacks on the - // natsConnection and waiting for notification from the underlying - // connection. - e.connectionWatcher = NATSWatcher{ - Connection: e.natsConnection, - // If the connection stays disconnected for more than 5 minutes, - // force a reconnection attempt. This prevents the source from being - // stuck in RECONNECTING state indefinitely. - ReconnectionTimeout: 5 * time.Minute, - FailureHandler: func() { - go func() { - log.Warn("NATSWatcher triggered failure handler, attempting to reconnect") - e.disconnect() - - if err := e.connect(); err != nil { - log.WithError(err).Error("Error reconnecting during failure handler") - } - }() - }, - } - e.connectionWatcher.Start(e.ConnectionWatchInterval) - - // Wait for the connection to be completed - err = e.natsConnection.Underlying().FlushTimeout(10 * time.Minute) - if err != nil { - return fmt.Errorf("error flushing NATS connection: %w", err) - } - - log.WithFields(log.Fields{ - "ServerID": e.natsConnection.Underlying().ConnectedServerId(), - "URL:": e.natsConnection.Underlying().ConnectedUrl(), - }).Info("NATS connected") - } - - if e.natsConnection == nil { - return errors.New("no NATSOptions struct and no natsConnection provided") - } - - // Since the underlying query processing logic creates its own spans - // when it has some real work to do, we are not passing a name to these - // query handlers so that we don't get spans that are completely empty - err := e.subscribe("request.all", sdp.NewAsyncRawQueryHandler("", func(ctx context.Context, _ *nats.Msg, i *sdp.Query) { - e.HandleQuery(ctx, i) - })) - if err != nil { - return fmt.Errorf("error subscribing to request.all: %w", err) - } - - err = e.subscribe("request.scope.>", sdp.NewAsyncRawQueryHandler("", func(ctx context.Context, m *nats.Msg, i *sdp.Query) { - e.HandleQuery(ctx, i) - })) - if err != nil { - return fmt.Errorf("error subscribing to request.scope.>: %w", err) - } - - err = e.subscribe("cancel.all", sdp.NewAsyncRawCancelQueryHandler("CancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.CancelQuery) { - e.HandleCancelQuery(ctx, i) - })) - if err != nil { - return fmt.Errorf("error subscribing to cancel.all: %w", err) - } - - err = e.subscribe("cancel.scope.>", sdp.NewAsyncRawCancelQueryHandler("WildcardCancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.CancelQuery) { - e.HandleCancelQuery(ctx, i) - })) - if err != nil { - return fmt.Errorf("error subscribing to cancel.scope.>: %w", err) - } - - if e.logAdapter != nil { - for _, scope := range e.logAdapter.Scopes() { - subj := fmt.Sprintf("logs.scope.%v", scope) - err = e.subscribe(subj, sdp.NewAsyncRawNATSGetLogRecordsRequestHandler("WildcardCancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.NATSGetLogRecordsRequest) { - replyTo := m.Header.Get("reply-to") - e.HandleLogRecordsRequest(ctx, replyTo, i) - })) - if err != nil { - return fmt.Errorf("error subscribing to %v: %w", subj, err) - } - } - } - - return nil -} - -// disconnect Disconnects the engine from the NATS network -func (e *Engine) disconnect() { - e.connectionWatcher.Stop() - - e.natsConnectionMutex.Lock() - defer e.natsConnectionMutex.Unlock() - - if e.natsConnection == nil { - return - } - - e.natsConnection.Close() - e.natsConnection.Drop() -} - -// Start performs all of the initialisation steps required for the engine to -// work. Note that this creates NATS subscriptions for all available adapters so -// modifying the Adapters value after an engine has been started will not have -// any effect until the engine is restarted -func (e *Engine) Start(ctx context.Context) error { - e.listExecutionPool = pool.New().WithMaxGoroutines(e.EngineConfig.MaxParallelExecutions) - e.getExecutionPool = pool.New().WithMaxGoroutines(e.EngineConfig.MaxParallelExecutions) - - e.backgroundJobContext, e.backgroundJobCancel = context.WithCancel(ctx) - - // Decide your own UUID if not provided - if e.EngineConfig.SourceUUID == uuid.Nil { - e.EngineConfig.SourceUUID = uuid.New() - } - - err := e.connect() //nolint:contextcheck // context is passed in through backgroundJobContext - if err != nil { - _ = e.SendHeartbeat(e.backgroundJobContext, err) //nolint:contextcheck - return fmt.Errorf("could not connect to NATS: %w", err) - } - - // Start background jobs - e.StartSendingHeartbeats(e.backgroundJobContext) //nolint:contextcheck - return nil -} - -// subscribe Subscribes to a subject using the current NATS connection. -// Remember to use sdp's genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling -func (e *Engine) subscribe(subject string, handler nats.MsgHandler) error { - var err error - - e.natsConnectionMutex.Lock() - defer e.natsConnectionMutex.Unlock() - - if e.natsConnection.Underlying() == nil { - return errors.New("cannot subscribe. NATS connection is nil") - } - - log.WithFields(log.Fields{ - "queueName": e.EngineConfig.NATSQueueName, - "subject": subject, - "engineName": e.EngineConfig.SourceName, - }).Debug("creating NATS subscription") - - if e.EngineConfig.NATSQueueName == "" { - _, err = e.natsConnection.Subscribe(subject, handler) - } else { - _, err = e.natsConnection.QueueSubscribe(subject, e.EngineConfig.NATSQueueName, handler) - } - if err != nil { - return fmt.Errorf("error subscribing to NATS: %w", err) - } - - return nil -} - -// Stop Stops the engine running and disconnects from NATS -func (e *Engine) Stop() error { - e.disconnect() - - // Stop purging and clear the cache - if e.backgroundJobCancel != nil { - e.backgroundJobCancel() - } - if e.heartbeatCancel != nil { - e.heartbeatCancel() - } - return nil -} - -// Restart Restarts the engine. If called in parallel, subsequent calls are -// ignored until the restart is completed -func (e *Engine) Restart(ctx context.Context) error { - e.restartMutex.Lock() - defer e.restartMutex.Unlock() - - err := e.Stop() - if err != nil { - return fmt.Errorf("Restart.Stop: %w", err) - } - - err = e.Start(ctx) - return fmt.Errorf("Restart.Start: %w", err) -} - -// IsNATSConnected returns whether the engine is connected to NATS -func (e *Engine) IsNATSConnected() bool { - e.natsConnectionMutex.Lock() - defer e.natsConnectionMutex.Unlock() - - if e.natsConnection == nil { - return false - } - - if conn := e.natsConnection.Underlying(); conn != nil { - return conn.IsConnected() - } - - return false -} - -// LivenessHealthCheck reports only engine initialization/health (NATS + heartbeat status). -// Kubernetes runs liveness/startup independently from readiness; adapter checks do NOT belong here. -// See: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes -func (e *Engine) LivenessHealthCheck(ctx context.Context) error { - span := trace.SpanFromContext(ctx) - - e.natsConnectionMutex.Lock() - var ( - encodedConn = e.natsConnection - underlying *nats.Conn - ) - if encodedConn != nil { - underlying = encodedConn.Underlying() - } - e.natsConnectionMutex.Unlock() - - natsConnected := underlying != nil && underlying.IsConnected() - - // Read memory stats and add them to the span - memStats := tracing.ReadMemoryStats() - tracing.SetMemoryAttributes(span, "ovm.healthcheck", memStats) - - span.SetAttributes( - attribute.String("ovm.engine.name", e.EngineConfig.SourceName), - attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), - attribute.Bool("ovm.nats.connected", natsConnected), - attribute.Int("ovm.discovery.listExecutionPoolCount", int(listExecutionPoolCount.Load())), - attribute.Int("ovm.discovery.getExecutionPoolCount", int(getExecutionPoolCount.Load())), - ) - - if underlying != nil { - span.SetAttributes( - attribute.String("ovm.nats.serverId", underlying.ConnectedServerId()), - attribute.String("ovm.nats.url", underlying.ConnectedUrl()), - attribute.Int64("ovm.nats.reconnects", int64(underlying.Reconnects)), //nolint:gosec // Reconnects is always a small positive number - ) - } - - if !natsConnected { - return errors.New("NATS connection is not connected") - } - - // Check if heartbeats are failing to submit to api-server - // This fails healthz faster than api-server marks sources as DISCONNECTED, - // allowing seamless pod recycling without customer-visible downtime - if e.EngineConfig.HeartbeatOptions != nil && e.EngineConfig.HeartbeatOptions.Frequency > 0 { - e.heartbeatStatusMutex.RLock() - lastSuccessfulHeartbeat := e.lastSuccessfulHeartbeat - lastHeartbeatError := e.lastHeartbeatError - e.heartbeatStatusMutex.RUnlock() - - // Only check if we've had at least one successful heartbeat - // This allows initial startup grace period - if !lastSuccessfulHeartbeat.IsZero() { - // Healthz fails at 2.0x frequency, api-server marks DISCONNECTED at 2.5x - // This 0.5x buffer allows time for pod recycling - healthzFailureThreshold := lastSuccessfulHeartbeat.Add(time.Duration(float64(e.EngineConfig.HeartbeatOptions.Frequency) * 2.0)) - now := time.Now() - - if now.After(healthzFailureThreshold) && lastHeartbeatError != nil { - return fmt.Errorf("heartbeat submission to api-server has been failing: %w (last successful heartbeat: %v, threshold: %v)", lastHeartbeatError, lastSuccessfulHeartbeat, healthzFailureThreshold) - } - } - } - - return nil -} - -// ReadinessHealthCheck reports whether adapters are ready to serve requests. -// It must not call LivenessHealthCheck; readiness should reflect adapter health only. -// See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes -func (e *Engine) ReadinessHealthCheck(ctx context.Context) error { - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.String("ovm.healthcheck.type", "readiness"), - ) - - // Check for persistent initialization errors first - if initErr := e.GetInitError(); initErr != nil { - return fmt.Errorf("source initialization failed: %w", initErr) - } - - // Check adapter-specific health using the ReadinessCheck function - if e.EngineConfig.HeartbeatOptions != nil && e.EngineConfig.HeartbeatOptions.ReadinessCheck != nil { - if err := e.EngineConfig.HeartbeatOptions.ReadinessCheck(ctx); err != nil { - return err - } - } - - return nil -} - -// HandleCancelQuery Takes a CancelQuery and cancels that query if it exists -func (e *Engine) HandleCancelQuery(ctx context.Context, cancelQuery *sdp.CancelQuery) { - span := trace.SpanFromContext(ctx) - span.SetName("HandleCancelQuery") - span.SetAttributes( - attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), - ) - - u, err := uuid.FromBytes(cancelQuery.GetUUID()) - if err != nil { - log.Errorf("Error parsing UUID for cancel query: %v", err) - return - } - - rt, err := e.GetTrackedQuery(u) - if err != nil { - log.WithFields(log.Fields{ - "UUID": u.String(), - }).Debug("Received cancel query for unknown UUID") - return - } - - if rt != nil && rt.Cancel != nil { - log.WithFields(log.Fields{ - "UUID": u.String(), - }).Debug("Cancelling query") - rt.Cancel() - } -} - -func (e *Engine) HandleLogRecordsRequest(ctx context.Context, replyTo string, request *sdp.NATSGetLogRecordsRequest) { - span := trace.SpanFromContext(ctx) - span.SetName("HandleLogRecordsRequest") - span.SetAttributes( - attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), - ) - - if !strings.HasPrefix(replyTo, "logs.records.") { - sentry.CaptureException(fmt.Errorf("received log records request with invalid reply-to header: %s", replyTo)) - return - } - - err := e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ - Content: &sdp.NATSGetLogRecordsResponse_Status{ - Status: &sdp.NATSGetLogRecordsResponseStatus{ - Status: sdp.NATSGetLogRecordsResponseStatus_STARTED, - }, - }, - }) - if err != nil { - sentry.CaptureException(fmt.Errorf("error publishing log records STARTED response: %w", err)) - return - } - - // ensure that we send an error response if the HandleLogRecordsRequestWithErrors call panics - defer func() { - if r := recover(); r != nil { - sentry.CaptureException(fmt.Errorf("panic in log records request handler: %v", r)) - err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ - Content: &sdp.NATSGetLogRecordsResponse_Status{ - Status: &sdp.NATSGetLogRecordsResponseStatus{ - Status: sdp.NATSGetLogRecordsResponseStatus_ERRORED, - Error: sdp.NewLocalSourceError(connect.CodeInternal, "panic in log records request handler"), - }, - }, - }) - if err != nil { - sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) - return - } - } - }() - - srcErr := e.HandleLogRecordsRequestWithErrors(ctx, replyTo, request) - if srcErr != nil { - err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ - Content: &sdp.NATSGetLogRecordsResponse_Status{ - Status: &sdp.NATSGetLogRecordsResponseStatus{ - Status: sdp.NATSGetLogRecordsResponseStatus_ERRORED, - Error: srcErr, - }, - }, - }) - if err != nil { - sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) - return - } - return - } - - err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ - Content: &sdp.NATSGetLogRecordsResponse_Status{ - Status: &sdp.NATSGetLogRecordsResponseStatus{ - Status: sdp.NATSGetLogRecordsResponseStatus_FINISHED, - }, - }, - }) - if err != nil { - sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) - return - } -} - -func (e *Engine) HandleLogRecordsRequestWithErrors(ctx context.Context, replyTo string, natsRequest *sdp.NATSGetLogRecordsRequest) *sdp.SourceError { - if e.logAdapter == nil { - return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "no logs adapter registered") - } - - if natsRequest == nil { - return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "received nil log records request") - } - - req := natsRequest.GetRequest() - if req == nil { - return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "received nil log records request body") - } - - err := req.Validate() - if err != nil { - return sdp.NewLocalSourceError(connect.CodeInvalidArgument, fmt.Sprintf("invalid log records request: %v", err)) - } - - if !slices.Contains(e.logAdapter.Scopes(), req.GetScope()) { - return sdp.NewLocalSourceError(connect.CodeInvalidArgument, fmt.Sprintf("scope %s is not available", req.GetScope())) - } - - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.String("ovm.logs.replyTo", replyTo), - attribute.String("ovm.logs.scope", req.GetScope()), - attribute.String("ovm.logs.query", req.GetQuery()), - attribute.String("ovm.logs.from", req.GetFrom().String()), - attribute.String("ovm.logs.to", req.GetTo().String()), - attribute.Int("ovm.logs.maxRecords", int(req.GetMaxRecords())), - attribute.Bool("ovm.logs.startFromOldest", req.GetStartFromOldest()), - attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), - ) - - stream := &LogRecordsStreamImpl{ - subject: replyTo, - stream: e.natsConnection, - } - err = e.logAdapter.Get(ctx, req, stream) - - span.SetAttributes( - attribute.Int("ovm.logs.numResponses", stream.responses), - attribute.Int("ovm.logs.numRecords", stream.records), - ) - srcErr := &sdp.SourceError{} - if errors.As(err, &srcErr) { - return srcErr - } - if errors.Is(err, context.DeadlineExceeded) || ctx.Err() == context.DeadlineExceeded { - return sdp.NewLocalSourceError(connect.CodeDeadlineExceeded, "log records request deadline exceeded") - } - if err != nil { - return sdp.NewLocalSourceError(connect.CodeInternal, fmt.Sprintf("error handling log records request: %v", err)) - } - - return nil -} - -// ClearAdapters Deletes all adapters from the engine, allowing new adapters to be -// added using `AddAdapter()`. Note that this requires a restart using -// `Restart()` in order to take effect -func (e *Engine) ClearAdapters() { - e.sh.ClearAllAdapters() -} - -// IsWildcard checks if a string is the wildcard. Use this instead of -// implementing the wildcard check everywhere so that if we need to change the -// wildcard at a later date we can do so here -func IsWildcard(s string) bool { - return s == sdp.WILDCARD -} - -// SetLogAdapter registers a single LogAdapter with the engine. -// Returns an error when there is already a log adapter registered. -func (e *Engine) SetLogAdapter(adapter LogAdapter) error { - if adapter == nil { - return errors.New("log adapter cannot be nil") - } - - e.logAdapterMu.Lock() - defer e.logAdapterMu.Unlock() - - if e.logAdapter != nil { - return errors.New("log adapter already registered") - } - - e.logAdapter = adapter - return nil -} - -// GetAvailableScopesAndMetadata returns the available scopes and adapter metadata -// from all visible adapters. This is useful for heartbeats and other reporting. -func (e *Engine) GetAvailableScopesAndMetadata() ([]string, []*sdp.AdapterMetadata) { - // Get available types and scopes - availableScopesMap := map[string]bool{} - adapterMetadata := []*sdp.AdapterMetadata{} - - for _, adapter := range e.sh.VisibleAdapters() { - for _, scope := range adapter.Scopes() { - availableScopesMap[scope] = true - } - adapterMetadata = append(adapterMetadata, adapter.Metadata()) - } - - // Extract slices from maps - availableScopes := []string{} - for s := range availableScopesMap { - availableScopes = append(availableScopes, s) - } - - return availableScopes, adapterMetadata -} - -// AdaptersByType returns adapters of the specified type. This is useful for health checks. -func (e *Engine) AdaptersByType(typ string) []Adapter { - return e.sh.AdaptersByType(typ) -} - -// SetInitError stores a persistent initialization error that will be reported via heartbeat and readiness checks. -// This should be called when source initialization fails in a way that prevents adapters from being added, -// but the process should continue running to serve probes and heartbeats (avoiding CrashLoopBackOff). -// -// Pass nil to clear a previously set error (e.g. after successful retry/restart). -// -// Example usage: -// -// if err := initializeAdapters(); err != nil { -// e.SetInitError(fmt.Errorf("adapter initialization failed: %w", err)) -// // Continue running - pod stays Running with readiness failing -// } -func (e *Engine) SetInitError(err error) { - e.initErrorMutex.Lock() - defer e.initErrorMutex.Unlock() - e.initError = err -} - -// GetInitError returns the persistent initialization error if any. -// Returns nil if no init error is set or if it was cleared via SetInitError(nil). -func (e *Engine) GetInitError() error { - e.initErrorMutex.RLock() - defer e.initErrorMutex.RUnlock() - return e.initError -} - -// InitialiseAdapters retries initFn with exponential backoff (capped at -// 5 minutes) until it succeeds or ctx is cancelled. It blocks the caller. -// -// This is intended for adapter initialization that makes API calls to upstream -// services and may fail transiently. Because it blocks, the caller can -// safely set up namespace watches or other reload mechanisms after it returns -// without racing against a background retry goroutine. -// -// On each attempt: -// - ClearAdapters() is called to remove any leftovers from previous attempts. -// - initFn is called. The init error is updated via SetInitError immediately -// (cleared on success, set on failure) and then a heartbeat is sent so the -// API/UI always reflects the current status. -// - On success, StartSendingHeartbeats is called and the function returns. -// -// The caller should have already called Start() before calling this. -func (e *Engine) InitialiseAdapters(ctx context.Context, initFn func(ctx context.Context) error) { - b := backoff.NewExponentialBackOff() - b.MaxInterval = 5 * time.Minute - tick := backoff.NewTicker(b) - defer tick.Stop() - - for { - select { - case <-ctx.Done(): - return - case _, ok := <-tick.C: - if !ok { - // Backoff exhausted (shouldn't happen with default MaxElapsedTime=0) - return - } - - e.ClearAdapters() - - err := initFn(ctx) - - if err != nil { - e.SetInitError(fmt.Errorf("adapter initialisation failed: %w", err)) - log.WithError(err).Warn("Adapter initialisation failed, will retry") - } else { - // Clear any previous init error before the heartbeat so the - // API/UI immediately sees the healthy status. - e.SetInitError(nil) - } - - // Send heartbeat regardless of outcome so the API/UI reflects current status - if hbErr := e.SendHeartbeat(ctx, nil); hbErr != nil { - log.WithError(hbErr).Error("Error sending heartbeat during adapter initialisation") - } - - if err != nil { - continue - } - - e.StartSendingHeartbeats(ctx) - return - } - } -} - -// LivenessProbeHandlerFunc returns an HTTP handler function for liveness probes. -// This checks only engine initialization (NATS connection, heartbeats) and does NOT check adapter-specific health. -func (e *Engine) LivenessProbeHandlerFunc() func(http.ResponseWriter, *http.Request) { - return func(rw http.ResponseWriter, r *http.Request) { - ctx, span := tracing.HealthCheckTracer().Start(r.Context(), "healthcheck.liveness") - defer span.End() - - err := e.LivenessHealthCheck(ctx) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Liveness check failed") - http.Error(rw, err.Error(), http.StatusServiceUnavailable) - return - } - - fmt.Fprint(rw, "ok") - } -} - -// SetReadinessCheck sets the readiness check and ensures HeartbeatOptions is initialized. -func (e *Engine) SetReadinessCheck(check func(context.Context) error) { - if e.EngineConfig.HeartbeatOptions == nil { - e.EngineConfig.HeartbeatOptions = &HeartbeatOptions{} - } - e.EngineConfig.HeartbeatOptions.ReadinessCheck = check -} - -// ReadinessProbeHandlerFunc returns an HTTP handler function for readiness probes. -// This checks adapter-specific health only (not engine/liveness). -func (e *Engine) ReadinessProbeHandlerFunc() func(http.ResponseWriter, *http.Request) { - return func(rw http.ResponseWriter, r *http.Request) { - ctx, span := tracing.HealthCheckTracer().Start(r.Context(), "healthcheck.readiness") - defer span.End() - - err := e.ReadinessHealthCheck(ctx) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Readiness check failed") - http.Error(rw, err.Error(), http.StatusServiceUnavailable) - return - } - - 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/engine_initerror_test.go b/discovery/engine_initerror_test.go deleted file mode 100644 index 37ed2404..00000000 --- a/discovery/engine_initerror_test.go +++ /dev/null @@ -1,365 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - "testing" - "time" - - "connectrpc.com/connect" - "github.com/overmindtech/cli/sdp-go" -) - -func TestSetInitError(t *testing.T) { - e := &Engine{ - initError: nil, - initErrorMutex: sync.RWMutex{}, - } - - testErr := errors.New("initialization failed") - e.SetInitError(testErr) - - // Direct pointer comparison is intentional here - we want to verify the exact error object is stored - if e.initError == nil || e.initError.Error() != testErr.Error() { - t.Errorf("expected initError to be %v, got %v", testErr, e.initError) - } -} - -func TestGetInitError(t *testing.T) { - e := &Engine{ - initError: nil, - initErrorMutex: sync.RWMutex{}, - } - - // Test nil case - if err := e.GetInitError(); err != nil { - t.Errorf("expected nil error, got %v", err) - } - - // Test with error set - testErr := errors.New("test error") - e.initError = testErr - - if err := e.GetInitError(); err == nil || err.Error() != testErr.Error() { - t.Errorf("expected error to be %v, got %v", testErr, err) - } -} - -func TestSetInitErrorNil(t *testing.T) { - e := &Engine{ - initError: errors.New("previous error"), - initErrorMutex: sync.RWMutex{}, - } - - // Clear the error - e.SetInitError(nil) - - if e.initError != nil { - t.Errorf("expected initError to be nil after clearing, got %v", e.initError) - } - - if err := e.GetInitError(); err != nil { - t.Errorf("expected GetInitError to return nil after clearing, got %v", err) - } -} - -func TestInitErrorConcurrentAccess(t *testing.T) { - e := &Engine{ - initError: nil, - initErrorMutex: sync.RWMutex{}, - } - - // Test concurrent access from multiple goroutines - var wg sync.WaitGroup - iterations := 100 - - // Writers - for i := range 10 { - wg.Add(1) - go func(id int) { - defer wg.Done() - for j := range iterations { - e.SetInitError(fmt.Errorf("error from goroutine %d iteration %d", id, j)) - } - }(i) - } - - // Readers - for range 10 { - wg.Add(1) - go func() { - defer wg.Done() - for range iterations { - _ = e.GetInitError() - } - }() - } - - wg.Wait() - - // Should not panic - error should be one of the written values or nil - finalErr := e.GetInitError() - if finalErr == nil { - t.Log("Final error is nil (acceptable in concurrent test)") - } else { - t.Logf("Final error: %v", finalErr) - } -} - -func TestReadinessHealthCheckWithInitError(t *testing.T) { - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - ReadinessCheck: func(ctx context.Context) error { - // Adapter health is fine - return nil - }, - }, - } - - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - ctx := context.Background() - - // Readiness should pass when no init error - if err := e.ReadinessHealthCheck(ctx); err != nil { - t.Errorf("expected readiness to pass with no init error, got: %v", err) - } - - // Set an init error - testErr := errors.New("AWS AssumeRole denied") - e.SetInitError(testErr) - - // Readiness should now fail with the init error - err = e.ReadinessHealthCheck(ctx) - if err == nil { - t.Error("expected readiness to fail with init error, got nil") - } else if !errors.Is(err, testErr) { - t.Errorf("expected readiness error to wrap init error, got: %v", err) - } - - // Clear the init error - e.SetInitError(nil) - - // Readiness should pass again - if err := e.ReadinessHealthCheck(ctx); err != nil { - t.Errorf("expected readiness to pass after clearing init error, got: %v", err) - } -} - -func TestSendHeartbeatWithInitError(t *testing.T) { - requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 10) - responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 10) - - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - ManagementClient: testHeartbeatClient{ - Requests: requests, - Responses: responses, - }, - Frequency: 0, // Disable automatic heartbeats - ReadinessCheck: func(ctx context.Context) error { - return nil // Adapters are fine - }, - }, - } - - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - ctx := context.Background() - - // Send heartbeat with init error - testErr := errors.New("configuration error: invalid credentials") - e.SetInitError(testErr) - - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - - err = e.SendHeartbeat(ctx, nil) - if err != nil { - t.Errorf("expected SendHeartbeat to succeed, got: %v", err) - } - - // Verify the heartbeat included the init error - req := <-requests - if req.Msg.GetError() == "" { - t.Error("expected heartbeat to include error, got empty string") - } else if !strings.Contains(req.Msg.GetError(), testErr.Error()) { - t.Errorf("expected heartbeat error to contain %q, got: %q", testErr.Error(), req.Msg.GetError()) - } -} - -func TestSendHeartbeatWithInitErrorAndCustomError(t *testing.T) { - requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 10) - responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 10) - - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - ManagementClient: testHeartbeatClient{ - Requests: requests, - Responses: responses, - }, - Frequency: 0, - }, - } - - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - ctx := context.Background() - - // Set init error and send heartbeat with custom error - initErr := errors.New("init failed: invalid config") - customErr := errors.New("custom error: readiness failed") - e.SetInitError(initErr) - - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - - err = e.SendHeartbeat(ctx, customErr) - if err != nil { - t.Errorf("expected SendHeartbeat to succeed, got: %v", err) - } - - // Verify both errors are included in the heartbeat - req := <-requests - if req.Msg.GetError() == "" { - t.Error("expected heartbeat to include errors, got empty string") - } else { - errMsg := req.Msg.GetError() - // Both errors should be in the joined error string - if !strings.Contains(errMsg, initErr.Error()) { - t.Errorf("expected heartbeat error to include init error %q, got: %q", initErr.Error(), errMsg) - } - if !strings.Contains(errMsg, customErr.Error()) { - t.Errorf("expected heartbeat error to include custom error %q, got: %q", customErr.Error(), errMsg) - } - } -} - -func TestInitialiseAdapters_Success(t *testing.T) { - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - Frequency: 0, // Disable automatic heartbeats from StartSendingHeartbeats - }, - } - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - // Set an init error to verify it gets cleared on success - e.SetInitError(errors.New("previous error")) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - var called bool - e.InitialiseAdapters(ctx, func(ctx context.Context) error { - called = true - return nil - }) - - if !called { - t.Error("initFn was not called") - } - if err := e.GetInitError(); err != nil { - t.Errorf("expected init error to be cleared after success, got: %v", err) - } -} - -func TestInitialiseAdapters_RetryThenSuccess(t *testing.T) { - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - Frequency: 0, - }, - } - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - attempts := 0 - e.InitialiseAdapters(ctx, func(ctx context.Context) error { - attempts++ - if attempts < 3 { - return fmt.Errorf("transient error attempt %d", attempts) - } - return nil - }) - - if attempts < 3 { - t.Errorf("expected at least 3 attempts, got %d", attempts) - } - if err := e.GetInitError(); err != nil { - t.Errorf("expected init error to be cleared after eventual success, got: %v", err) - } -} - -func TestInitialiseAdapters_ContextCancelled(t *testing.T) { - ec := &EngineConfig{ - EngineType: "test", - SourceName: "test-source", - HeartbeatOptions: &HeartbeatOptions{ - Frequency: 0, - }, - } - e, err := NewEngine(ec) - if err != nil { - t.Fatalf("failed to create engine: %v", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - var callCount int - - // InitialiseAdapters blocks; cancel ctx after a short delay so it returns - time.AfterFunc(500*time.Millisecond, cancel) - - done := make(chan struct{}) - go func() { - e.InitialiseAdapters(ctx, func(ctx context.Context) error { - callCount++ - return errors.New("always fails") - }) - close(done) - }() - - select { - case <-done: - // InitialiseAdapters returned (ctx was cancelled) - case <-time.After(5 * time.Second): - t.Fatal("InitialiseAdapters did not return after context cancellation") - } - - if callCount == 0 { - t.Error("expected initFn to be called at least once before context cancellation") - } - if err := e.GetInitError(); err == nil { - t.Error("expected init error to be set after context cancellation with failures") - } -} diff --git a/discovery/engine_test.go b/discovery/engine_test.go deleted file mode 100644 index 8dd7ad6d..00000000 --- a/discovery/engine_test.go +++ /dev/null @@ -1,808 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - "os" - "sync" - "testing" - "time" - - "github.com/google/uuid" - "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats-server/v2/test" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "golang.org/x/oauth2" -) - -func newEngine(t *testing.T, name string, no *auth.NATSOptions, eConn sdp.EncodedConnection, adapters ...Adapter) *Engine { - t.Helper() - - if no != nil && eConn != nil { - t.Fatal("Cannot provide both NATSOptions and EncodedConnection") - } - - ec := EngineConfig{ - MaxParallelExecutions: 10, - SourceName: name, - NATSQueueName: "test", - } - if no != nil { - ec.NATSOptions = no - if no.TokenClient == nil { - ec.Unauthenticated = true - } - } else if eConn == nil { - ec.NATSOptions = &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - TokenClient: GetTestOAuthTokenClient(t, "org_hdeUXbB55sMMvJLa"), - } - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - if eConn != nil { - e.natsConnection = eConn - } - - if err := e.AddAdapters(adapters...); err != nil { - t.Fatalf("Error adding adapters: %v", err) - } - - return e -} - -func newStartedEngine(t *testing.T, name string, no *auth.NATSOptions, eConn sdp.EncodedConnection, adapters ...Adapter) *Engine { - t.Helper() - - e := newEngine(t, name, no, eConn, adapters...) - - err := e.Start(t.Context()) - if err != nil { - t.Fatalf("Error starting Engine: %v", err) - } - - t.Cleanup(func() { - err = e.Stop() - if err != nil { - t.Errorf("Error stopping Engine: %v", err) - } - }) - - return e -} - -func TestTrackQuery(t *testing.T) { - t.Run("With normal query", func(t *testing.T) { - t.Parallel() - - e := newStartedEngine(t, "TestTrackQuery_normal", nil, nil) - - u := uuid.New() - - qt := QueryTracker{ - Engine: e, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_LIST, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 10, - }, - UUID: u[:], - }, - } - - e.TrackQuery(u, &qt) - - if got, err := e.GetTrackedQuery(u); err == nil { - if got != &qt { - t.Errorf("Got mismatched QueryTracker objects %v and %v", got, &qt) - } - } else { - t.Error(err) - } - }) - - t.Run("With many queries", func(t *testing.T) { - t.Parallel() - - e := newStartedEngine(t, "TestTrackQuery_many", nil, nil) - - var wg sync.WaitGroup - - for i := range 1000 { - wg.Add(1) - go func(i int) { - defer wg.Done() - u := uuid.New() - - qt := QueryTracker{ - Engine: e, - Query: &sdp.Query{ - Type: "person", - Query: fmt.Sprintf("person-%v", i), - Method: sdp.QueryMethod_GET, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 10, - }, - UUID: u[:], - }, - } - - e.TrackQuery(u, &qt) - }(i) - } - - wg.Wait() - - if len(e.trackedQueries) != 1000 { - t.Errorf("Expected 1000 tracked queries, got %v", len(e.trackedQueries)) - } - }) -} - -func TestDeleteTrackedQuery(t *testing.T) { - t.Parallel() - e := newStartedEngine(t, "TestDeleteTrackedQuery", nil, nil) - - var wg sync.WaitGroup - - // Add and delete many query in parallel - for i := 1; i < 1000; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - u := uuid.New() - - qt := QueryTracker{ - Engine: e, - Query: &sdp.Query{ - Type: "person", - Query: fmt.Sprintf("person-%v", i), - Method: sdp.QueryMethod_GET, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 10, - }, - UUID: u[:], - }, - } - - e.TrackQuery(u, &qt) - wg.Add(1) - go func(u uuid.UUID) { - defer wg.Done() - e.DeleteTrackedQuery(u) - }(u) - }(i) - } - - wg.Wait() - - if len(e.trackedQueries) != 0 { - t.Errorf("Expected 0 tracked queries, got %v", len(e.trackedQueries)) - } -} - -func TestNats(t *testing.T) { - SkipWithoutNats(t) - - ec := EngineConfig{ - MaxParallelExecutions: 10, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - }, - NATSQueueName: "test", - } - - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - adapter := TestAdapter{} - adapter.cache = sdpcache.NewNoOpCache() - err = e.AddAdapters( - &adapter, - &TestAdapter{ - ReturnScopes: []string{ - sdp.WILDCARD, - }, - ReturnName: "test-adapter", - ReturnType: "test", - cache: sdpcache.NewNoOpCache(), - }, - ) - if err != nil { - t.Fatal(err) - } - - t.Run("Starting", func(t *testing.T) { - err := e.Start(t.Context()) - if err != nil { - t.Error(err) - } - - if e.natsConnection.Underlying().NumSubscriptions() != 4 { - t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) - } - }) - - t.Run("Restarting", func(t *testing.T) { - err := e.Stop() - if err != nil { - t.Error(err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Error(err) - } - - if e.natsConnection.Underlying().NumSubscriptions() != 4 { - t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) - } - }) - - t.Run("Handling a basic query", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - query := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "basic", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - } - - _, _, _, err := sdp.RunSourceQuerySync(context.Background(), query, sdp.DefaultStartTimeout, e.natsConnection) - if err != nil { - t.Error(err) - } - - if len(adapter.GetCalls) != 1 { - t.Errorf("expected 1 get call, got %v: %v", len(adapter.GetCalls), adapter.GetCalls) - } - }) - - t.Run("stopping", func(t *testing.T) { - err := e.Stop() - if err != nil { - t.Error(err) - } - }) -} - -func TestNatsCancel(t *testing.T) { - SkipWithoutNats(t) - - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - adapter := SpeedTestAdapter{ - QueryDelay: 2 * time.Second, - ReturnType: "person", - ReturnScopes: []string{"test"}, - } - - if err := e.AddAdapters(&adapter); err != nil { - t.Fatalf("Error adding adapters: %v", err) - } - - t.Run("Starting", func(t *testing.T) { - err := e.Start(t.Context()) - if err != nil { - t.Error(err) - } - }) - - t.Run("Cancelling queries", func(t *testing.T) { - conn := e.natsConnection - u := uuid.New() - - query := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "foo", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 100, - }, - Scope: "*", - UUID: u[:], - } - - responses := make(chan *sdp.QueryResponse, 1000) - progress, err := sdp.RunSourceQuery(t.Context(), query, sdp.DefaultStartTimeout, conn, responses) - if err != nil { - t.Error(err) - } - - time.Sleep(250 * time.Millisecond) - - err = conn.Publish(context.Background(), "cancel.all", &sdp.CancelQuery{ - UUID: u[:], - }) - if err != nil { - t.Error(err) - } - - // Read and discard all items and errors until they are closed - for range responses { - } - - time.Sleep(250 * time.Millisecond) - - if progress.Progress().Cancelled != 1 { - t.Errorf("Expected query to be cancelled, got\n%v", progress.String()) - } - }) - - t.Run("stopping", func(t *testing.T) { - err := e.Stop() - if err != nil { - t.Error(err) - } - }) -} - -func TestNatsConnections(t *testing.T) { - t.Run("with a bad hostname", func(t *testing.T) { - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - Servers: []string{"nats://bad.server"}, - ConnectionName: "test-disconnection", - ConnectionTimeout: time.Second, - MaxReconnects: 1, - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - err = e.Start(t.Context()) - - if err == nil { - t.Error("expected error but got nil") - } - }) - - t.Run("with a server that disconnects", func(t *testing.T) { - // We are running a custom server here so that we can control its lifecycle - opts := test.DefaultTestOptions - // Need to change this to avoid port clashes in github actions - opts.Port = 4111 - s := test.RunServer(&opts) - - if !s.ReadyForConnections(10 * time.Second) { - t.Fatal("Could not start goroutine NATS server") - } - - t.Cleanup(func() { - if s != nil { - s.Shutdown() - } - }) - - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: []string{"127.0.0.1:4111"}, - ConnectionName: "test-disconnection", - ConnectionTimeout: time.Second, - MaxReconnects: 10, - ReconnectWait: time.Second, - ReconnectJitter: time.Second, - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - - t.Log("Stopping NATS server") - s.Shutdown() - - for i := range 21 { - if i == 20 { - t.Errorf("Engine did not report a NATS disconnect after %v tries", i) - } - - if !e.IsNATSConnected() { - break - } - - time.Sleep(time.Second) - } - - // Reset the server - s = test.RunServer(&opts) - - // Wait for the server to start - s.ReadyForConnections(10 * time.Second) - - // Wait 2 more seconds for a reconnect - time.Sleep(2 * time.Second) - - for range 21 { - if e.IsNATSConnected() { - return - } - - time.Sleep(time.Second) - } - - t.Error("Engine should have reconnected but hasn't") - }) - - t.Run("with a server that takes a while to start", func(t *testing.T) { - // We are running a custom server here so that we can control its lifecycle - opts := test.DefaultTestOptions - // Need to change this to avoid port clashes in github actions - opts.Port = 4112 - - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 10, - RetryDelay: time.Second, - Servers: []string{"127.0.0.1:4112"}, - ConnectionName: "test-disconnection", - ConnectionTimeout: time.Second, - MaxReconnects: 10, - ReconnectWait: time.Second, - ReconnectJitter: time.Second, - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - var s *server.Server - - go func() { - // Start the server after a delay - time.Sleep(2 * time.Second) - - // We are running a custom server here so that we can control its lifecycle - s = test.RunServer(&opts) - - t.Cleanup(func() { - if s != nil { - s.Shutdown() - } - }) - }() - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - }) -} - -func TestNATSFailureRestart(t *testing.T) { - restartTestOption := test.DefaultTestOptions - restartTestOption.Port = 4113 - - // We are running a custom server here so that we can control its lifecycle - s := test.RunServer(&restartTestOption) - - if !s.ReadyForConnections(10 * time.Second) { - t.Fatal("Could not start goroutine NATS server") - } - - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 10, - RetryDelay: time.Second, - Servers: []string{"127.0.0.1:4113"}, - ConnectionName: "test-disconnection", - ConnectionTimeout: time.Second, - MaxReconnects: 10, - ReconnectWait: 100 * time.Millisecond, - ReconnectJitter: 10 * time.Millisecond, - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - e.ConnectionWatchInterval = 1 * time.Second - - // Connect successfully - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - err = e.Stop() - if err != nil { - t.Fatal(err) - } - }) - - // Lose the connection - t.Log("Stopping NATS server") - s.Shutdown() - s.WaitForShutdown() - - // The watcher should keep watching while the nats connection is - // RECONNECTING, once it's CLOSED however it won't keep trying to connect so - // we want to make sure that the watcher detects this and kills the whole - // thing - time.Sleep(2 * time.Second) - - s = test.RunServer(&restartTestOption) - if !s.ReadyForConnections(10 * time.Second) { - t.Fatal("Could not start goroutine NATS server a second time") - } - - t.Cleanup(func() { - s.Shutdown() - }) - - time.Sleep(3 * time.Second) - - if !e.IsNATSConnected() { - t.Error("NATS didn't manage to reconnect") - } -} - -func TestNatsAuth(t *testing.T) { - SkipWithoutNatsAuth(t) - - ec := EngineConfig{ - MaxParallelExecutions: 1, - SourceName: "nats-test", - NATSOptions: &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - TokenClient: GetTestOAuthTokenClient(t, "org_hdeUXbB55sMMvJLa"), - }, - NATSQueueName: "test", - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - adapter := TestAdapter{} - adapter.cache = sdpcache.NewNoOpCache() - if err := e.AddAdapters( - &adapter, - &TestAdapter{ - ReturnScopes: []string{ - sdp.WILDCARD, - }, - ReturnType: "test", - ReturnName: "test-adapter", - cache: sdpcache.NewNoOpCache(), - }, - ); err != nil { - t.Fatalf("Error adding adapters: %v", err) - } - - t.Run("Starting", func(t *testing.T) { - err := e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - - if e.natsConnection.Underlying().NumSubscriptions() != 4 { - t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) - } - }) - - t.Run("Handling a basic query", func(t *testing.T) { - t.Cleanup(func() { - adapter.ClearCalls() - }) - - query := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "basic", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - } - - _, _, _, err := sdp.RunSourceQuerySync(t.Context(), query, sdp.DefaultStartTimeout, e.natsConnection) - if err != nil { - t.Error(err) - } - - if len(adapter.GetCalls) != 1 { - t.Errorf("expected 1 get call, got %v: %v", len(adapter.GetCalls), adapter.GetCalls) - } - }) - - t.Run("stopping", func(t *testing.T) { - err := e.Stop() - if err != nil { - t.Error(err) - } - }) -} - -func TestSetupMaxQueryTimeout(t *testing.T) { - t.Run("with no value", func(t *testing.T) { - ec := EngineConfig{} - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - - if e.MaxRequestTimeout != DefaultMaxRequestTimeout { - t.Errorf("max request timeout did not default. Got %v expected %v", e.MaxRequestTimeout.String(), DefaultMaxRequestTimeout.String()) - } - }) - - t.Run("with a value", func(t *testing.T) { - ec := EngineConfig{} - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - e.MaxRequestTimeout = 1 * time.Second - - if e.MaxRequestTimeout != 1*time.Second { - t.Errorf("max request timeout did not take provided value. Got %v expected %v", e.MaxRequestTimeout.String(), (1 * time.Second).String()) - } - }) -} - -func TestEngineHealthCheckHandlesNilConnection(t *testing.T) { - t.Parallel() - - t.Run("without connection", func(t *testing.T) { - t.Parallel() - e := newEngine(t, "TestEngineHealthCheckHandlesNilConnection_NoConn", &auth.NATSOptions{}, nil) - - assertHealthCheckDoesNotPanic(t, e) - }) - - t.Run("with dropped underlying connection", func(t *testing.T) { - t.Parallel() - e := newEngine(t, "TestEngineHealthCheckHandlesNilConnection_Dropped", &auth.NATSOptions{}, nil) - e.natsConnection = &sdp.EncodedConnectionImpl{} - - assertHealthCheckDoesNotPanic(t, e) - }) -} - -func assertHealthCheckDoesNotPanic(t *testing.T, e *Engine) { - t.Helper() - - defer func() { - if r := recover(); r != nil { - t.Fatalf("LivenessHealthCheck panic: %v", r) - } - }() - - ctx := context.Background() - if err := e.LivenessHealthCheck(ctx); err == nil { - t.Fatalf("expected LivenessHealthCheck to report disconnected NATS") - } -} - -var ( - testTokenSource oauth2.TokenSource - testTokenSourceMu sync.Mutex -) - -func GetTestOAuthTokenClient(t *testing.T, account string) auth.TokenClient { - var domain string - var clientID string - var clientSecret string - var exists bool - - errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/auth0-test-data" - - // Read secrets form the environment - if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_DOMAIN") - t.Skip("Skipping due to missing environment setup") - } - - if clientID, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_ID"); !exists || clientID == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_ID") - t.Skip("Skipping due to missing environment setup") - } - - if clientSecret, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_SECRET"); !exists || clientSecret == "" { - t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_SECRET") - t.Skip("Skipping due to missing environment setup") - } - - exchangeURL, err := GetWorkingTokenExchange() - if err != nil { - t.Skipf("Token exchange API server not available: %v", err) - return nil - } - - testTokenSourceMu.Lock() - defer testTokenSourceMu.Unlock() - if testTokenSource == nil { - ccc := auth.ClientCredentialsConfig{ - ClientID: clientID, - ClientSecret: clientSecret, - } - testTokenSource = ccc.TokenSource( - t.Context(), - fmt.Sprintf("https://%v/oauth/token", domain), - os.Getenv("API_SERVER_AUDIENCE"), - ) - } - - return auth.NewOAuthTokenClient( - exchangeURL, - account, - testTokenSource, - ) -} diff --git a/discovery/enginerequests.go b/discovery/enginerequests.go deleted file mode 100644 index c3bf6856..00000000 --- a/discovery/enginerequests.go +++ /dev/null @@ -1,532 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/google/uuid" - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "github.com/sourcegraph/conc/pool" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// NewItemSubject Generates a random subject name for returning items e.g. -// return.item._INBOX.712ab421 -func NewItemSubject() string { - return fmt.Sprintf("return.item.%v", nats.NewInbox()) -} - -// NewResponseSubject Generates a random subject name for returning responses -// e.g. return.response._INBOX.978af6de -func NewResponseSubject() string { - return fmt.Sprintf("return.response.%v", nats.NewInbox()) -} - -// HandleQuery Handles a single query. This includes responses, linking -// etc. -func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { - var deadlineOverride bool - - // Respond saying we've got it - responder := sdp.ResponseSender{ - ResponseSubject: query.Subject(), - } - - var pub sdp.EncodedConnection - - if e.IsNATSConnected() { - pub = e.natsConnection - } else { - pub = NilConnection{} - } - - ru := uuid.New() - responder.Start( - ctx, - pub, - e.EngineConfig.SourceName, - ru, - ) - - // Ensure responder ends exactly once (prevents double-ending on panic) - var responderEndOnce sync.Once - defer func() { - // Safety net: if we panic before explicitly ending, mark as error - responderEndOnce.Do(func() { - responder.ErrorWithContext(ctx) - }) - }() - - // If there is no deadline OR further in the future than MaxRequestTimeout, clamp the deadline to MaxRequestTimeout - maxRequestDeadline := time.Now().Add(e.MaxRequestTimeout) - if query.GetDeadline() == nil || query.GetDeadline().AsTime().After(maxRequestDeadline) { - query.Deadline = timestamppb.New(maxRequestDeadline) - deadlineOverride = true - log.WithContext(ctx).WithField("ovm.deadline", query.GetDeadline().AsTime()).Debug("capping deadline to MaxRequestTimeout") - } - - // Add the query timeout to the context stack - ctx, cancel := query.TimeoutContext(ctx) - defer cancel() - - numExpandedQueries := len(e.sh.ExpandQuery(query)) - - if numExpandedQueries == 0 { - // If we don't have any relevant adapters, mark as done (OK) and exit - responderEndOnce.Do(func() { - responder.DoneWithContext(ctx) - }) - return - } - - // Extract and parse the UUID - u, uuidErr := uuid.FromBytes(query.GetUUID()) - - // 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.Bool("ovm.sdp.deadlineOverridden", deadlineOverride), - 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())), - attribute.Bool("ovm.sdp.followOnlyBlastPropagation", query.GetRecursionBehaviour().GetFollowOnlyBlastPropagation()), - ) - } - - qt := QueryTracker{ - Query: query, - Engine: e, - Context: ctx, - Cancel: cancel, - } - - if uuidErr == nil { - e.TrackQuery(u, &qt) - defer e.DeleteTrackedQuery(u) - } - - // the query tracker will send responses directly through the embedded - // engine's nats connection - _, _, _, err := qt.Execute(ctx) - - // End responder based on execution result - if err != nil { - if errors.Is(err, context.Canceled) { - responderEndOnce.Do(func() { - responder.CancelWithContext(ctx) - }) - } else { - responderEndOnce.Do(func() { - responder.ErrorWithContext(ctx) - }) - } - span.SetAttributes( - attribute.String("ovm.sdp.errorType", "OTHER"), - attribute.String("ovm.sdp.errorString", err.Error()), - ) - } else { - responderEndOnce.Do(func() { - responder.DoneWithContext(ctx) - }) - } -} - -var listExecutionPoolCount atomic.Int32 -var getExecutionPoolCount atomic.Int32 - -// ExecuteQuery Executes a single Query and returns the results without any -// linking. Will return an error if the Query couldn't be run. -// -// Items and errors will be sent to the supplied channels as they are found. -// Note that if these channels are not buffered, something will need to be -// receiving the results or this method will never finish. If results are not -// required the channels can be nil -func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses chan<- *sdp.QueryResponse) error { - span := trace.SpanFromContext(ctx) - - // Make sure we close channels once we're done - if responses != nil { - defer close(responses) - } - - if ctx.Err() != nil { - return ctx.Err() - } - - expanded := e.sh.ExpandQuery(query) - - span.SetAttributes( - attribute.Int("ovm.adapter.numExpandedQueries", len(expanded)), - ) - - if len(expanded) == 0 { - responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ - ErrorType: sdp.QueryError_NOSCOPE, - ErrorString: "no matching adapters found", - Scope: query.GetScope(), - }) - - return errors.New("no matching adapters found") - } - - // Since we need to wait for only the processing of this query's executions, we need a separate WaitGroup here - // Overall MaxParallelExecutions evaluation is handled by e.executionPool - wg := sync.WaitGroup{} - expandedMutex := sync.RWMutex{} - expandedMutex.RLock() - for q, adapter := range expanded { - wg.Add(1) - // localize values for the closure below - localQ, localAdapter := q, adapter - - var p *pool.Pool - if localQ.GetMethod() == sdp.QueryMethod_LIST { - p = e.listExecutionPool - listExecutionPoolCount.Add(1) - } else { - p = e.getExecutionPool - getExecutionPoolCount.Add(1) - } - - // push all queued items through a goroutine to avoid blocking `ExecuteQuery` from progressing - // as `executionPool.Go()` will block once the max parallelism is hit - go func() { - // queue everything into the execution pool - defer tracing.LogRecoverToReturn(ctx, "ExecuteQuery outer") - span.SetAttributes( - attribute.Int("ovm.discovery.listExecutionPoolCount", int(listExecutionPoolCount.Load())), - attribute.Int("ovm.discovery.getExecutionPoolCount", int(getExecutionPoolCount.Load())), - ) - p.Go(func() { - defer tracing.LogRecoverToReturn(ctx, "ExecuteQuery inner") - defer func() { - // Mark the work as done. This happens before we start - // waiting on `expandedMutex` below, to ensure that the - // queues can continue executing even if we are waiting on - // the mutex. - wg.Done() - - // Delete our query from the map so that we can track which - // ones are still running - expandedMutex.Lock() - defer expandedMutex.Unlock() - delete(expanded, localQ) - }() - defer func() { - if localQ.GetMethod() == sdp.QueryMethod_LIST { - listExecutionPoolCount.Add(-1) - } else { - getExecutionPoolCount.Add(-1) - } - }() - - // If the context is cancelled, don't even bother doing - // anything. Since the `p.Go` will block, it's possible that if - // the pool was exhausted, the context could be cancelled before - // the goroutine is executed - if ctx.Err() != nil { - return - } - - // Execute the query against the adapter - e.Execute(ctx, localQ, localAdapter, responses) - }) - }() - } - expandedMutex.RUnlock() - - waitGroupDone := make(chan struct{}) - go func() { - wg.Wait() - close(waitGroupDone) - }() - - select { - case <-waitGroupDone: - // All adapters have finished - case <-ctx.Done(): - // The context was cancelled, this should have propagated to all the - // adapters and therefore we should see the wait group finish very - // quickly now. We will check this though to make sure. This will wait - // until we reach Change Analysis SLO violation territory. If this is - // too quick, we are only spamming logs for nothing. - longRunningAdaptersTimeout := 2 * time.Minute - - // Wait for the wait group, but ping the logs if it's taking - // too long - func() { - for { - select { - case <-waitGroupDone: - return - case <-time.After(longRunningAdaptersTimeout): - // If we're here, then the wait group didn't finish in time - expandedMutex.RLock() - for q, adapter := range expanded { - // There is a honeycomb trigger for this message: - // - // https://ui.honeycomb.io/overmind/environments/prod/datasets/kubernetes-metrics/triggers/saWNAnCAXNb - // - // This is to ensure we are aware of any adapters that - // are taking too long to respond to a query, which - // 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.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() - // the query is already bolloxed up, we don't need continue to wait and spam the logs any more - return - } - } - }() - } - - // If the context is cancelled, return that error - if ctx.Err() != nil { - return ctx.Err() - } - - return nil -} - -// Runs a query against an adapter. Returns an error if the query fails in a -// "fatal" way that should consider the query as failed. Other non-fatal errors -// should be sent on the stream. Channels for items and errors will NOT be -// closed by this function, the caller should do that as this will likely be -// 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.query", q.GetQuery()), - )) - 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 - // a List to block all subsequent Get queries until it is done - switch q.GetMethod() { - case sdp.QueryMethod_GET: - e.gfm.GetLock(q.GetScope(), q.GetType()) - defer e.gfm.GetUnlock(q.GetScope(), q.GetType()) - case sdp.QueryMethod_LIST: - e.gfm.ListLock(q.GetScope(), q.GetType()) - defer e.gfm.ListUnlock(q.GetScope(), q.GetType()) - case sdp.QueryMethod_SEARCH: - // We don't need to lock for a search since they are independent and - // will only ever have a cache hit if the query is identical - } - - // 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 - // it. - go func() { - <-ctx.Done() - if ctx.Err() != nil { - // get a fresh copy of the span to avoid data races - span := trace.SpanFromContext(ctx) - span.RecordError(ctx.Err()) - span.SetAttributes( - attribute.Bool("ovm.discover.hang", true), - ) - span.End() - } - }() - - // Set up handling for the items and errors that are returned before they - // are passed back to the caller - var numItems atomic.Int32 - var numErrs atomic.Int32 - var itemHandler ItemHandler = func(item *sdp.Item) { - if item == nil { - return - } - - if err := item.Validate(); err != nil { - span.RecordError(err) - responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ - UUID: q.GetUUID(), - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - Scope: q.GetScope(), - ResponderName: e.EngineConfig.SourceName, - ItemType: q.GetType(), - }) - return - } - - // Store metadata - item.Metadata = &sdp.Metadata{ - Timestamp: timestamppb.New(time.Now()), - SourceName: adapter.Name(), - SourceQuery: q, - } - - // Mark the item as hidden if the adapter is hidden - if hs, ok := adapter.(HiddenAdapter); ok { - item.Metadata.Hidden = hs.Hidden() - } - - // Send the item back to the caller - numItems.Add(1) - responses <- sdp.NewQueryResponseFromItem(item) - } - var errHandler ErrHandler = func(err error) { - if err == nil { - return - } - // add a recover to prevent panic from stream error handler. - defer tracing.LogRecoverToReturn(ctx, "StreamErrorHandler") - - // Record the error in the trace - span.RecordError(err, trace.WithStackTrace(true)) - - // Send the error back to the caller - numErrs.Add(1) - responses <- queryResponseFromError(err, q, adapter, e.EngineConfig.SourceName) - } - stream := NewQueryResultStream(itemHandler, errHandler) - - // Check that our context is okay before doing anything expensive - if ctx.Err() != nil { - span.RecordError(ctx.Err()) - - responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ - UUID: q.GetUUID(), - ErrorType: sdp.QueryError_OTHER, - ErrorString: ctx.Err().Error(), - Scope: q.GetScope(), - ResponderName: e.EngineConfig.SourceName, - ItemType: q.GetType(), - }) - return - } - - switch q.GetMethod() { - case sdp.QueryMethod_GET: - span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) - newItem, err := adapter.Get(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache()) - - if newItem != nil { - stream.SendItem(newItem) - } - if err != nil { - stream.SendError(err) - } - case sdp.QueryMethod_LIST: - if listStreamingAdapter, ok := adapter.(ListStreamableAdapter); ok { - // Prefer the streaming methods if they are available - span.SetAttributes(attribute.Bool("ovm.sdp.streaming", true)) - listStreamingAdapter.ListStream(ctx, q.GetScope(), q.GetIgnoreCache(), stream) - } else if listableAdapter, ok := adapter.(ListableAdapter); ok { - // Fall back to the non-streaming methods - span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) - resultItems, err := listableAdapter.List(ctx, q.GetScope(), q.GetIgnoreCache()) - - for _, i := range resultItems { - stream.SendItem(i) - } - if err != nil { - stream.SendError(err) - } - } else { - // Log the error instead of sending it over the stream - log.WithContext(ctx).WithFields(log.Fields{ - "ovm.adapter.name": adapter.Name(), - "ovm.sdp.type": q.GetType(), - "ovm.sdp.scope": q.GetScope(), - }).Warn("adapter is not listable") - } - case sdp.QueryMethod_SEARCH: - if searchStreamingAdapter, ok := adapter.(SearchStreamableAdapter); ok { - // Prefer the streaming methods if they are available - span.SetAttributes(attribute.Bool("ovm.sdp.streaming", true)) - searchStreamingAdapter.SearchStream(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache(), stream) - } else if searchableAdapter, ok := adapter.(SearchableAdapter); ok { - // Fall back to the non-streaming methods - span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) - resultItems, err := searchableAdapter.Search(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache()) - - for _, i := range resultItems { - stream.SendItem(i) - } - if err != nil { - stream.SendError(err) - } - } else { - // Log the error instead of sending it over the stream - log.WithContext(ctx).WithFields(log.Fields{ - "ovm.adapter.name": adapter.Name(), - "ovm.sdp.type": q.GetType(), - "ovm.sdp.scope": q.GetScope(), - }).Warn("adapter is not searchable") - } - } - - span.SetAttributes( - attribute.Int("ovm.adapter.numItems", int(numItems.Load())), - attribute.Int("ovm.adapter.numErrors", int(numErrs.Load())), - ) -} - -// queryResponseFromError converts an error into a QueryResponse. This takes -// care to not double-wrap `sdp.QueryError` errors. -func queryResponseFromError(err error, q *sdp.Query, adapter Adapter, sourceName string) *sdp.QueryResponse { - var sdpErr *sdp.QueryError - if !errors.As(err, &sdpErr) { - sdpErr = &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - } - } - - // Add details that might not be populated by the adapter - sdpErr.Scope = q.GetScope() - sdpErr.UUID = q.GetUUID() - sdpErr.SourceName = adapter.Name() - sdpErr.ItemType = adapter.Metadata().GetType() - sdpErr.ResponderName = sourceName - - return sdp.NewQueryResponseFromError(sdpErr) -} diff --git a/discovery/enginerequests_test.go b/discovery/enginerequests_test.go deleted file mode 100644 index 8dfb2787..00000000 --- a/discovery/enginerequests_test.go +++ /dev/null @@ -1,460 +0,0 @@ -package discovery - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/google/uuid" - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" - "github.com/sourcegraph/conc/pool" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// executeQuerySync Executes a Query, waiting for all results, then returns them -// along with the error, rather than using channels. The singular error sill only -// be returned if the query could not be executed, otherwise all errors will be -// in the slice -func (e *Engine) executeQuerySync(ctx context.Context, q *sdp.Query) ([]*sdp.Item, []*sdp.Edge, []*sdp.QueryError, error) { - responseChan := make(chan *sdp.QueryResponse, 100_000) - items := make([]*sdp.Item, 0) - edges := make([]*sdp.Edge, 0) - errs := make([]*sdp.QueryError, 0) - - err := e.ExecuteQuery(ctx, q, responseChan) - - for r := range responseChan { - switch r := r.GetResponseType().(type) { - case *sdp.QueryResponse_NewItem: - items = append(items, r.NewItem) - case *sdp.QueryResponse_Edge: - edges = append(edges, r.Edge) - case *sdp.QueryResponse_Error: - errs = append(errs, r.Error) - } - } - - return items, edges, errs, err -} - -func TestExecuteQuery(t *testing.T) { - adapter := TestAdapter{ - ReturnType: "person", - ReturnScopes: []string{"test"}, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestExecuteQuery", - &auth.NATSOptions{ - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - }, - nil, - &adapter, - ) - - 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", - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 3, - }, - } - - items, _, errs, err := e.executeQuerySync(context.Background(), q) - if err != nil { - t.Error(err) - } - - for _, e := range errs { - t.Error(e) - } - - if x := len(adapter.GetCalls); x != 1 { - t.Errorf("expected adapter's Get() to have been called 1 time, got %v", x) - } - - if len(items) == 0 { - t.Fatal("expected 1 item, got none") - } - - if len(items) > 1 { - t.Errorf("expected 1 item, got %v", items) - } - - 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") - } - }) - - t.Run("Wrong scope Get query", func(t *testing.T) { - q := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "foo", - Scope: "wrong", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), q) - - if err == nil { - t.Error("expected error but got nil") - } - - if len(errs) == 1 { - if errs[0].GetErrorType() != sdp.QueryError_NOSCOPE { - t.Errorf("expected error type to be NOSCOPE, got %v", errs[0].GetErrorType()) - } - } else { - t.Errorf("expected 1 error, got %v", len(errs)) - } - }) - - t.Run("Wrong type Get query", func(t *testing.T) { - q := &sdp.Query{ - Type: "house", - Method: sdp.QueryMethod_GET, - Query: "foo", - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - } - - _, _, errs, err := e.executeQuerySync(context.Background(), q) - - if err == nil { - t.Error("expected error but got nil") - } - - if len(errs) == 1 { - if errs[0].GetErrorType() != sdp.QueryError_NOSCOPE { - t.Errorf("expected error type to be NOSCOPE, got %v", errs[0].GetErrorType()) - } - } else { - t.Errorf("expected 1 error, got %v", len(errs)) - } - }) - - t.Run("Basic List query", func(t *testing.T) { - q := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_LIST, - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 5, - }, - } - - items, _, errs, err := e.executeQuerySync(context.Background(), q) - if err != nil { - t.Error(err) - } - - for _, e := range errs { - t.Error(e) - } - - if len(items) < 1 { - t.Error("expected at least one item") - } - }) - - t.Run("Basic Search query", func(t *testing.T) { - q := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_SEARCH, - Query: "TEST", - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 5, - }, - } - - items, _, errs, err := e.executeQuerySync(context.Background(), q) - if err != nil { - t.Error(err) - } - - for _, e := range errs { - t.Error(e) - } - - if len(items) < 1 { - t.Error("expected at least one item") - } - }) -} - -func TestHandleQuery(t *testing.T) { - personAdapter := TestAdapter{ - ReturnType: "person", - ReturnScopes: []string{ - "test1", - "test2", - }, - cache: sdpcache.NewNoOpCache(), - } - - dogAdapter := TestAdapter{ - ReturnType: "dog", - ReturnScopes: []string{ - "test1", - "testA", - "testB", - }, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestHandleQuery", nil, nil, &personAdapter, &dogAdapter) - - t.Run("Wildcard type should be expanded", func(t *testing.T) { - t.Cleanup(func() { - personAdapter.ClearCalls() - dogAdapter.ClearCalls() - }) - - req := sdp.Query{ - Type: sdp.WILDCARD, - Method: sdp.QueryMethod_GET, - Query: "Dylan", - Scope: "test1", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - } - - // Run the handler - e.HandleQuery(context.Background(), &req) - - // I'm expecting both adapter to get a query since the type was * - if l := len(personAdapter.GetCalls); l != 1 { - t.Errorf("expected person backend to have 1 Get call, got %v", l) - } - - if l := len(dogAdapter.GetCalls); l != 1 { - t.Errorf("expected dog backend to have 1 Get call, got %v", l) - } - }) - - t.Run("Wildcard scope should be expanded", func(t *testing.T) { - t.Cleanup(func() { - personAdapter.ClearCalls() - dogAdapter.ClearCalls() - }) - - req := sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan1", - Scope: sdp.WILDCARD, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - } - - // Run the handler - e.HandleQuery(context.Background(), &req) - - if l := len(personAdapter.GetCalls); l != 2 { - t.Errorf("expected person backend to have 2 Get calls, got %v", l) - } - - if l := len(dogAdapter.GetCalls); l != 0 { - t.Errorf("expected dog backend to have 0 Get calls, got %v", l) - } - }) -} - -func TestWildcardAdapterExpansion(t *testing.T) { - personAdapter := TestAdapter{ - ReturnType: "person", - ReturnScopes: []string{ - sdp.WILDCARD, - }, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestWildcardAdapterExpansion", nil, nil, &personAdapter) - - t.Run("query scope should be preserved", func(t *testing.T) { - req := sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan1", - Scope: "something.specific", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - } - - // Run the handler - e.HandleQuery(context.Background(), &req) - - if len(personAdapter.GetCalls) != 1 { - t.Errorf("expected 1 get call got %v", len(personAdapter.GetCalls)) - } - - if len(personAdapter.GetCalls) == 0 { - t.Fatal("Can't continue without calls") - } - - call := personAdapter.GetCalls[0] - - if expected := "something.specific"; call[0] != expected { - t.Errorf("expected scope to be %v, got %v", expected, call[0]) - } - - if expected := "Dylan1"; call[1] != expected { - t.Errorf("expected query to be %v, got %v", expected, call[1]) - } - }) -} - -func TestSendQuerySync(t *testing.T) { - SkipWithoutNats(t) - - ctx := context.Background() - - ctx, span := tracing.Tracer().Start(ctx, "TestSendQuerySync") - defer span.End() - - adapter := TestAdapter{ - ReturnType: "person", - ReturnScopes: []string{ - "test", - }, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestSendQuerySync", nil, nil, &adapter) - - p := pool.New() - for range 250 { - p.Go(func() { - u := uuid.New() - t.Log("starting query: ", u) - - var items []*sdp.Item - - query := &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan", - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - IgnoreCache: false, - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(10 * time.Minute)), - } - - items, _, errs, err := sdp.RunSourceQuerySync(ctx, query, 1*time.Second, e.natsConnection) - if err != nil { - t.Error(err) - } - - if len(errs) != 0 { - for _, err := range errs { - t.Error(err) - } - } - - if len(items) != 1 { - t.Fatalf("expected 1 item, got %v: %v", len(items), items) - } - }) - } - - p.Wait() -} - -func TestExpandQuery(t *testing.T) { - t.Run("with a single adapter with a single scope", func(t *testing.T) { - simple := TestAdapter{ - ReturnScopes: []string{ - "test1", - }, - cache: sdpcache.NewNoOpCache(), - } - e := newStartedEngine(t, "TestExpandQuery", nil, nil, &simple) - - e.HandleQuery(context.Background(), &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Debby", - Scope: "*", - }) - - if expected := 1; len(simple.GetCalls) != expected { - t.Errorf("Expected %v calls, got %v", expected, len(simple.GetCalls)) - } - }) - - t.Run("with a single adapter with many scopes", func(t *testing.T) { - many := TestAdapter{ - ReturnName: "many", - ReturnScopes: []string{ - "test1", - "test2", - "test3", - }, - cache: sdpcache.NewNoOpCache(), - } - e := newStartedEngine(t, "TestExpandQuery", nil, nil, &many) - - e.HandleQuery(context.Background(), &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Debby", - Scope: "*", - }) - - if expected := 3; len(many.GetCalls) != expected { - t.Errorf("Expected %v calls, got %v", expected, many.GetCalls) - } - }) - - t.Run("with a single wildcard adapter", func(t *testing.T) { - sx := TestAdapter{ - ReturnType: "person", - ReturnName: "sx", - ReturnScopes: []string{ - sdp.WILDCARD, - }, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestExpandQuery", nil, nil, &sx) - - e.HandleQuery(context.Background(), &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_LIST, - Query: "Rachel", - Scope: "*", - }) - - if expected := 1; len(sx.ListCalls) != expected { - t.Errorf("Expected %v calls, got %v", expected, sx.ListCalls) - } - }) -} diff --git a/discovery/getfindmutex.go b/discovery/getfindmutex.go deleted file mode 100644 index d7f26602..00000000 --- a/discovery/getfindmutex.go +++ /dev/null @@ -1,80 +0,0 @@ -package discovery - -import ( - "fmt" - "sync" -) - -// GetListMutex A modified version of a RWMutex. Many get locks can be held but -// only one List lock. A waiting List lock (even if it hasn't been locked, just -// if someone is waiting) blocks all other get locks until it unlocks. -// -// The intended usage of this is that it will allow an adapter which is trying to -// process many queries at once, to process a LIST query before any GET -// queries, since it's likely that once LIST has been run, subsequent GET -// queries will be able to be served from cache -type GetListMutex struct { - mutexMap map[string]*sync.RWMutex - mapLock sync.Mutex -} - -// GetLock Gets a lock that can be held by an unlimited number of goroutines, -// these locks are only blocked by ListLocks. A type and scope must be -// provided since a Get in one type (or scope) should not be blocked by a List -// in another -func (g *GetListMutex) GetLock(scope string, typ string) { - g.mutexFor(scope, typ).RLock() -} - -// GetUnlock Unlocks the GetLock. This must be called once for each GetLock -// otherwise it will be impossible to ever obtain a ListLock -func (g *GetListMutex) GetUnlock(scope string, typ string) { - g.mutexFor(scope, typ).RUnlock() -} - -// ListLock An exclusive lock. Ensure that all GetLocks have been unlocked and -// stops any more from being obtained. Provide a type and scope to ensure that -// the lock is only help for that type and scope combination rather than -// locking the whole engine -func (g *GetListMutex) ListLock(scope string, typ string) { - g.mutexFor(scope, typ).Lock() -} - -// ListUnlock Unlocks a ListLock -func (g *GetListMutex) ListUnlock(scope string, typ string) { - g.mutexFor(scope, typ).Unlock() -} - -// mutexFor Returns the relevant RWMutex for a given scope and type, creating -// and storing a new one if needed -func (g *GetListMutex) mutexFor(scope string, typ string) *sync.RWMutex { - var mutex *sync.RWMutex - var ok bool - - keyName := g.keyName(scope, typ) - - g.mapLock.Lock() - defer g.mapLock.Unlock() - - // Create the map if needed - if g.mutexMap == nil { - g.mutexMap = make(map[string]*sync.RWMutex) - } - - // Get the mutex from storage - mutex, ok = g.mutexMap[keyName] - - // If the mutex wasn't found for this key, create a new one - if !ok { - mutex = &sync.RWMutex{} - g.mutexMap[keyName] = mutex - } - - return mutex -} - -// keyName Returns the name of the key for a given scope and type combo for -// use with the mutexMap -func (g *GetListMutex) keyName(scope string, typ string) string { - return fmt.Sprintf("%v.%v", scope, typ) -} diff --git a/discovery/getfindmutex_test.go b/discovery/getfindmutex_test.go deleted file mode 100644 index ff7b4916..00000000 --- a/discovery/getfindmutex_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package discovery - -import ( - "context" - "sync" - "testing" - "time" -) - -func TestGetLock(t *testing.T) { - t.Run("many get locks can be held at once", func(t *testing.T) { - var gfm GetListMutex - ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) - doneChan := make(chan bool) - - go func() { - gfm.GetLock("testScope", "testType") - gfm.GetLock("testScope", "testType") - gfm.GetLock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - doneChan <- true - }() - - select { - case <-ctx.Done(): - t.Error("Timeout") - case <-doneChan: - } - - cancel() - }) - - t.Run("many find locks from different types and scopes can be held at once", func(t *testing.T) { - var gfm GetListMutex - ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) - doneChan := make(chan bool) - - go func() { - gfm.ListLock("testScope1", "testType1") - gfm.ListLock("testScope1", "testType2") - gfm.ListLock("testScope2", "testType") - gfm.ListLock("testScope3", "testType") - gfm.ListUnlock("testScope1", "testType1") - gfm.ListUnlock("testScope1", "testType2") - gfm.ListUnlock("testScope2", "testType") - gfm.ListUnlock("testScope3", "testType") - doneChan <- true - }() - - select { - case <-ctx.Done(): - t.Error("Timeout") - case <-doneChan: - } - - cancel() - }) - - t.Run("get locks are blocked by a find lock", func(t *testing.T) { - var gfm GetListMutex - ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) - getChan := make(chan bool) - findChan := make(chan bool) - - gfm.ListLock("testScope", "testType") - - go func() { - gfm.GetLock("testScope", "testType") - gfm.GetLock("testScope", "testType") - gfm.GetLock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - gfm.GetUnlock("testScope", "testType") - getChan <- true - }() - - go func() { - // Seep for long enough to allow the above goroutine to complete if not - // blocked - time.Sleep(10 * time.Millisecond) - - findChan <- true - }() - - select { - case <-ctx.Done(): - t.Error("Timeout") - case <-getChan: - t.Error("Get locks were not blocked") - case <-findChan: - // This is the expected path - } - - cancel() - }) - - t.Run("active gets block finds", func(t *testing.T) { - var gfm GetListMutex - var actionWG sync.WaitGroup - ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) - - order := make([]string, 0) - actionChan := make(chan string) - doneChan := make(chan bool) - var wg sync.WaitGroup - wg.Add(3) - - go func() { - defer wg.Done() - gfm.GetLock("testScope", "testType") - actionChan <- "getLock1" - - // do some work - time.Sleep(50 * time.Millisecond) - - gfm.GetUnlock("testScope", "testType") - - }() - - go func() { - defer wg.Done() - time.Sleep(10 * time.Millisecond) - - gfm.ListLock("testScope", "testType") - - actionChan <- "findLock1" - - // do some work - time.Sleep(50 * time.Millisecond) - - gfm.ListUnlock("testScope", "testType") - - }() - - go func() { - defer wg.Done() - time.Sleep(20 * time.Millisecond) - - gfm.GetLock("testScope", "testType") - - actionChan <- "getLock2" - - // do some work - time.Sleep(50 * time.Millisecond) - - gfm.GetUnlock("testScope", "testType") - - }() - - actionWG.Add(1) - - go func() { - for action := range actionChan { - order = append(order, action) - } - actionWG.Done() - }() - - go func(t *testing.T) { - wg.Wait() - close(actionChan) - actionWG.Wait() - - // The expected order is: Firstly getLock1 since nothing else is waiting - // for a lock. While this one is working there is a query for a - // findLock, then a getLock. The findLock should block the getLock until - // it is done - if order[0] != "getLock1" { - t.Errorf("expected getLock1 to be first. Order was: %v", order) - } - - if order[1] != "findLock1" { - t.Errorf("expected findLock1 to be middle. Order was: %v", order) - } - - if order[2] != "getLock2" { - t.Errorf("expected getLock2 to be last. Order was: %v", order) - } - - doneChan <- true - }(t) - - select { - case <-ctx.Done(): - t.Errorf("timeout. Completed actions were: %v", order) - case <-doneChan: - // This is good - } - - cancel() - }) -} diff --git a/discovery/heartbeat.go b/discovery/heartbeat.go deleted file mode 100644 index ee7563ad..00000000 --- a/discovery/heartbeat.go +++ /dev/null @@ -1,165 +0,0 @@ -package discovery - -import ( - "context" - "errors" - "time" - - "connectrpc.com/connect" - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-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" -) - -const DefaultHeartbeatFrequency = 5 * time.Minute - -var ErrNoHealthcheckDefined = errors.New("no healthcheck defined") - -// HeartbeatSender sends a heartbeat to the management API, this is called at -// `DefaultHeartbeatFrequency` by default when the engine is running, or -// `StartSendingHeartbeats` has been called manually. Users can also call this -// method to immediately send a heartbeat if required. Pass non-`nil` error -// to indicate that the engine is in an error state, this will be sent to the -// management API and will be displayed in the UI. -func (e *Engine) SendHeartbeat(ctx context.Context, customErr error) error { - // Get span from context - span := trace.SpanFromContext(ctx) - - // Read memory stats and add them to the span - memStats := tracing.ReadMemoryStats() - tracing.SetMemoryAttributes(span, "ovm.heartbeat", memStats) - span.SetAttributes( - 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), - ) - - if e.EngineConfig.HeartbeatOptions == nil { - return ErrNoHealthcheckDefined - } - - // No-op when running without management API (e.g. ALLOW_UNAUTHENTICATED local dev) - if e.EngineConfig.HeartbeatOptions.ManagementClient == nil { - log.WithFields(log.Fields{ - "source_name": e.EngineConfig.SourceName, - "engine_type": e.EngineConfig.EngineType, - }).Info("Running in unauthenticated mode; no heartbeats will be sent") - return nil - } - - // Collect all health check errors - var allErrors []error - if customErr != nil { - allErrors = append(allErrors, customErr) - } - - // Check for persistent initialization errors first - if initErr := e.GetInitError(); initErr != nil { - allErrors = append(allErrors, initErr) - } - - // Check adapter readiness (ReadinessCheck) - with timeout to prevent hanging - if e.EngineConfig.HeartbeatOptions.ReadinessCheck != nil { - // Add timeout for readiness checks to prevent hanging heartbeats - readinessCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - if err := e.EngineConfig.HeartbeatOptions.ReadinessCheck(readinessCtx); err != nil { - allErrors = append(allErrors, err) - } - } - - // Combine all errors - var heartbeatError *string - if len(allErrors) > 0 { - combinedError := errors.Join(allErrors...) - heartbeatError = new(string) - *heartbeatError = combinedError.Error() - } - - var engineUUID []byte - - if e.EngineConfig.SourceUUID != uuid.Nil { - engineUUID = e.EngineConfig.SourceUUID[:] - } - - availableScopes, adapterMetadata := e.GetAvailableScopesAndMetadata() - - // Calculate the duration for the next heartbeat, based on the current - // frequency x2.5 to give us some leeway - nextHeartbeat := time.Duration(float64(e.EngineConfig.HeartbeatOptions.Frequency) * 2.5) - - _, err := e.EngineConfig.HeartbeatOptions.ManagementClient.SubmitSourceHeartbeat(ctx, &connect.Request[sdp.SubmitSourceHeartbeatRequest]{ - Msg: &sdp.SubmitSourceHeartbeatRequest{ - UUID: engineUUID, - Version: e.EngineConfig.Version, - Name: e.EngineConfig.SourceName, - Type: e.EngineConfig.EngineType, - AvailableScopes: availableScopes, - AdapterMetadata: adapterMetadata, - Managed: e.EngineConfig.OvermindManagedSource, - Error: heartbeatError, - NextHeartbeatMax: durationpb.New(nextHeartbeat), - }, - }) - - // Update heartbeat status tracking - e.heartbeatStatusMutex.Lock() - if err != nil { - e.lastHeartbeatError = err - } else { - e.lastSuccessfulHeartbeat = time.Now() - e.lastHeartbeatError = nil - } - e.heartbeatStatusMutex.Unlock() - - return err -} - -// Starts sending heartbeats at the specified frequency. These will be sent in -// the background and this function will return immediately. Heartbeats are -// automatically started when the engine started, but if an adapter has startup -// steps that take a long time, or are liable to fail, the user may want to -// start the heartbeats first so that users can see that the adapter has failed -// to start. -// -// If this is called multiple times, nothing will happen. Heartbeats will be -// stopped when the engine is stopped, or when the provided context is canceled. -// -// This will send one heartbeat initially when the method is called, and will -// then run in a background goroutine that sends heartbeats at the specified -// frequency, and will stop when the provided context is canceled. -func (e *Engine) StartSendingHeartbeats(ctx context.Context) { - if e.EngineConfig.HeartbeatOptions == nil || e.EngineConfig.HeartbeatOptions.Frequency == 0 || e.heartbeatCancel != nil { - return - } - - var heartbeatContext context.Context - heartbeatContext, e.heartbeatCancel = context.WithCancel(ctx) - - // Send one heartbeat at the beginning - err := e.SendHeartbeat(heartbeatContext, nil) - if err != nil { - log.WithError(err).Error("Failed to send heartbeat") - } - - go func() { - ticker := time.NewTicker(e.EngineConfig.HeartbeatOptions.Frequency) - defer ticker.Stop() - - for { - select { - case <-heartbeatContext.Done(): - return - case <-ticker.C: - err := e.SendHeartbeat(heartbeatContext, nil) - if err != nil { - log.WithError(err).Error("Failed to send heartbeat") - } - } - } - }() -} diff --git a/discovery/heartbeat_test.go b/discovery/heartbeat_test.go deleted file mode 100644 index 74b79e0b..00000000 --- a/discovery/heartbeat_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package discovery - -import ( - "context" - "slices" - "testing" - "time" - - "connectrpc.com/connect" - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" -) - -type testHeartbeatClient struct { - // Requests will be sent to this channel - Requests chan *connect.Request[sdp.SubmitSourceHeartbeatRequest] - // Responses should be sent here - Responses chan *connect.Response[sdp.SubmitSourceHeartbeatResponse] -} - -func (t testHeartbeatClient) SubmitSourceHeartbeat(ctx context.Context, req *connect.Request[sdp.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp.SubmitSourceHeartbeatResponse], error) { - t.Requests <- req - return <-t.Responses, nil -} - -func TestHeartbeats(t *testing.T) { - name := t.Name() - u := uuid.New() - version := "v0.0.0-test" - engineType := "aws" - - requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 1) - responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 1) - - heartbeatOptions := HeartbeatOptions{ - ManagementClient: testHeartbeatClient{ - Requests: requests, - Responses: responses, - }, - } - ec := EngineConfig{ - SourceName: name, - SourceUUID: u, - Version: version, - EngineType: engineType, - HeartbeatOptions: &heartbeatOptions, - } - e, _ := NewEngine(&ec) - - if err := e.AddAdapters( - &TestAdapter{ - ReturnScopes: []string{"test"}, - ReturnType: "test-type", - ReturnName: "test-name", - }, - &TestAdapter{ - ReturnScopes: []string{"test"}, - ReturnType: "test-type2", - ReturnName: "test-name2", - }, - ); err != nil { - t.Fatalf("unexpected error adding adapters: %v", err) - } - - t.Run("sendHeartbeat when healthy", func(t *testing.T) { - ec.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { - return nil - } - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - - err := e.SendHeartbeat(context.Background(), nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - req := <-requests - - if reqUUID, err := uuid.FromBytes(req.Msg.GetUUID()); err == nil { - if reqUUID != u { - t.Errorf("expected uuid %v, got %v", u, reqUUID) - } - } else { - t.Errorf("error parsing uuid: %v", err) - } - - if req.Msg.GetVersion() != version { - t.Errorf("expected version %v, got %v", version, req.Msg.GetVersion()) - } - - if req.Msg.GetName() != name { - t.Errorf("expected name %v, got %v", name, req.Msg.GetName()) - } - - if req.Msg.GetType() != engineType { - t.Errorf("expected type %v, got %v", engineType, req.Msg.GetType()) - } - - if req.Msg.GetManaged() != sdp.SourceManaged_LOCAL { - t.Errorf("expected managed %v, got %v", sdp.SourceManaged_LOCAL, req.Msg.GetManaged()) - } - - if req.Msg.GetError() != "" { - t.Errorf("expected no error, got %v", req.Msg.GetError()) - } - - reqAvailableScopes := req.Msg.GetAvailableScopes() - - if len(reqAvailableScopes) != 1 { - t.Errorf("expected 1 scope, got %v", len(reqAvailableScopes)) - } - - if !slices.Contains(reqAvailableScopes, "test") { - t.Errorf("expected scope 'test' to be present in the response") - } - - reqAdapterMetadata := req.Msg.GetAdapterMetadata() - - if len(reqAdapterMetadata) != 2 { - t.Errorf("expected 2 adapter metadata, got %v", len(reqAdapterMetadata)) - } - }) - - t.Run("sendHeartbeat when unhealthy", func(t *testing.T) { - e.EngineConfig.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { - return ErrNoHealthcheckDefined - } - - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - - err := e.SendHeartbeat(context.Background(), nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - req := <-requests - - // Error message is no longer wrapped (wrapping removed to avoid double-prefixing) - expectedError := "no healthcheck defined" - if req.Msg.GetError() != expectedError { - t.Errorf("expected error %q, got %q", expectedError, req.Msg.GetError()) - } - }) - - t.Run("startSendingHeartbeats", func(t *testing.T) { - e.EngineConfig.HeartbeatOptions.Frequency = time.Millisecond * 250 - e.EngineConfig.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { - return nil - } - - ctx, cancel := context.WithCancel(context.Background()) - - start := time.Now() - - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - e.StartSendingHeartbeats(ctx) - - // Get the initial heartbeat - <-requests - - // Get two - responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ - Msg: &sdp.SubmitSourceHeartbeatResponse{}, - } - <-requests - - cancel() - - // Make sure that took the expected amount of time - if elapsed := time.Since(start); elapsed < time.Millisecond*250 { - t.Errorf("expected to take at least 500ms, took %v", elapsed) - } - - if elapsed := time.Since(start); elapsed > time.Millisecond*500 { - t.Errorf("expected to take at most 750ms, took %v", elapsed) - } - }) -} - -// TestSendHeartbeatNilManagementClient ensures unauthenticated/local dev mode -// (HeartbeatOptions set by SetReadinessCheck but ManagementClient nil) does not error. -func TestSendHeartbeatNilManagementClient(t *testing.T) { - ec := EngineConfig{ - SourceName: t.Name(), - SourceUUID: uuid.New(), - Version: "v0.0.0-test", - EngineType: "aws", - Unauthenticated: true, - HeartbeatOptions: &HeartbeatOptions{ - ManagementClient: nil, // e.g. ALLOW_UNAUTHENTICATED - no API to send to - Frequency: time.Second * 30, - }, - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("NewEngine: %v", err) - } - err = e.SendHeartbeat(context.Background(), nil) - if err != nil { - t.Errorf("SendHeartbeat with nil ManagementClient should be no-op, got: %v", err) - } -} diff --git a/discovery/item_tests.go b/discovery/item_tests.go deleted file mode 100644 index 6627154d..00000000 --- a/discovery/item_tests.go +++ /dev/null @@ -1,100 +0,0 @@ -// Reusable testing libraries for testing adapters -package discovery - -import ( - "regexp" - "testing" - - "github.com/overmindtech/cli/sdp-go" -) - -var RFC1123 = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) - -// TestValidateItem Checks an item to ensure it is a valid SDP item. This includes -// checking that all required attributes are populated -func TestValidateItem(t *testing.T, i *sdp.Item) { - // Ensure that the item has the required fields set i.e. - // - // * Type - // * UniqueAttribute - // * Attributes - if i.GetType() == "" { - t.Errorf("Item %v has an empty Type", i.GloballyUniqueName()) - } - - // Validate that the pattern is RFC1123 - if !RFC1123.MatchString(i.GetType()) { - pattern := ` -Type names should match RFC1123 (lower case). This means the name must: - - * contain at most 63 characters - * contain only lowercase alphanumeric characters or '-' - * start with an alphanumeric character - * end with an alphanumeric character -` - - t.Errorf("Item type %v is invalid. %v", i.GetType(), pattern) - } - - if i.GetUniqueAttribute() == "" { - t.Errorf("Item %v has an empty UniqueAttribute", i.GloballyUniqueName()) - } - - attrMap := i.GetAttributes().GetAttrStruct().AsMap() - - if len(attrMap) == 0 { - t.Errorf("Attributes for item %v are empty", i.GloballyUniqueName()) - } - - // Check the attributes themselves for validity - for k := range attrMap { - if k == "" { - t.Errorf("Item %v has an attribute with an empty name", i.GloballyUniqueName()) - } - } - - // Make sure that the UniqueAttributeValue is populated - if i.UniqueAttributeValue() == "" { - t.Errorf("UniqueAttribute %v for item %v is empty", i.GetUniqueAttribute(), i.GloballyUniqueName()) - } - - // TODO(LIQs): delete this - for index, linkedItem := range i.GetLinkedItems() { - item := linkedItem.GetItem() - if item.GetType() == "" { - t.Errorf("LinkedItem %v of item %v has empty type", index, i.GloballyUniqueName()) - } - - if item.GetUniqueAttributeValue() == "" { - t.Errorf("LinkedItem %v of item %v has empty UniqueAttributeValue", index, i.GloballyUniqueName()) - } - - // We don't need to check for an empty scope here since if it's empty - // it will just inherit the scope of the parent - } - - // TODO(LIQs): delete this - for index, linkedItemQuery := range i.GetLinkedItemQueries() { - query := linkedItemQuery.GetQuery() - if query.GetType() == "" { - t.Errorf("LinkedItemQueries %v of item %v has empty type", index, i.GloballyUniqueName()) - } - - if query.GetMethod() != sdp.QueryMethod_LIST { - if query.GetQuery() == "" { - t.Errorf("LinkedItemQueries %v of item %v has empty query. This is not allowed unless the method is LIST", index, i.GloballyUniqueName()) - } - } - - if query.GetScope() == "" { - t.Errorf("LinkedItemQueries %v of item %v has empty scope", index, i.GloballyUniqueName()) - } - } -} - -// TestValidateItems Runs TestValidateItem on many items -func TestValidateItems(t *testing.T, is []*sdp.Item) { - for _, i := range is { - TestValidateItem(t, i) - } -} diff --git a/discovery/logs.go b/discovery/logs.go deleted file mode 100644 index ebb46d09..00000000 --- a/discovery/logs.go +++ /dev/null @@ -1,102 +0,0 @@ -package discovery - -import ( - "context" - "errors" - - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" -) - -// LogAdapter is a singleton from the source that handles GetLogRecordsRequest -// that come in via NATS. The discovery Engine takes care of the common -// implementation details like subscribing to NATS, unpacking the request, -// framing the responses, and error handling. Implementors only need to pass -// results into the LogRecordsStream. -type LogAdapter interface { - // Get gets called when a GetLogRecordsRequest needs to be processed. To - // return data to the requestor, use the provided `stream` to send - // `GetLogRecordsResponse` messages back. - // - // If the implementation encounters an error, it should return the error as - // `SourceError`. To indicate that the error is within the source, set the - // `SourceError.Upstream` field to `false`. To indicate that the error is - // with the upstream API, set the `SourceError.Upstream` field to `true`. - // Always make sure that the error detail is set to a human-readable string - // that is helpful for debugging. - // - // Implementations must not hold on to or share the `stream` object outside - // of the scope of a single call. - // - // Concurrency: Every invocation of this method will happen in its own - // goroutine, so implementors need to take care of ensuring thread safety. - // - // Cancellation: The context passed to this method will be cancelled when - // any errors are encountered, like the NATS connection closing, the - // requestor going away, or hitting a deadline. Implementations are expected - // to timely detect the cancellation and clean up on the way out. After - // `ctx` is cancelled, the implementation should not attempt to send any - // more messages to the stream. - Get(ctx context.Context, req *sdp.GetLogRecordsRequest, stream LogRecordsStream) error - - // Scopes returns all scopes this adapter is capable of handling. This is - // used by the Engine to subscribe to the correct subjects. The Engine will - // only call this method once, so implementors don't need to cache the - // result. - Scopes() []string -} - -type LogRecordsStream interface { - // Send takes a GetLogRecordsResponse, and forwards it to the caller over - // NATS. Note that the order of responses is relevant and will be preserved. - // - // Errors returned from this method should be treated as fatal, and the - // stream should be closed. The caller should not attempt to send any more - // messages after this method returns an error. Basically, treat this like a - // context cancellation on the `LogAdapter.Get` method. - // - // Concurrency: This method is not thread safe. The caller needs to ensure - // that There is only one call of Send active at any time. - Send(ctx context.Context, r *sdp.GetLogRecordsResponse) error -} - -type LogRecordsStreamImpl struct { - // The NATS stream that is used to send messages - stream sdp.EncodedConnection - // The NATS subject that is used to send messages - subject string - // responder has gone away - responderGone bool - - responses int - records int -} - -// assert interface implementation -var _ LogRecordsStream = (*LogRecordsStreamImpl)(nil) - -func (s *LogRecordsStreamImpl) Send(ctx context.Context, r *sdp.GetLogRecordsResponse) error { - // immediately return if the gateway is gone - if s.responderGone { - return nats.ErrNoResponders - } - - s.responses += 1 - s.records += len(r.GetRecords()) - - // Send the message to the NATS stream - err := s.stream.Publish(ctx, s.subject, &sdp.NATSGetLogRecordsResponse{ - Content: &sdp.NATSGetLogRecordsResponse_Response{ - Response: r, - }, - }) - if errors.Is(err, nats.ErrNoResponders) { - s.responderGone = true - return err - } - if err != nil { - return err - } - - return nil -} diff --git a/discovery/logs_test.go b/discovery/logs_test.go deleted file mode 100644 index 33c0d00d..00000000 --- a/discovery/logs_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package discovery - -import ( - "context" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" - "google.golang.org/protobuf/types/known/timestamppb" -) - -type testLogAdapter struct { - t *testing.T - expected *sdp.GetLogRecordsRequest - responses []*sdp.GetLogRecordsResponse - err error -} - -// assert interface implementation -var _ LogAdapter = (*testLogAdapter)(nil) - -func (t *testLogAdapter) Get(ctx context.Context, request *sdp.GetLogRecordsRequest, stream LogRecordsStream) error { - if t.expected == nil { - t.t.Fatalf("expected LogAdapter to not get called, but got %v", request) - } - if t.expected.GetScope() != request.GetScope() { - t.t.Errorf("expected scope %s but got %s", t.expected.GetScope(), request.GetScope()) - } - if t.expected.GetQuery() != request.GetQuery() { - t.t.Errorf("expected query %s but got %s", t.expected.GetQuery(), request.GetQuery()) - } - // Compare timestamp values correctly - if (t.expected.GetFrom() == nil) != (request.GetFrom() == nil) { - t.t.Errorf("timestamp nullability mismatch: expected from is nil: %v, got from is nil: %v", t.expected.GetFrom() == nil, request.GetFrom() == nil) - } else if t.expected.GetFrom() != nil && !t.expected.GetFrom().AsTime().Equal(request.GetFrom().AsTime()) { - t.t.Errorf("expected from %s but got %s", t.expected.GetFrom().AsTime(), request.GetFrom().AsTime()) - } - - if (t.expected.GetTo() == nil) != (request.GetTo() == nil) { - t.t.Errorf("timestamp nullability mismatch: expected to is nil: %v, got to is nil: %v", t.expected.GetTo() == nil, request.GetTo() == nil) - } else if t.expected.GetTo() != nil && !t.expected.GetTo().AsTime().Equal(request.GetTo().AsTime()) { - t.t.Errorf("expected to %s but got %s", t.expected.GetTo().AsTime(), request.GetTo().AsTime()) - } - if t.expected.GetMaxRecords() != request.GetMaxRecords() { - t.t.Errorf("expected maxRecords %d but got %d", t.expected.GetMaxRecords(), request.GetMaxRecords()) - } - if t.expected.GetStartFromOldest() != request.GetStartFromOldest() { - t.t.Errorf("expected startFromOldest %v but got %v", t.expected.GetStartFromOldest(), request.GetStartFromOldest()) - } - - for _, r := range t.responses { - err := stream.Send(ctx, r) - if err != nil { - return err - } - } - return t.err -} - -func (t *testLogAdapter) Scopes() []string { - return []string{"test"} -} - -func TestLogAdapter_HappyPath(t *testing.T) { - t.Parallel() - - ts := timestamppb.Now() - tla := &testLogAdapter{ - t: t, - expected: &sdp.GetLogRecordsRequest{ - Scope: "test", - Query: "test", - From: ts, - To: ts, - MaxRecords: 10, - StartFromOldest: false, - }, - responses: []*sdp.GetLogRecordsResponse{ - { - Records: []*sdp.LogRecord{ - { - CreatedAt: timestamppb.Now(), - ObservedAt: timestamppb.Now(), - Severity: sdp.LogSeverity_INFO, - Body: "page1/record1", - }, - { - CreatedAt: timestamppb.Now(), - ObservedAt: timestamppb.Now(), - Severity: sdp.LogSeverity_INFO, - Body: "page1/record2", - }, - }, - }, - { - Records: []*sdp.LogRecord{ - { - CreatedAt: timestamppb.Now(), - ObservedAt: timestamppb.Now(), - Severity: sdp.LogSeverity_INFO, - Body: "page2/record1", - }, - { - CreatedAt: timestamppb.Now(), - ObservedAt: timestamppb.Now(), - Severity: sdp.LogSeverity_INFO, - Body: "page2/record2", - }, - }, - }, - }, - } - - tc := &sdp.TestConnection{ - Messages: make([]sdp.ResponseMessage, 0), - } - - e := newEngine(t, "logs.happyPath", nil, tc) - if e == nil { - t.Fatal("failed to create engine") - } - - err := e.SetLogAdapter(tla) - if err != nil { - t.Fatal(err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = e.Stop() - }() - - _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( - "", - func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { - t.Log("Received message:", msg) - }, - )) - - err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ - Request: &sdp.GetLogRecordsRequest{ - Scope: "test", - Query: "test", - From: ts, - To: ts, - MaxRecords: 10, - StartFromOldest: false, - }, - }) - if err != nil { - t.Log("Subscriptions:", tc.Subscriptions) - t.Fatal(err) - } - - // TODO: properly sync the test to wait for the messages to be sent - time.Sleep(1 * time.Second) - - tc.MessagesMu.Lock() - defer tc.MessagesMu.Unlock() - - if len(tc.Messages) != 5 { - t.Fatalf("expected 5 messages but got %d: %v", len(tc.Messages), tc.Messages) - } - - started := tc.Messages[1] - if started.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_STARTED { - t.Errorf("expected status STARTED but got %v", started.V) - } - - page1 := tc.Messages[2] - records := page1.V.(*sdp.NATSGetLogRecordsResponse).GetResponse().GetRecords() - if len(records) != 2 { - t.Errorf("expected 2 records but got %d: %v", len(records), records) - } - if records[0].GetBody() != "page1/record1" { - t.Errorf("expected page1/record1 but got %v", page1.V) - } - - page2 := tc.Messages[3] - records = page2.V.(*sdp.NATSGetLogRecordsResponse).GetResponse().GetRecords() - if len(records) != 2 { - t.Errorf("expected 2 records but got %d: %v", len(records), records) - } - if records[0].GetBody() != "page2/record1" { - t.Errorf("expected page2/record1 but got %v", page2.V) - } - - finished := tc.Messages[4] - if finished.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_FINISHED { - t.Errorf("expected status FINISHED but got %v", finished.V) - } -} - -func TestLogAdapter_Validation_Scope(t *testing.T) { - t.Parallel() - - ts := timestamppb.Now() - tla := &testLogAdapter{ - t: t, - expected: nil, - } - - tc := &sdp.TestConnection{ - Messages: make([]sdp.ResponseMessage, 0), - } - - e := newEngine(t, "logs.validation_scope", nil, tc) - if e == nil { - t.Fatal("failed to create engine") - } - - err := e.SetLogAdapter(tla) - if err != nil { - t.Fatal(err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = e.Stop() - }() - - _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( - "", - func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { - t.Log("Received message:", msg) - }, - )) - - err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ - Request: &sdp.GetLogRecordsRequest{ - Scope: "different-scope", - Query: "test", - From: ts, - To: ts, - MaxRecords: 10, - StartFromOldest: false, - }, - }) - if err != nil { - t.Log("Subscriptions:", tc.Subscriptions) - t.Fatal(err) - } - - // TODO: properly sync the test to wait for the messages to be sent - time.Sleep(1 * time.Second) - - tc.MessagesMu.Lock() - defer tc.MessagesMu.Unlock() - - if len(tc.Messages) == 0 { - t.Fatalf("expected messages but got none: %v", tc.Messages) - } - - msg := tc.Messages[len(tc.Messages)-1] - if msg.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_ERRORED { - t.Errorf("expected status ERRORED but got %v", msg.V) - } -} - -func TestLogAdapter_Validation_Empty(t *testing.T) { - t.Parallel() - - ts := timestamppb.Now() - tla := &testLogAdapter{ - t: t, - expected: nil, - } - - tc := &sdp.TestConnection{ - Messages: make([]sdp.ResponseMessage, 0), - } - - e := newEngine(t, "logs.validation_scope", nil, tc) - if e == nil { - t.Fatal("failed to create engine") - } - - err := e.SetLogAdapter(tla) - if err != nil { - t.Fatal(err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = e.Stop() - }() - - _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( - "", - func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { - t.Log("Received message:", msg) - }, - )) - - err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ - Request: &sdp.GetLogRecordsRequest{ - Scope: "test", - Query: "", - From: ts, - To: ts, - MaxRecords: 10, - StartFromOldest: false, - }, - }) - if err != nil { - t.Log("Subscriptions:", tc.Subscriptions) - t.Fatal(err) - } - - // TODO: properly sync the test to wait for the messages to be sent - time.Sleep(1 * time.Second) - - tc.MessagesMu.Lock() - defer tc.MessagesMu.Unlock() - - if len(tc.Messages) == 0 { - t.Fatalf("expected messages but got none: %v", tc.Messages) - } - - msg := tc.Messages[len(tc.Messages)-1] - if msg.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_ERRORED { - t.Errorf("expected status ERRORED but got %v", msg.V) - } -} - -func TestLogAdapter_Validation_NoReplyTo(t *testing.T) { - t.Parallel() - - ts := timestamppb.Now() - tla := &testLogAdapter{ - t: t, - expected: nil, - } - - tc := &sdp.TestConnection{ - Messages: make([]sdp.ResponseMessage, 0), - } - - e := newEngine(t, "logs.validation_scope", nil, tc) - if e == nil { - t.Fatal("failed to create engine") - } - - err := e.SetLogAdapter(tla) - if err != nil { - t.Fatal(err) - } - - err = e.Start(t.Context()) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = e.Stop() - }() - - _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( - "", - func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { - t.Log("Received message:", msg) - }, - )) - - err = tc.Publish(t.Context(), "logs.scope.test", &sdp.NATSGetLogRecordsRequest{ - Request: &sdp.GetLogRecordsRequest{ - Scope: "test", - Query: "test", - From: ts, - To: ts, - MaxRecords: 10, - StartFromOldest: false, - }, - }) - if err != nil { - t.Log("Subscriptions:", tc.Subscriptions) - t.Fatal(err) - } - - // TODO: properly sync the test to wait for the messages to be sent - time.Sleep(1 * time.Second) - - tc.MessagesMu.Lock() - defer tc.MessagesMu.Unlock() - - // only the Request message should be sent, no responses - if len(tc.Messages) != 1 { - t.Fatalf("expected 1 message but got %d: %v", len(tc.Messages), tc.Messages) - } -} diff --git a/discovery/main_test.go b/discovery/main_test.go deleted file mode 100644 index bcaee571..00000000 --- a/discovery/main_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package discovery - -import ( - "context" - "log" - "os" - "testing" - - "github.com/overmindtech/cli/tracing" -) - -func TestMain(m *testing.M) { - exitCode := func() int { - defer tracing.ShutdownTracer(context.Background()) - - if err := tracing.InitTracerWithUpstreams("discovery-tests", os.Getenv("HONEYCOMB_API_KEY"), ""); err != nil { - log.Fatal(err) - } - - return m.Run() - }() - - os.Exit(exitCode) -} diff --git a/discovery/nats_shared_test.go b/discovery/nats_shared_test.go deleted file mode 100644 index ecb8ce05..00000000 --- a/discovery/nats_shared_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - "net" - "net/url" - "testing" - "time" -) - -var NatsTestURLs = []string{ - "nats://nats:4222", - "nats://localhost:4222", -} - -var NatsAuthTestURLs = []string{ - "nats://nats-auth:4222", - "nats://localhost:4223", -} - -var tokenExchangeURLs = []string{ - "http://api-server:8080/api", - "http://localhost:8080/api", -} - -// SkipWithoutNats Skips a test if NATS is not available -func SkipWithoutNats(t *testing.T) { - var err error - - for _, url := range NatsTestURLs { - err = testURL(url) - - if err == nil { - return - } - } - - if err != nil { - t.Error(err) - t.Skip("NATS not available") - } -} - -// SkipWithoutNatsAuth Skips a test if authenticated NATS is not available -func SkipWithoutNatsAuth(t *testing.T) { - var err error - - for _, url := range NatsAuthTestURLs { - err = testURL(url) - - if err == nil { - return - } - } - - if err != nil { - t.Error(err) - t.Skip("NATS not available") - } -} - -// SkipWithoutTokenExchange Skips a test if the token exchange API server is not available -func SkipWithoutTokenExchange(t *testing.T) { - var err error - - for _, url := range tokenExchangeURLs { - err = testURL(url) - - if err == nil { - return - } - } - - if err != nil { - t.Error(err) - t.Skip("Token exchange API server not available") - } -} - -func GetWorkingTokenExchange() (string, error) { - var err error - - for _, url := range tokenExchangeURLs { - if err = testURL(url); err == nil { - return url, nil - } - } - - return "", fmt.Errorf("no working token exchanges found: %w", err) -} - -func testURL(testURL string) error { - url, err := url.Parse(testURL) - - if err != nil { - return fmt.Errorf("could not parse NATS URL: %v. Error: %w", testURL, err) - } - - dialer := &net.Dialer{ - Timeout: time.Second, - } - conn, err := dialer.DialContext(context.Background(), "tcp", net.JoinHostPort(url.Hostname(), url.Port())) - - if err == nil { - conn.Close() - return nil - } - - return err -} diff --git a/discovery/nats_watcher.go b/discovery/nats_watcher.go deleted file mode 100644 index 3a68daea..00000000 --- a/discovery/nats_watcher.go +++ /dev/null @@ -1,126 +0,0 @@ -package discovery - -import ( - "context" - "sync" - "time" - - "github.com/nats-io/nats.go" - log "github.com/sirupsen/logrus" -) - -// WatchableConnection Is ususally a *nats.Conn, we are using an interface here -// to allow easier testing -type WatchableConnection interface { - Status() nats.Status - Stats() nats.Statistics - LastError() error -} - -type NATSWatcher struct { - // Connection The NATS connection to watch - Connection WatchableConnection - - // FailureHandler will be called when the connection has been closed and is - // no longer trying to reconnect, or when the connection has been in a - // non-CONNECTED state for longer than ReconnectionTimeout. - FailureHandler func() - - // ReconnectionTimeout is the maximum duration to wait for a reconnection - // before triggering the FailureHandler. If set to 0, no timeout is applied - // and the watcher only triggers on CLOSED status (legacy behavior). - // Recommended value: 5 minutes. - ReconnectionTimeout time.Duration - - watcherContext context.Context - watcherCancel context.CancelFunc - watcherTicker *time.Ticker - watchingMutex sync.Mutex - disconnectedSince time.Time - hasBeenDisconnected bool - failureHandlerTriggered bool -} - -func (w *NATSWatcher) Start(checkInterval time.Duration) { - if w == nil || w.Connection == nil { - return - } - - w.watcherContext, w.watcherCancel = context.WithCancel(context.Background()) - w.watcherTicker = time.NewTicker(checkInterval) - w.watchingMutex.Lock() - - go func(ctx context.Context) { - defer w.watchingMutex.Unlock() - for { - select { - case <-w.watcherTicker.C: - status := w.Connection.Status() - if status != nats.CONNECTED { - // Track when we first became disconnected - if !w.hasBeenDisconnected { - w.disconnectedSince = time.Now() - w.hasBeenDisconnected = true - w.failureHandlerTriggered = false - } - - disconnectedDuration := time.Since(w.disconnectedSince) - - log.WithFields(log.Fields{ - "status": status.String(), - "inBytes": w.Connection.Stats().InBytes, - "outBytes": w.Connection.Stats().OutBytes, - "reconnects": w.Connection.Stats().Reconnects, - "lastError": w.Connection.LastError(), - "disconnectedDuration": disconnectedDuration.String(), - }).Warn("NATS not connected") - - // Trigger failure handler if connection is CLOSED (won't retry) - // or if we've been disconnected for too long. Only trigger once - // per disconnection period to avoid repeated calls while the - // handler is working on reconnection. - if !w.failureHandlerTriggered { - shouldTriggerFailure := false - if status == nats.CLOSED { - log.Warn("NATS connection is CLOSED, triggering failure handler") - shouldTriggerFailure = true - } else if w.ReconnectionTimeout > 0 && disconnectedDuration > w.ReconnectionTimeout { - log.WithFields(log.Fields{ - "disconnectedDuration": disconnectedDuration.String(), - "reconnectionTimeout": w.ReconnectionTimeout.String(), - }).Error("NATS connection has been disconnected for too long, triggering failure handler") - shouldTriggerFailure = true - } - - if shouldTriggerFailure { - // Mark that we've triggered the handler for this disconnection - // period to prevent repeated calls - w.failureHandlerTriggered = true - w.FailureHandler() - } - } - } else { - // Reset the disconnection tracking when we're connected - w.hasBeenDisconnected = false - w.failureHandlerTriggered = false - } - case <-ctx.Done(): - w.watcherTicker.Stop() - - return - } - } - }(w.watcherContext) -} - -func (w *NATSWatcher) Stop() { - if w.watcherCancel != nil { - w.watcherCancel() - - // Once we have sent the signal, wait until it's unlocked so we know - // it's completely stopped - w.watchingMutex.Lock() - defer w.watchingMutex.Unlock() - - } -} diff --git a/discovery/nats_watcher_test.go b/discovery/nats_watcher_test.go deleted file mode 100644 index ade303ab..00000000 --- a/discovery/nats_watcher_test.go +++ /dev/null @@ -1,572 +0,0 @@ -package discovery - -import ( - "sync" - "testing" - "time" - - "github.com/nats-io/nats.go" -) - -type TestConnection struct { - ReturnStatus nats.Status - ReturnStats nats.Statistics - ReturnError error - Mutex sync.Mutex -} - -func (t *TestConnection) Status() nats.Status { - t.Mutex.Lock() - defer t.Mutex.Unlock() - return t.ReturnStatus -} - -func (t *TestConnection) Stats() nats.Statistics { - t.Mutex.Lock() - defer t.Mutex.Unlock() - return t.ReturnStats -} - -func (t *TestConnection) LastError() error { - t.Mutex.Lock() - defer t.Mutex.Unlock() - return t.ReturnError -} - -func TestNATSWatcher(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTING, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - time.Sleep(interval * 2) - - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTED - c.Mutex.Unlock() - - time.Sleep(interval * 2) - - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - time.Sleep(interval * 2) - - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTED - c.Mutex.Unlock() - - time.Sleep(interval * 2) - - c.Mutex.Lock() - c.ReturnStatus = nats.CLOSED - c.Mutex.Unlock() - - select { - case <-time.After(interval * 2): - t.Errorf("FailureHandler not called in %v", (interval * 2).String()) - case <-fail: - // The fail handler has been called! - t.Log("Fail handler called successfully 🥳") - } -} - -func TestFailureHandler(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTING, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - var w *NATSWatcher - done := make(chan bool, 1024) - - w = &NATSWatcher{ - Connection: &c, - FailureHandler: func() { - go w.Stop() - done <- true - }, - } - - interval := 100 * time.Millisecond - - w.Start(interval) - - time.Sleep(interval * 2) - - c.Mutex.Lock() - c.ReturnStatus = nats.CLOSED - c.Mutex.Unlock() - - time.Sleep(interval * 2) - - select { - case <-time.After(interval * 2): - t.Errorf("FailureHandler not completed in %v", (interval * 2).String()) - case <-done: - if len(done) != 0 { - t.Errorf("Handler was called more than once") - } - // The fail handler has been called! - t.Log("Fail handler called successfully 🥳") - } -} - -func TestReconnectionTimeout(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 100 * time.Millisecond, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Start connected - time.Sleep(interval * 2) - - // Transition to RECONNECTING state - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for the timeout to trigger (100ms + some buffer) - select { - case <-time.After(200 * time.Millisecond): - t.Error("FailureHandler not called after reconnection timeout") - case <-fail: - t.Log("Fail handler called successfully after reconnection timeout 🥳") - } - - w.Stop() -} - -func TestReconnectionTimeoutNotTriggeredWhenConnected(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 50 * time.Millisecond, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Briefly go to RECONNECTING state - time.Sleep(interval * 2) - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // But reconnect before timeout - time.Sleep(20 * time.Millisecond) - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTED - c.Mutex.Unlock() - - // Wait longer than the timeout to ensure it doesn't trigger - select { - case <-time.After(100 * time.Millisecond): - t.Log("Timeout not triggered as expected when connection recovered 🥳") - case <-fail: - t.Error("FailureHandler should not be called when connection recovers before timeout") - } - - w.Stop() -} - -func TestReconnectionTimeoutDisabled(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - // No timeout set (0 means disabled) - ReconnectionTimeout: 0, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Transition to RECONNECTING state - time.Sleep(interval * 2) - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for a while - should not trigger failure handler - select { - case <-time.After(100 * time.Millisecond): - t.Log("Timeout correctly disabled, failure handler not called 🥳") - case <-fail: - t.Error("FailureHandler should not be called when timeout is disabled") - } - - w.Stop() -} - -func TestFailureHandlerNotCalledRepeatedly(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - failCount := 0 - var mu sync.Mutex - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 50 * time.Millisecond, - FailureHandler: func() { - mu.Lock() - failCount++ - mu.Unlock() - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Transition to RECONNECTING state - time.Sleep(interval * 2) - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for timeout to trigger (50ms timeout + buffer) - time.Sleep(80 * time.Millisecond) - - // Give it more time to ensure handler isn't called again - time.Sleep(50 * time.Millisecond) - - w.Stop() - - mu.Lock() - count := failCount - mu.Unlock() - - if count != 1 { - t.Errorf("FailureHandler should be called exactly once, but was called %d times", count) - } else { - t.Log("Failure handler called exactly once as expected 🥳") - } -} - -func TestStartWithNilConnection(t *testing.T) { - w := NATSWatcher{ - Connection: nil, - FailureHandler: func() { - t.Error("FailureHandler should not be called when connection is nil") - }, - } - - // Should not panic and should return early - w.Start(10 * time.Millisecond) - time.Sleep(20 * time.Millisecond) - - // If we get here without panicking, the test passes - t.Log("Start with nil connection handled gracefully 🥳") -} - -func TestStartWithNilWatcher(t *testing.T) { - var w *NATSWatcher - - // Should not panic - w.Start(10 * time.Millisecond) - time.Sleep(20 * time.Millisecond) - - // If we get here without panicking, the test passes - t.Log("Start with nil watcher handled gracefully 🥳") -} - -func TestReconnectionTimeoutWithConnectingState(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 100 * time.Millisecond, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Start connected - time.Sleep(interval * 2) - - // Transition to CONNECTING state (not just RECONNECTING) - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTING - c.Mutex.Unlock() - - // Wait for the timeout to trigger (100ms + some buffer) - select { - case <-time.After(200 * time.Millisecond): - t.Error("FailureHandler not called after reconnection timeout with CONNECTING state") - case <-fail: - t.Log("Fail handler called successfully after reconnection timeout with CONNECTING state 🥳") - } - - w.Stop() -} - -func TestMultipleDisconnectionCycles(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - failCount := 0 - var mu sync.Mutex - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 50 * time.Millisecond, - FailureHandler: func() { - mu.Lock() - failCount++ - mu.Unlock() - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // First disconnection cycle - time.Sleep(interval * 2) - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for timeout to trigger - time.Sleep(80 * time.Millisecond) - - // Reconnect - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTED - c.Mutex.Unlock() - time.Sleep(interval * 2) - - // Second disconnection cycle - should reset and allow handler to be called again - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for timeout to trigger again - time.Sleep(80 * time.Millisecond) - - w.Stop() - - mu.Lock() - count := failCount - mu.Unlock() - - if count != 2 { - t.Errorf("FailureHandler should be called twice (once per disconnection cycle), but was called %d times", count) - } else { - t.Log("Failure handler called correctly for multiple disconnection cycles 🥳") - } -} - -func TestStopBeforeStart(t *testing.T) { - w := NATSWatcher{ - Connection: &TestConnection{ - ReturnStatus: nats.CONNECTED, - }, - } - - // Should not panic if Stop is called before Start - w.Stop() - t.Log("Stop before Start handled gracefully 🥳") -} - -func TestStopMultipleTimes(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - w := NATSWatcher{ - Connection: &c, - FailureHandler: func() {}, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - time.Sleep(interval * 2) - - // Stop multiple times should not panic - w.Stop() - w.Stop() - w.Stop() - - t.Log("Multiple Stop calls handled gracefully 🥳") -} - -func TestHandlerResetAfterReconnection(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - failCount := 0 - var mu sync.Mutex - - w := NATSWatcher{ - Connection: &c, - // Set a short timeout for testing - ReconnectionTimeout: 50 * time.Millisecond, - FailureHandler: func() { - mu.Lock() - failCount++ - mu.Unlock() - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // First disconnection - trigger timeout - time.Sleep(interval * 2) - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for timeout - time.Sleep(80 * time.Millisecond) - - // Reconnect - this should reset the tracking - c.Mutex.Lock() - c.ReturnStatus = nats.CONNECTED - c.Mutex.Unlock() - time.Sleep(interval * 2) - - // Disconnect again - should be able to trigger handler again - c.Mutex.Lock() - c.ReturnStatus = nats.RECONNECTING - c.Mutex.Unlock() - - // Wait for timeout again - time.Sleep(80 * time.Millisecond) - - w.Stop() - - mu.Lock() - count := failCount - mu.Unlock() - - if count != 2 { - t.Errorf("FailureHandler should be called twice after reconnection reset, but was called %d times", count) - } else { - t.Log("Handler reset correctly after reconnection 🥳") - } -} - -func TestCLOSEDStatusTriggersImmediately(t *testing.T) { - c := TestConnection{ - ReturnStatus: nats.CONNECTED, - ReturnStats: nats.Statistics{}, - ReturnError: nil, - } - - fail := make(chan bool) - - w := NATSWatcher{ - Connection: &c, - // Even with timeout set, CLOSED should trigger immediately - ReconnectionTimeout: 100 * time.Millisecond, - FailureHandler: func() { - fail <- true - }, - } - - interval := 10 * time.Millisecond - - w.Start(interval) - - // Start connected - time.Sleep(interval * 2) - - // Transition directly to CLOSED (should trigger immediately, not wait for timeout) - c.Mutex.Lock() - c.ReturnStatus = nats.CLOSED - c.Mutex.Unlock() - - // Should trigger much faster than the timeout - select { - case <-time.After(50 * time.Millisecond): - t.Error("FailureHandler not called immediately for CLOSED status") - case <-fail: - t.Log("Fail handler called immediately for CLOSED status 🥳") - } - - w.Stop() -} diff --git a/discovery/nil_publisher.go b/discovery/nil_publisher.go deleted file mode 100644 index 15119900..00000000 --- a/discovery/nil_publisher.go +++ /dev/null @@ -1,111 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" - "google.golang.org/protobuf/proto" -) - -// When testing this library, or running without a real NATS connection, it is -// necessary to create a fake publisher rather than pass in a nil pointer. This -// is due to the fact that the NATS libraries will panic if a method is called -// on a nil pointer -type NilConnection struct{} - -// assert interface implementation -var _ sdp.EncodedConnection = (*NilConnection)(nil) - -// Publish Does nothing except log an error -func (n NilConnection) Publish(ctx context.Context, subj string, m proto.Message) error { - log.WithFields(log.Fields{ - "subject": subj, - "message": fmt.Sprint(m), - }).Error("Could not publish NATS message due to no connection") - - return nil -} - -// PublishRequest Does nothing except log an error -func (n NilConnection) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { - log.WithFields(log.Fields{ - "subject": subj, - "replyTo": replyTo, - "message": fmt.Sprint(m), - }).Error("Could not publish NATS message request due to no connection") - - return nil -} - -// PublishMsg Does nothing except log an error -func (n NilConnection) PublishMsg(ctx context.Context, msg *nats.Msg) error { - log.WithFields(log.Fields{ - "subject": msg.Subject, - "message": fmt.Sprint(msg), - }).Error("Could not publish NATS message due to no connection") - - return nil -} - -// Subscribe Does nothing except log an error -func (n NilConnection) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { - log.WithFields(log.Fields{ - "subject": subj, - }).Error("Could not subscribe to NAT subject due to no connection") - - return nil, nil -} - -// QueueSubscribe Does nothing except log an error -func (n NilConnection) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { - log.WithFields(log.Fields{ - "subject": subj, - "queue": queue, - }).Error("Could not subscribe to NAT subject queue due to no connection") - - return nil, nil -} - -// Request Does nothing except log an error -func (n NilConnection) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { - log.WithFields(log.Fields{ - "subject": msg.Subject, - "message": fmt.Sprint(msg), - }).Error("Could not publish NATS request due to no connection") - - return nil, nil -} - -// Status Always returns nats.CONNECTED -func (n NilConnection) Status() nats.Status { - return nats.CONNECTED -} - -// Stats Always returns empty/zero nats.Statistics -func (n NilConnection) Stats() nats.Statistics { - return nats.Statistics{} -} - -// LastError Always returns nil -func (n NilConnection) LastError() error { - return nil -} - -// Drain Always returns nil -func (n NilConnection) Drain() error { - return nil -} - -// Close Does nothing -func (n NilConnection) Close() {} - -// Underlying Always returns nil -func (n NilConnection) Underlying() *nats.Conn { - return nil -} - -// Drop Does nothing -func (n NilConnection) Drop() {} diff --git a/discovery/performance_test.go b/discovery/performance_test.go deleted file mode 100644 index a6b4bd8f..00000000 --- a/discovery/performance_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package discovery - -import ( - "context" - "math" - "os" - "sync" - "testing" - "time" - - "github.com/overmindtech/cli/auth" - "github.com/overmindtech/cli/sdp-go" -) - -type SlowAdapter struct { - QueryDuration time.Duration -} - -func (s *SlowAdapter) Type() string { - return "person" -} - -func (s *SlowAdapter) Name() string { - return "slow-adapter" -} - -func (s *SlowAdapter) DefaultCacheDuration() time.Duration { - return 10 * time.Minute -} - -func (s *SlowAdapter) Metadata() *sdp.AdapterMetadata { - return &sdp.AdapterMetadata{} -} - -func (s *SlowAdapter) Scopes() []string { - return []string{"test"} -} - -func (s *SlowAdapter) Hidden() bool { - return false -} - -func (s *SlowAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { - end := time.Now().Add(s.QueryDuration) - attributes, _ := sdp.ToAttributes(map[string]interface{}{ - "name": query, - }) - - item := sdp.Item{ - Type: "person", - UniqueAttribute: "name", - Attributes: attributes, - Scope: "test", - // TODO(LIQs): delete this - LinkedItemQueries: []*sdp.LinkedItemQuery{}, - } - - // TODO(LIQs): convert to returning edges - for i := 0; i != 2; i++ { - item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: RandomName(), - Scope: "test", - }}) - } - - time.Sleep(time.Until(end)) - - return &item, nil -} - -func (s *SlowAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { - return []*sdp.Item{}, nil -} - -func (s *SlowAdapter) Weight() int { - return 100 -} - -func TestParallelQueryPerformance(t *testing.T) { - if os.Getenv("GITHUB_ACTIONS") != "" { - t.Skip("Performance tests under github actions are too unreliable") - } - - // This test is designed to ensure that query duration is linear up to a - // certain point. Above that point the overhead caused by having so many - // goroutines running will start to make the response times non-linear which - // maybe isn't ideal but given realistic loads we probably don't care. - t.Run("Without linking", func(t *testing.T) { - RunLinearPerformanceTest(t, "1 query", 1, 0, 1) - RunLinearPerformanceTest(t, "10 queries", 10, 0, 1) - RunLinearPerformanceTest(t, "100 queries", 100, 0, 10) - RunLinearPerformanceTest(t, "1,000 queries", 1000, 0, 100) - }) -} - -// RunLinearPerformanceTest Runs a test with a given number in input queries, -// link depth and parallelization limit. Expected results and expected duration -// are determined automatically meaning all this is testing for is the fact that -// the performance continues to be linear and predictable -func RunLinearPerformanceTest(t *testing.T, name string, numQueries int, linkDepth int, numParallel int) { - t.Helper() - - t.Run(name, func(t *testing.T) { - result := TimeQueries(t, numQueries, linkDepth, numParallel) - - if len(result.Results) != result.ExpectedItems { - t.Errorf("Expected %v items, got %v (%v errors)", result.ExpectedItems, len(result.Results), len(result.Errors)) - } - - if result.TimeTaken > result.MaxTime { - t.Errorf("Queries took too long: %v Max: %v", result.TimeTaken.String(), result.MaxTime.String()) - } - }) -} - -type TimedResults struct { - ExpectedItems int - MaxTime time.Duration - TimeTaken time.Duration - Results []*sdp.Item - Errors []*sdp.QueryError -} - -func TimeQueries(t *testing.T, numQueries int, linkDepth int, numParallel int) TimedResults { - ec := EngineConfig{ - MaxParallelExecutions: numParallel, - Unauthenticated: true, - NATSOptions: &auth.NATSOptions{ - NumRetries: 5, - RetryDelay: time.Second, - Servers: NatsTestURLs, - ConnectionName: "test-connection", - ConnectionTimeout: time.Second, - MaxReconnects: 5, - }, - } - e, err := NewEngine(&ec) - if err != nil { - t.Fatalf("Error initializing Engine: %v", err) - } - err = e.AddAdapters(&SlowAdapter{ - QueryDuration: 100 * time.Millisecond, - }) - if err != nil { - t.Fatalf("Error adding adapter: %v", err) - } - err = e.Start(t.Context()) - if err != nil { - t.Fatalf("Error starting Engine: %v", err) - } - defer func() { - err = e.Stop() - if err != nil { - t.Fatalf("Error stopping Engine: %v", err) - } - }() - - // Calculate how many items to expect and the expected duration - var expectedItems int - var expectedDuration time.Duration - for i := 0; i <= linkDepth; i++ { - thisLayer := int(math.Pow(2, float64(i))) * numQueries - - // Expect that it'll take no longer that 120% of the sleep time. - thisDuration := 120 * math.Ceil(float64(thisLayer)/float64(numParallel)) - expectedDuration = expectedDuration + (time.Duration(thisDuration) * time.Millisecond) - expectedItems = expectedItems + thisLayer - } - - results := make([]*sdp.Item, 0) - errors := make([]*sdp.QueryError, 0) - resultsMutex := sync.Mutex{} - wg := sync.WaitGroup{} - - start := time.Now() - - for range numQueries { - qt := QueryTracker{ - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: RandomName(), - Scope: "test", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: uint32(linkDepth), - }, - }, - Engine: e, - } - - wg.Add(1) - - go func(qt *QueryTracker) { - defer wg.Done() - - items, _, errs, _ := qt.Execute(context.Background()) - - resultsMutex.Lock() - results = append(results, items...) - errors = append(errors, errs...) - resultsMutex.Unlock() - }(&qt) - } - - wg.Wait() - - return TimedResults{ - ExpectedItems: expectedItems, - MaxTime: expectedDuration, - TimeTaken: time.Since(start), - Results: results, - Errors: errors, - } -} diff --git a/discovery/querytracker.go b/discovery/querytracker.go deleted file mode 100644 index d7bb9a1f..00000000 --- a/discovery/querytracker.go +++ /dev/null @@ -1,92 +0,0 @@ -package discovery - -import ( - "context" - "errors" - - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// QueryTracker is used for tracking the progress of a single query. This -// is used because a single query could have a link depth that results in many -// additional queries being executed meaning that we need to not only track the first -// query, but also all other queries and items that result from linking -type QueryTracker struct { - // The query to track - Query *sdp.Query - - Context context.Context // The context that this query is running in - Cancel context.CancelFunc // The cancel function for the context - - // The engine that this is connected to, used for sending NATS messages - Engine *Engine -} - -// Execute Executes a given item query and publishes results and errors on the -// relevant nats subjects. Returns the full list of items, errors, and a final -// error. The final error will be populated if all adapters failed, or some other -// error was encountered while trying run the query -// -// If the context is cancelled, all query work will stop -func (qt *QueryTracker) Execute(ctx context.Context) ([]*sdp.Item, []*sdp.Edge, []*sdp.QueryError, error) { - if qt.Query == nil { - return nil, nil, nil, nil - } - - if qt.Engine == nil { - return nil, nil, nil, errors.New("no engine supplied, cannot execute") - } - - span := trace.SpanFromContext(ctx) - span.SetAttributes( - attribute.String("ovm.sdp.source_name", qt.Engine.EngineConfig.SourceName), - attribute.String("ovm.engine.type", qt.Engine.EngineConfig.EngineType), - attribute.String("ovm.engine.version", qt.Engine.EngineConfig.Version), - ) - - responses := make(chan *sdp.QueryResponse) - errChan := make(chan error, 1) - - sdpItems := make([]*sdp.Item, 0) - sdpEdges := make([]*sdp.Edge, 0) - sdpErrs := make([]*sdp.QueryError, 0) - - // Run the query in the background - go func(e chan error) { - defer tracing.LogRecoverToReturn(ctx, "Execute -> ExecuteQuery") - defer close(e) - e <- qt.Engine.ExecuteQuery(ctx, qt.Query, responses) - }(errChan) - - // Process the responses as they come in - for response := range responses { - if qt.Query.Subject() != "" && qt.Engine.natsConnection != nil { - err := qt.Engine.natsConnection.Publish(ctx, qt.Query.Subject(), response) - if err != nil { - span.RecordError(err) - log.WithError(err).Error("Response publishing error") - } - } - - switch response := response.GetResponseType().(type) { - case *sdp.QueryResponse_NewItem: - sdpItems = append(sdpItems, response.NewItem) - case *sdp.QueryResponse_Edge: - sdpEdges = append(sdpEdges, response.Edge) - case *sdp.QueryResponse_Error: - sdpErrs = append(sdpErrs, response.Error) - } - } - - // Get the result of the execution - err := <-errChan - if err != nil { - return sdpItems, sdpEdges, sdpErrs, err - } - - return sdpItems, sdpEdges, sdpErrs, ctx.Err() -} diff --git a/discovery/querytracker_test.go b/discovery/querytracker_test.go deleted file mode 100644 index 9a8a31a5..00000000 --- a/discovery/querytracker_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package discovery - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "google.golang.org/protobuf/types/known/structpb" -) - -type SpeedTestAdapter struct { - QueryDelay time.Duration - ReturnType string - ReturnScopes []string -} - -func (s *SpeedTestAdapter) Type() string { - if s.ReturnType != "" { - return s.ReturnType - } - - return "person" -} - -func (s *SpeedTestAdapter) Name() string { - return "SpeedTestAdapter" -} - -func (s *SpeedTestAdapter) Scopes() []string { - if len(s.ReturnScopes) > 0 { - return s.ReturnScopes - } - - return []string{"test"} -} - -func (s *SpeedTestAdapter) Metadata() *sdp.AdapterMetadata { - return &sdp.AdapterMetadata{} -} - -func (s *SpeedTestAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { - select { - case <-time.After(s.QueryDelay): - return &sdp.Item{ - Type: s.Type(), - UniqueAttribute: "name", - Attributes: &sdp.ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": { - Kind: &structpb.Value_StringValue{ - StringValue: query, - }, - }, - }, - }, - }, - // TODO(LIQs): convert to returning edges - LinkedItemQueries: []*sdp.LinkedItemQuery{ - { - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: query + time.Now().String(), - Scope: scope, - }, - }, - }, - Scope: scope, - }, nil - case <-ctx.Done(): - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_TIMEOUT, - ErrorString: ctx.Err().Error(), - Scope: scope, - } - } -} - -func (s *SpeedTestAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { - item, err := s.Get(ctx, scope, "dylan", ignoreCache) - - return []*sdp.Item{item}, err -} - -func (s *SpeedTestAdapter) Weight() int { - return 10 -} - -func TestExecute(t *testing.T) { - adapter := TestAdapter{ - ReturnType: "person", - ReturnScopes: []string{ - "test", - }, - cache: sdpcache.NewNoOpCache(), - } - - e := newStartedEngine(t, "TestExecute", nil, nil, &adapter) - - t.Run("Without linking", func(t *testing.T) { - t.Parallel() - - qt := QueryTracker{ - Engine: e, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - }, - } - - items, edges, errs, err := qt.Execute(context.Background()) - if err != nil { - t.Error(err) - } - - for _, e := range errs { - t.Error(e) - } - - if l := len(items); l != 1 { - t.Errorf("expected 1 items, got %v: %v", l, items) - } - - if l := len(edges); l != 0 { - t.Errorf("expected 0 items, got %v: %v", l, edges) - } - }) - - t.Run("With no engine", func(t *testing.T) { - t.Parallel() - - qt := QueryTracker{ - Engine: nil, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 10, - }, - Scope: "test", - }, - } - - _, _, _, err := qt.Execute(context.Background()) - - if err == nil { - t.Error("expected error but got nil") - } - }) - - t.Run("With no queries", func(t *testing.T) { - t.Parallel() - - qt := QueryTracker{ - Engine: e, - } - - _, _, _, err := qt.Execute(context.Background()) - if err != nil { - t.Error(err) - } - }) -} - -func TestTimeout(t *testing.T) { - adapter := SpeedTestAdapter{ - QueryDelay: 100 * time.Millisecond, - } - e := newStartedEngine(t, "TestTimeout", nil, nil, &adapter) - - t.Run("With a timeout, but not exceeding it", func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) - - qt := QueryTracker{ - Engine: e, - Context: ctx, - Cancel: cancel, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - }, - } - - items, edges, errs, err := qt.Execute(context.Background()) - if err != nil { - t.Error(err) - } - - for _, e := range errs { - t.Error(e) - } - - if l := len(items); l != 1 { - t.Errorf("expected 1 items, got %v: %v", l, items) - } - - if l := len(edges); l != 0 { - t.Errorf("expected 0 edges, got %v: %v", l, edges) - } - }) - - t.Run("With a timeout that is exceeded", func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) - - qt := QueryTracker{ - Engine: e, - Context: ctx, - Cancel: cancel, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "somethingElse", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - }, - } - - _, _, _, err := qt.Execute(ctx) - - if err == nil { - t.Error("Expected timeout but got no error") - } - }) -} - -func TestCancel(t *testing.T) { - e := newStartedEngine(t, "TestCancel", nil, nil) - - u := uuid.New() - ctx, cancel := context.WithCancel(context.Background()) - - qt := QueryTracker{ - Engine: e, - Context: ctx, - Cancel: cancel, - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: "somethingElse1", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 10, - }, - Scope: "test", - UUID: u[:], - }, - } - - items := make([]*sdp.Item, 0) - edges := make([]*sdp.Edge, 0) - var wg sync.WaitGroup - - var err error - wg.Add(1) - go func() { - items, edges, _, err = qt.Execute(context.Background()) - wg.Done() - }() - - // Give it some time to populate the cancelFunc - time.Sleep(100 * time.Millisecond) - - qt.Cancel() - - wg.Wait() - - if err == nil { - t.Error("expected error but got none") - } - - if len(items) != 0 { - t.Errorf("Expected no items but got %v", items) - } - - if len(edges) != 0 { - t.Errorf("Expected no edges but got %v", edges) - } -} diff --git a/discovery/shared_test.go b/discovery/shared_test.go deleted file mode 100644 index ce2943f5..00000000 --- a/discovery/shared_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - "math/rand" - "sync" - "sync/atomic" - "time" - - "github.com/goombaio/namegenerator" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" - "google.golang.org/protobuf/types/known/structpb" -) - -const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -func randString(length int) string { - var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} - -func RandomName() string { - seed := time.Now().UTC().UnixNano() - nameGenerator := namegenerator.NewNameGenerator(seed) - name := nameGenerator.Generate() - randGarbage := randString(10) - return fmt.Sprintf("%v-%v", name, randGarbage) -} - -var generation atomic.Int32 - -func (s *TestAdapter) NewTestItem(scope string, query string) *sdp.Item { - gen := generation.Add(1) - return &sdp.Item{ - Type: s.Type(), - Scope: scope, - UniqueAttribute: "name", - Attributes: &sdp.ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": structpb.NewStringValue(query), - "age": structpb.NewNumberValue(28), - "generation": structpb.NewNumberValue(float64(gen)), - }, - }, - }, - // TODO(LIQs): convert to returning edges - LinkedItemQueries: []*sdp.LinkedItemQuery{ - { - Query: &sdp.Query{ - Type: "person", - Method: sdp.QueryMethod_GET, - Query: RandomName(), - Scope: scope, - }, - }, - }, - } -} - -type TestAdapter struct { - ReturnScopes []string - ReturnType string - GetCalls [][]string - ListCalls [][]string - SearchCalls [][]string - IsHidden bool - ReturnWeight int // Weight to be returned - ReturnName string // The name of the Adapter - mutex sync.Mutex - - CacheDuration time.Duration // How long to cache items for - cache sdpcache.Cache // This is mandatory -} - -// NewTestAdapter creates a new TestAdapter with cache initialized -func NewTestAdapter() *TestAdapter { - return &TestAdapter{ - cache: sdpcache.NewNoOpCache(), // Initialize with NoOpCache to avoid nil pointer dereferences - } -} - -// ClearCalls Clears the call counters between tests -func (s *TestAdapter) ClearCalls() { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.ListCalls = make([][]string, 0) - s.SearchCalls = make([][]string, 0) - s.GetCalls = make([][]string, 0) - if s.cache != nil { - s.cache.Clear() - } -} - -func (s *TestAdapter) Type() string { - if s.ReturnType != "" { - return s.ReturnType - } - - return "person" -} - -func (s *TestAdapter) Name() string { - return fmt.Sprintf("testAdapter-%v", s.ReturnName) -} - -func (s *TestAdapter) DefaultCacheDuration() time.Duration { - return 100 * time.Millisecond -} - -func (s *TestAdapter) Metadata() *sdp.AdapterMetadata { - return &sdp.AdapterMetadata{ - Type: s.Type(), - DescriptiveName: "Person", - } -} - -func (s *TestAdapter) Scopes() []string { - if len(s.ReturnScopes) > 0 { - return s.ReturnScopes - } - - return []string{"test"} -} - -func (s *TestAdapter) Hidden() bool { - return s.IsHidden -} - -func (s *TestAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - var cacheHit bool - var ck sdpcache.CacheKey - var cachedItems []*sdp.Item - var qErr *sdp.QueryError - var done func() - - cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_GET, scope, s.Type(), query, ignoreCache) - defer done() - if qErr != nil { - return nil, qErr - } - if cacheHit { - if len(cachedItems) > 0 { - return cachedItems[0], nil - } else { - return nil, nil - } - } - - s.GetCalls = append(s.GetCalls, []string{scope, query}) - - switch scope { - case "empty": - err := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no items found", - Scope: scope, - } - s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) - return nil, err - case "error": - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "Error for testing", - Scope: scope, - } - default: - item := s.NewTestItem(scope, query) - s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) - return item, nil - } -} - -func (s *TestAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - var cacheHit bool - var ck sdpcache.CacheKey - var cachedItems []*sdp.Item - var qErr *sdp.QueryError - var done func() - - cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_LIST, scope, s.Type(), "", ignoreCache) - defer done() - if qErr != nil { - return nil, qErr - } - if cacheHit { - return cachedItems, nil - } - - s.ListCalls = append(s.ListCalls, []string{scope}) - - switch scope { - case "empty": - err := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no items found", - Scope: scope, - } - s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) - return nil, err - case "error": - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "Error for testing", - Scope: scope, - } - default: - item := s.NewTestItem(scope, "Dylan") - items := []*sdp.Item{item} - s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) - return items, nil - } -} - -func (s *TestAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - - var cacheHit bool - var ck sdpcache.CacheKey - var cachedItems []*sdp.Item - var qErr *sdp.QueryError - var done func() - - cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_SEARCH, scope, s.Type(), query, ignoreCache) - defer done() - if qErr != nil { - return nil, qErr - } - if cacheHit { - return cachedItems, nil - } - - s.SearchCalls = append(s.SearchCalls, []string{scope, query}) - - switch scope { - case "empty": - err := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no items found", - Scope: scope, - } - s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) - return nil, err - case "error": - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "Error for testing", - Scope: scope, - } - default: - item := s.NewTestItem(scope, "Dylan") - items := []*sdp.Item{item} - s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) - return items, nil - } -} - -func (s *TestAdapter) Weight() int { - return s.ReturnWeight -} diff --git a/discovery/tracing.go b/discovery/tracing.go deleted file mode 100644 index d2d90396..00000000 --- a/discovery/tracing.go +++ /dev/null @@ -1,20 +0,0 @@ -package discovery - -import ( - "go.opentelemetry.io/otel" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" - "go.opentelemetry.io/otel/trace" -) - -const ( - instrumentationName = "github.com/overmindtech/cli/discovery/discovery" - instrumentationVersion = "0.0.1" -) - -var ( - tracer = otel.GetTracerProvider().Tracer( - instrumentationName, - trace.WithInstrumentationVersion(instrumentationVersion), - trace.WithSchemaURL(semconv.SchemaURL), - ) -) diff --git a/docs.overmind.tech/docs/sources/_category_.json b/docs.overmind.tech/docs/sources/_category_.json new file mode 100644 index 00000000..974a8cf6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Infrastructure Sources", + "position": 3, + "collapsed": true, + "link": null +} diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md new file mode 100644 index 00000000..bb16518e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-domain-name.md @@ -0,0 +1,16 @@ +--- +title: API Gateway Domain Name +sidebar_label: apigateway-domain-name +--- + +An AWS API Gateway Domain Name represents a custom DNS name (e.g. `api.example.com`) that you attach to one or more stages of a REST, HTTP or WebSocket API. By creating this resource you can present a branded, user-friendly endpoint instead of the default `*.execute-api..amazonaws.com` host, configure an ACM or imported TLS certificate, choose an edge-optimised or regional endpoint, enable mutual TLS and define API mappings. Further information can be found in the official documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-create-custom-domain-name.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_domain_name.domain_name` + +## Supported Methods + +- `GET`: Get a Domain Name by domain-name +- `LIST`: List Domain Names +- `SEARCH`: Search Domain Names by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md new file mode 100644 index 00000000..2816caa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-resource.md @@ -0,0 +1,17 @@ +--- +title: API Gateway +sidebar_label: apigateway-resource +--- + +An **API Gateway Resource** represents a single path segment within an Amazon API Gateway REST API. Each resource forms part of the hierarchical URL structure of your API and can have HTTP methods (such as GET, POST, DELETE) attached to it, along with integrations, authorisers and request/response models. Correctly mapping these resources is critical because mis-configured paths can expose unintended back-ends or shadow existing routes. Overmind pulls every API Gateway Resource into its graph so you can understand how proposed changes will affect downstream services before you deploy them. +For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/apigateway/latest/api/API_Resource.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_resource.id` + +## Supported Methods + +- `GET`: Get a Resource by rest-api-id/resource-id +- ~~`LIST`~~ +- `SEARCH`: Search Resources by REST API ID diff --git a/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md b/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md new file mode 100644 index 00000000..83569578 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/apigateway-rest-api.md @@ -0,0 +1,27 @@ +--- +title: REST API +sidebar_label: apigateway-rest-api +--- + +AWS API Gateway REST APIs allow you to build, deploy and manage REST-style interfaces that front your application logic, Lambda functions or other AWS services. A REST API in API Gateway represents the top-level container for all stages, resources, methods, authorisers and deployments that make up your service. Once created, the API can be exposed publicly or kept private behind a VPC endpoint, throttled, monitored and versioned across stages. +For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html + +**Terrafrom Mappings:** + +- `aws_api_gateway_rest_api.id` + +## Supported Methods + +- `GET`: Get a REST API by ID +- `LIST`: List all REST APIs +- `SEARCH`: Search for REST APIs by their name + +## Possible Links + +### [`ec2-vpc-endpoint`](/sources/aws/Types/ec2-vpc-endpoint) + +If the REST API is configured as a private API, it is exposed inside a VPC through an Interface VPC Endpoint. Overmind links the `apigateway-rest-api` resource to the corresponding `ec2-vpc-endpoint` to show which endpoint clients inside the VPC must use to reach the API and to surface any network-level risks (such as missing security-group rules). + +### [`apigateway-resource`](/sources/aws/Types/apigateway-resource) + +An API Gateway REST API is composed of one or more resources, each representing a path segment (for example `/users` or `/orders/{orderId}`). Overmind links the parent `apigateway-rest-api` to each individual `apigateway-resource` so you can trace how a request traverses the API hierarchy and identify unprotected or redundant paths. diff --git a/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md b/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md new file mode 100644 index 00000000..03b3db07 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/autoscaling-auto-scaling-group.md @@ -0,0 +1,39 @@ +--- +title: Autoscaling Group +sidebar_label: autoscaling-auto-scaling-group +--- + +An AWS Autoscaling Group (ASG) is a logical collection of Amazon EC2 instances that are treated as a single scalable resource. It automatically adjusts the number of running instances to maintain a desired capacity, respond to demand spikes, enforce health‐based replacement, and support rolling updates. Configuration parameters such as minimum, maximum and desired instance counts, scaling policies, health checks and lifecycle hooks are all defined at the group level. +Further information is available in the official AWS documentation: https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html + +**Terrafrom Mappings:** + +- `aws_autoscaling_group.arn` + +## Supported Methods + +- `GET`: Get an Autoscaling Group by name +- `LIST`: List Autoscaling Groups +- `SEARCH`: Search for Autoscaling Groups by ARN + +## Possible Links + +### [`ec2-launch-template`](/sources/aws/Types/ec2-launch-template) + +An ASG normally references a launch template that describes how each EC2 instance should be configured (AMI, instance type, security groups, IAM instance profile, user data, etc.). Therefore the ASG is linked to its associated `ec2-launch-template`. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +ASGs can be attached to one or more ALB/NLB target groups so that their member instances are automatically registered and deregistered as they scale. The link shows which `elbv2-target-group`(s) an ASG feeds. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +The running EC2 instances that currently belong to an ASG are directly related to it. Overmind surfaces this connection so you can see which `ec2-instance` objects are under the control of a specific ASG. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Autoscaling uses an AWS service-linked role (typically `AWSServiceRoleForAutoScaling`) to perform scaling and health check actions on your behalf. Additionally, the launch template referenced by the ASG may specify an instance profile containing an IAM role for the launched instances. Both relationships are captured via the `iam-role` link. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +If the ASG’s launch template specifies a placement group, any instances it launches will be placed accordingly for improved networking performance or spread. The link reveals the `ec2-placement-group` associated with the ASG. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md new file mode 100644 index 00000000..892c7bac --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-cache-policy.md @@ -0,0 +1,16 @@ +--- +title: CloudFront Cache Policy +sidebar_label: cloudfront-cache-policy +--- + +An AWS CloudFront Cache Policy specifies the rules that dictate how CloudFront caches HTTP responses at edge locations. It determines which headers, cookies and query-string parameters are included in the cache key, how long objects remain in the cache (TTL values), and whether to compress the response before it is served to viewers. By creating and attaching custom cache policies to distributions or behaviours, you can fine-tune cache efficiency, control origin load, and optimise performance for different types of content. For a full description of the resource and its attributes, refer to the [AWS documentation](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CachePolicy.html). + +**Terrafrom Mappings:** + +- `aws_cloudfront_cache_policy.id` + +## Supported Methods + +- `GET`: Get a CloudFront Cache Policy +- `LIST`: List CloudFront Cache Policies +- `SEARCH`: Search CloudFront Cache Policies by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md new file mode 100644 index 00000000..49f4679c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-continuous-deployment-policy.md @@ -0,0 +1,19 @@ +--- +title: CloudFront Continuous Deployment Policy +sidebar_label: cloudfront-continuous-deployment-policy +--- + +A CloudFront Continuous Deployment Policy is an Amazon CloudFront configuration object that allows you to shift viewer traffic between two CloudFront distributions (normally a _staging_ and a _production_ distribution) in a controlled, progressive way. By defining percentage-based traffic splits or header-based routing rules, you can carry out blue/green or canary releases, test new versions of your application, and roll back instantly if problems occur. +Official documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/continuous-deployment.html + +## Supported Methods + +- `GET`: Get a CloudFront Continuous Deployment Policy by ID +- `LIST`: List CloudFront Continuous Deployment Policies +- `SEARCH`: Search CloudFront Continuous Deployment Policies by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +DNS records (usually CNAME or ALIAS/ANAME) that point end-user domains to the target CloudFront distributions determine which viewers are subject to a continuous deployment policy. When a policy is enabled, those DNS entries still resolve to the same CloudFront hostnames, but the policy decides how the resulting requests are routed internally between the staging and production distributions. Overmind therefore links the policy to related DNS resources so you can trace which public hostnames—and consequently which users—are affected by a particular traffic-splitting setup. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md new file mode 100644 index 00000000..8222aaa1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-distribution.md @@ -0,0 +1,58 @@ +--- +title: CloudFront Distribution +sidebar_label: cloudfront-distribution +--- + +Amazon CloudFront Distributions are globally-replicated configurations that tell the CloudFront CDN how to cache and deliver your content to end-users. Each distribution defines one or more origins, cache behaviours, security settings and optional edge-compute integrations. See the official AWS documentation for a full description: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_distribution.arn` + +## Supported Methods + +- `GET`: Get a distribution by ID +- `LIST`: List all distributions +- `SEARCH`: Search distributions by ARN + +## Possible Links + +### [`cloudfront-key-group`](/sources/aws/Types/cloudfront-key-group) + +A distribution can reference one or more Key Groups in its `TrustedKeyGroups` configuration to validate signed URLs or signed cookies. If a Key Group ID appears in the distribution’s config, Overmind links the two. + +### [`cloudfront-continuous-deployment-policy`](/sources/aws/Types/cloudfront-continuous-deployment-policy) + +Distributions may have an attached Continuous Deployment Policy (`ContinuousDeploymentPolicyId`) that allows blue/green traffic shifting. Overmind links the distribution to that policy. + +### [`cloudfront-cache-policy`](/sources/aws/Types/cloudfront-cache-policy) + +Every cache behaviour in a distribution can specify a `CachePolicyId`. Overmind links the distribution to any Cache Policies it relies on. + +### [`cloudfront-function`](/sources/aws/Types/cloudfront-function) + +Viewer request / response CloudFront Functions can be associated with behaviours in the distribution. Those references create links between the distribution and the function resources. + +### [`cloudfront-origin-request-policy`](/sources/aws/Types/cloudfront-origin-request-policy) + +Behaviours can also specify an `OriginRequestPolicyId` that controls which headers, cookies and query strings are sent to the origin. Overmind links distributions to the referenced Origin Request Policies. + +### [`cloudfront-realtime-log-config`](/sources/aws/Types/cloudfront-realtime-log-config) + +If real-time logging is enabled, the distribution contains one or more `RealtimeLogConfigArn` values. Overmind uses those to link the distribution to its real-time log configuration. + +### [`cloudfront-response-headers-policy`](/sources/aws/Types/cloudfront-response-headers-policy) + +Behaviours may include a `ResponseHeadersPolicyId` that injects security or custom headers. Overmind links the distribution to the associated Response Headers Policies. + +### [`dns`](/sources/stdlib/Types/dns) + +Public access to a distribution is normally via the CloudFront domain name or an alias/CNAME such as `www.example.com`. When a DNS record (e.g., Route 53 ALIAS) targets the distribution’s domain, Overmind links the DNS record to the distribution. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +Lambda@Edge functions (standard Lambda functions replicated to edge locations) can be attached to behaviours for request or response processing. These associations create links between the distribution and the Lambda functions. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +An S3 bucket is commonly used as an origin. When the distribution’s origin points at an S3 bucket domain or ARN, Overmind links the distribution to that bucket. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md new file mode 100644 index 00000000..dc43f440 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-function.md @@ -0,0 +1,16 @@ +--- +title: CloudFront Function +sidebar_label: cloudfront-function +--- + +Amazon CloudFront Functions let you run lightweight JavaScript code at CloudFront edge locations, enabling real-time manipulation of HTTP requests and responses without the latency of invoking AWS Lambda. Typical use-cases include URL rewrites, header manipulation, access control and A/B testing, all executed in under one millisecond at every edge. For more detail see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_function.name` + +## Supported Methods + +- `GET`: Get a CloudFront Function by name +- `LIST`: List CloudFront Functions +- `SEARCH`: Search CloudFront Functions by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md new file mode 100644 index 00000000..f2647ffb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-key-group.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Key Group +sidebar_label: cloudfront-key-group +--- + +A CloudFront Key Group is an Amazon CloudFront configuration object that aggregates several public keys under a single identifier. CloudFront uses the keys in the group to verify the signatures on signed URLs, signed cookies, or JSON Web Tokens that you employ to control access to private content. By attaching a key group to a distribution or cache behaviour you can centrally manage which public keys are trusted; adding or removing a key from the group immediately changes who can generate valid signatures without the need to touch individual distributions. +For more information, refer to the AWS documentation on Key Groups: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html#PrivateContent-KeyGroups + +**Terrafrom Mappings:** + +- `aws_cloudfront_key_group.id` + +## Supported Methods + +- `GET`: Get a CloudFront Key Group by ID +- `LIST`: List CloudFront Key Groups +- `SEARCH`: Search CloudFront Key Groups by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md new file mode 100644 index 00000000..42454fe8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-access-control.md @@ -0,0 +1,17 @@ +--- +title: Cloudfront Origin Access Control +sidebar_label: cloudfront-origin-access-control +--- + +Amazon CloudFront Origin Access Control (OAC) is a security feature that allows you to restrict access to the origin of a CloudFront distribution, ensuring that all requests are authenticated and authorised by CloudFront before reaching your S3 bucket, Application Load Balancer, or custom origin. OAC is the modern replacement for Origin Access Identities (OAI) and supports both SigV4‐signed requests and IAM authentication, giving you more granular control over how CloudFront communicates with your back-end resources. By configuring an OAC you prevent direct exposure of your origin on the public internet, helping to mitigate data-exfiltration and origin-based attacks. +For further information see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-origin.html#concept-origin-access-control + +**Terrafrom Mappings:** + +- `aws_cloudfront_origin_access_control.id` + +## Supported Methods + +- `GET`: Get Origin Access Control by ID +- `LIST`: List Origin Access Controls +- `SEARCH`: Origin Access Control by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md new file mode 100644 index 00000000..2b59261c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-origin-request-policy.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Origin Request Policy +sidebar_label: cloudfront-origin-request-policy +--- + +A CloudFront Origin Request Policy defines which HTTP headers, cookies and query-string parameters Amazon CloudFront passes from the edge to your origin. By attaching a policy to a cache behaviour you can standardise the information that reaches your origin, independent of any caching decisions. Policies are reusable across multiple distributions, making configuration simpler and less error-prone. +For further details refer to the [AWS documentation](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginRequestPolicy.html). + +**Terrafrom Mappings:** + +- `aws_cloudfront_origin_request_policy.id` + +## Supported Methods + +- `GET`: Get Origin Request Policy by ID +- `LIST`: List Origin Request Policies +- `SEARCH`: Origin Request Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md new file mode 100644 index 00000000..5202bc2c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-realtime-log-config.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Realtime Log Config +sidebar_label: cloudfront-realtime-log-config +--- + +Amazon CloudFront Realtime Log Configs define the structure of the near-real-time log data that CloudFront can stream to a destination such as Kinesis Data Streams. A Realtime Log Config specifies which data fields are captured, the sampling rate, and the endpoint to which the records are delivered. This enables teams to observe viewer requests, latency, cache behaviour and other metrics with sub-second visibility, allowing faster troubleshooting and performance tuning. +For a detailed description of the service and its capabilities, refer to the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_realtime_log_config.arn` + +## Supported Methods + +- `GET`: Get Realtime Log Config by Name +- `LIST`: List Realtime Log Configs +- `SEARCH`: Search Realtime Log Configs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md new file mode 100644 index 00000000..74c25ae6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-response-headers-policy.md @@ -0,0 +1,17 @@ +--- +title: CloudFront Response Headers Policy +sidebar_label: cloudfront-response-headers-policy +--- + +A CloudFront Response Headers Policy is an AWS configuration object that specifies the HTTP response headers that Amazon CloudFront adds to, removes from, or overrides on the responses it returns to viewers. By defining a policy you can, for example, enforce security-related headers (such as `Strict-Transport-Security` or `Content-Security-Policy`), apply custom cache-control directives, or expose additional headers to browsers for client-side logic. Once created, a response headers policy can be associated with one or more CloudFront distributions, allowing consistent header behaviour across multiple delivery configurations. +For full details see the AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/response-headers-policies.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_response_headers_policy.id` + +## Supported Methods + +- `GET`: Get Response Headers Policy by ID +- `LIST`: List Response Headers Policies +- `SEARCH`: Search Response Headers Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md new file mode 100644 index 00000000..91412fd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudfront-streaming-distribution.md @@ -0,0 +1,23 @@ +--- +title: CloudFront Streaming Distribution +sidebar_label: cloudfront-streaming-distribution +--- + +An Amazon CloudFront Streaming Distribution is a special type of CloudFront distribution optimised for on-demand media streaming (historically using the RTMP protocol) and for serving video content over HTTP/S from an origin such as Amazon S3 or an on-premises media server. It automatically places edge cache nodes close to viewers, reducing latency and bandwidth costs while providing scalability, encryption and access control options. For full details see the official AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-streaming.html + +**Terrafrom Mappings:** + +- `aws_cloudfront_distribution.arn` +- `aws_cloudfront_distribution.id` + +## Supported Methods + +- `GET`: Get a Streaming Distribution by ID +- `LIST`: List Streaming Distributions +- `SEARCH`: Search Streaming Distributions by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Each CloudFront Streaming Distribution is reachable via a unique domain name that ends in `cloudfront.net`, and may also be associated with custom CNAMEs. These domain names appear in DNS records that overmind can discover and connect to the distribution resource. diff --git a/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md b/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md new file mode 100644 index 00000000..eb3b85cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/cloudwatch-alarm.md @@ -0,0 +1,17 @@ +--- +title: CloudWatch Alarm +sidebar_label: cloudwatch-alarm +--- + +An Amazon CloudWatch Alarm watches a single CloudWatch metric (or a maths expression based on one or more metrics) and performs one or more actions when the metric breaches a threshold for a specified number of evaluation periods. Typical actions include sending an SNS notification, invoking an Auto Scaling policy or stopping, terminating, rebooting or recovering an EC2 instance. Alarms are therefore often a critical part of operational resilience and cost-control strategies, and mis-configuration can lead to missed incidents or unwanted automated actions. +Official documentation: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html + +**Terraform Mappings:** + +- `aws_cloudwatch_metric_alarm.alarm_name` + +## Supported Methods + +- `GET`: Get an alarm by name +- `LIST`: List all alarms +- `SEARCH`: Search for alarms. This accepts JSON in the format of `cloudwatch.DescribeAlarmsForMetricInput` diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md new file mode 100644 index 00000000..0d08e744 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-connection.md @@ -0,0 +1,30 @@ +--- +title: Connection +sidebar_label: directconnect-connection +--- + +An AWS Direct Connect Connection represents a single dedicated network circuit between your on-premises environment (or colocation facility) and an AWS Direct Connect location. By provisioning a connection you obtain a physical 1 Gbps, 10 Gbps or 100 Gbps port on an AWS router, through which you can create one or more virtual interfaces to reach AWS services or your VPCs. A connection is the fundamental building-block for achieving consistent, low-latency private connectivity into AWS, bypassing the public Internet and allowing you to commit to specific bandwidth and service-level requirements. See the official AWS documentation for further details: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithConnections.html + +**Terrafrom Mappings:** + +- `aws_dx_connection.id` + +## Supported Methods + +- `GET`: Get a connection by ID +- `LIST`: List all connections +- `SEARCH`: Search connection by ARN + +## Possible Links + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +A Link Aggregation Group (LAG) can aggregate one or more individual connections into a single managed logical interface. A connection may belong to a LAG, and conversely a LAG lists each underlying connection that forms part of the group. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every connection is terminated at a specific Direct Connect location (e.g. an Equinix or Digital Realty data centre). The connection resource references its chosen location to indicate where the physical port is installed. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Virtual interfaces (public, private or transit) are configured on top of a connection to carry customer traffic. Each virtual interface is associated with exactly one connection (or LAG), while a single connection can host multiple virtual interfaces for different routing purposes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md new file mode 100644 index 00000000..1e33f6b8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-customer-metadata.md @@ -0,0 +1,13 @@ +--- +title: Customer Metadata +sidebar_label: directconnect-customer-metadata +--- + +Customer Metadata represents the customer agreement that is on file for your AWS account in relation to AWS Direct Connect. The record contains information such as the name and Amazon Resource Name (ARN) of the agreement, its current revision and status, and the Region in which the agreement applies. Being able to inspect this resource lets you confirm that the correct contractual terms have been accepted before you attempt to create or modify Direct Connect connections, helping you avoid deployment failures that stem from missing or outdated agreements. +For further details see the AWS API documentation: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeCustomerMetadata.html + +## Supported Methods + +- `GET`: Get a customer agreement by name +- `LIST`: List all customer agreements +- `SEARCH`: Search customer agreements by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md new file mode 100644 index 00000000..54d9add6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association-proposal.md @@ -0,0 +1,24 @@ +--- +title: Direct Connect Gateway Association Proposal +sidebar_label: directconnect-direct-connect-gateway-association-proposal +--- + +An AWS Direct Connect Gateway Association Proposal represents a cross-account request to attach a Virtual Private Gateway (VGW) or Transit Gateway (TGW) to an existing Direct Connect Gateway (DXGW). +The proposal is created by the owner of the VGW/TGW and must be accepted by the DXGW owner before the association is established. It contains details such as allowed prefixes and the identifiers of the gateways involved, providing both parties with a clear record of what will change once the proposal is accepted. +For more information, see the official AWS API documentation: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_CreateDirectConnectGatewayAssociationProposal.html + +**Terrafrom Mappings:** + +- `aws_dx_gateway_association_proposal.id` + +## Supported Methods + +- `GET`: Get a Direct Connect Gateway Association Proposal by ID +- `LIST`: List all Direct Connect Gateway Association Proposals +- `SEARCH`: Search Direct Connect Gateway Association Proposals by ARN + +## Possible Links + +### [`directconnect-direct-connect-gateway-association`](/sources/aws/Types/directconnect-direct-connect-gateway-association) + +A proposal, once accepted, becomes a Direct Connect Gateway Association. Therefore, every accepted `directconnect-direct-connect-gateway-association-proposal` will have a corresponding `directconnect-direct-connect-gateway-association` resource that represents the live attachment between the DXGW and the VGW/TGW. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md new file mode 100644 index 00000000..2f7f64bd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-association.md @@ -0,0 +1,23 @@ +--- +title: Direct Connect Gateway Association +sidebar_label: directconnect-direct-connect-gateway-association +--- + +A Direct Connect Gateway Association represents the attachment of a virtual private gateway (VGW) or a transit gateway (TGW) to an AWS Direct Connect gateway. Once associated, the on-premises network that is connected through an AWS Direct Connect dedicated or hosted connection can reach the VPCs behind the VGW/TGW, even if they are in different AWS Regions. +For more detail, see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways-intro.html#direct-connect-gateway-associations + +**Terraform Mappings:** + +- `aws_dx_gateway_association.id` + +## Supported Methods + +- `GET`: Get a direct connect gateway association by direct connect gateway ID and virtual gateway ID +- ~~`LIST`~~ +- `SEARCH`: Search direct connect gateway associations by direct connect gateway ID + +## Possible Links + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +A Direct Connect Gateway Association is a child resource of a Direct Connect Gateway, so every association is linked to the Direct Connect Gateway to which the VGW/TGW is attached. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md new file mode 100644 index 00000000..86f6233b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway-attachment.md @@ -0,0 +1,23 @@ +--- +title: Direct Connect Gateway Attachment +sidebar_label: directconnect-direct-connect-gateway-attachment +--- + +An AWS Direct Connect **gateway attachment** represents the binding between a Direct Connect Gateway and a Virtual Interface (VIF). When the attachment is in the `attached` state, traffic that reaches the VIF can be routed to any VPCs or on-premises networks that are associated with the gateway, even across accounts or Regions. +For a full description of the concept, states, and quotas involved, see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways.html#dx-gateway-attachments + +## Supported Methods + +- `GET`: Get a direct connect gateway attachment by DirectConnectGatewayId/VirtualInterfaceId +- ~~`LIST`~~ +- `SEARCH`: Search direct connect gateway attachments for given VirtualInterfaceId + +## Possible Links + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +Each gateway attachment belongs to exactly one Direct Connect Gateway. Overmind links the attachment back to its parent gateway so you can see every VIF that is currently associated with that gateway. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +The attachment is also linked to the Virtual Interface that is being attached. This lets you trace which VIFs are connected to which gateways and, in turn, to the networks that sit behind those gateways. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md new file mode 100644 index 00000000..4deb30f5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-direct-connect-gateway.md @@ -0,0 +1,17 @@ +--- +title: Direct Connect Gateway +sidebar_label: directconnect-direct-connect-gateway +--- + +An AWS Direct Connect gateway is a global virtual routing resource that allows you to attach one or more Direct Connect private virtual interfaces to one or more Virtual Private Gateways (VGWs) or Transit Gateways (TGWs) across any AWS Region (with the exception of the AWS China Regions). By decoupling the physical Direct Connect connection from a specific VPC or Region, it simplifies multi-region and multi-account network architectures, provides centralised route control, and reduces the number of BGP sessions that need to be managed. +For a detailed overview, refer to the official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/direct-connect-gateways.html + +**Terrafrom Mappings:** + +- `aws_dx_gateway.id` + +## Supported Methods + +- `GET`: Get a direct connect gateway by ID +- `LIST`: List all direct connect gateways +- `SEARCH`: Search direct connect gateway by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md new file mode 100644 index 00000000..332dadab --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-hosted-connection.md @@ -0,0 +1,31 @@ +--- +title: Hosted Connection +sidebar_label: directconnect-hosted-connection +--- + +A **Hosted Connection** is an AWS Direct Connect circuit that is provisioned for you by an AWS Direct Connect Delivery Partner on their own network infrastructure and then allocated to your AWS account. It provides a dedicated, layer-2 link that terminates at an AWS Direct Connect location and can be used to create virtual interfaces (VIFs) to access AWS services or your VPCs. Unlike dedicated connections, hosted connections are requested from the partner rather than from AWS directly, and their capacity is limited to 50 Mbps, 100 Mbps, 200 Mbps, 300 Mbps, 400 Mbps or 500 Mbps. +See the official AWS documentation for full details: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithConnections.html#HostedConnections + +**Terrafrom Mappings:** + +- `aws_dx_hosted_connection.id` + +## Supported Methods + +- `GET`: Get a Hosted Connection by connection ID +- ~~`LIST`~~ +- `SEARCH`: Search Hosted Connections by Interconnect or LAG ID + +## Possible Links + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +A hosted connection can be delivered over a Link Aggregation Group (LAG). In this case the LAG is the parent resource that physically contains the hosted connection, so the hosted connection links **to** its associated LAG. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every hosted connection terminates at a specific AWS Direct Connect location (for example, a colocation data centre). The hosted connection therefore links **to** the location where its physical port is situated. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +After a hosted connection becomes available you create one or more virtual interfaces on top of it. These virtual interfaces depend on the hosted connection, so they link **from** the hosted connection. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md new file mode 100644 index 00000000..aed4e26f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-interconnect.md @@ -0,0 +1,27 @@ +--- +title: Interconnect +sidebar_label: directconnect-interconnect +--- + +An AWS Direct Connect **Interconnect** is a high-capacity physical Ethernet link (10 Gbps or 100 Gbps) between an AWS Direct Connect location and the network of an approved network service provider. The provider uses the interconnect to carve out and allocate Hosted Connections or Hosted Virtual Interfaces for individual customer accounts, allowing many end-users to share the same physical infrastructure while maintaining logical separation and security. In Overmind, the **directconnect-interconnect** type lets you surface configuration details (such as bandwidth, location, and operational state) and map its relationships to other Direct Connect resources so you can spot mis-configuration or single-point-of-failure risks before deployment. +For authoritative information see the AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithInterconnects.html + +## Supported Methods + +- `GET`: Get a Interconnect by InterconnectId +- `LIST`: List all Interconnects +- `SEARCH`: Search Interconnects by ARN + +## Possible Links + +### [`directconnect-hosted-connection`](/sources/aws/Types/directconnect-hosted-connection) + +Hosted connections are provisioned on top of an Interconnect. Each hosted connection link points back to the parent Interconnect that physically carries its traffic. + +### [`directconnect-lag`](/sources/aws/Types/directconnect-lag) + +LAGs (Link Aggregation Groups) created on an Interconnect combine multiple physical ports of that Interconnect into a single logical interface, increasing bandwidth and providing redundancy. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every Interconnect terminates at a specific Direct Connect location such as an AWS-aligned colocation facility; this link shows where the Interconnect is physically hosted. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md new file mode 100644 index 00000000..e7c62741 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-lag.md @@ -0,0 +1,31 @@ +--- +title: Link Aggregation Group +sidebar_label: directconnect-lag +--- + +An AWS Direct Connect **Link Aggregation Group (LAG)** allows you to combine multiple physical Direct Connect connections into a single, logical interface. Doing so simplifies management, provides higher aggregate bandwidth and offers built-in resiliency: if one underlying connection goes down, traffic is automatically redistributed across the remaining links. Each LAG behaves as a single port on the AWS side while still exposing the individual connections (with their own light-levels and alarms) for troubleshooting. +Official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/lag.html + +**Terrafrom Mappings:** + +- `aws_dx_lag.id` + +## Supported Methods + +- `GET`: Get a Link Aggregation Group by ID +- `LIST`: List all Link Aggregation Groups +- `SEARCH`: Search Link Aggregation Group by ARN + +## Possible Links + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +A LAG is essentially a collection of Direct Connect connections. Each linked `directconnect-connection` represents one of the physical ports that has been bundled into the LAG. + +### [`directconnect-hosted-connection`](/sources/aws/Types/directconnect-hosted-connection) + +Hosted connections can also be associated with a LAG. Overmind links these `directconnect-hosted-connection` resources to show which hosted (customer-provisioned) circuits are aggregated under the same LAG. + +### [`directconnect-location`](/sources/aws/Types/directconnect-location) + +Every LAG is created at a specific AWS Direct Connect location (data centre or colocation facility). The `directconnect-location` link identifies the physical site where the LAG’s constituent connections terminate. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md new file mode 100644 index 00000000..5d5f6c1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-location.md @@ -0,0 +1,17 @@ +--- +title: Direct Connect Location +sidebar_label: directconnect-location +--- + +An AWS Direct Connect Location represents one of the globally distributed, carrier-neutral data-centre facilities where you can order and terminate an AWS Direct Connect dedicated circuit. Each location has a unique location code that you reference when requesting a connection, viewing available port speeds, generating LOAs, or validating the physical site of an existing circuit. Understanding which locations are available – and the risks or constraints linked to each – helps you design resilient, low-latency connectivity between your on-premises network and AWS. +For full details see the official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithLocations.html + +**Terrafrom Mappings:** + +- `aws_dx_location.location_code` + +## Supported Methods + +- `GET`: Get a Location by its code +- `LIST`: List all Direct Connect Locations +- `SEARCH`: Search Direct Connect Locations by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md new file mode 100644 index 00000000..cf5c91d4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-router-configuration.md @@ -0,0 +1,23 @@ +--- +title: Router Configuration +sidebar_label: directconnect-router-configuration +--- + +AWS Direct Connect can automatically generate a sample configuration that you can paste into the customer-side router that terminates a private, public or transit virtual interface. The Router Configuration object represents that text file. Because the template is created by AWS specifically for the selected virtual interface it already contains the correct BGP ASN, VLAN, IP addressing and other parameters for the connection, reducing the chance of a mis-configuration. +Full details of the API can be found in the AWS Direct Connect API Reference: https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DescribeRouterConfiguration.html + +**Terrafrom Mappings:** + +- `aws_dx_router_configuration.virtual_interface_id` + +## Supported Methods + +- `GET`: Get a Router Configuration by Virtual Interface ID +- ~~`LIST`~~ +- `SEARCH`: Search Router Configuration by ARN + +## Possible Links + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +A Router Configuration is generated for, and therefore has a **1-to-1** relationship with, a Direct Connect Virtual Interface. The link allows you to navigate from the virtual interface to the exact configuration you should apply to your on-premises router (and vice-versa), making it easier to validate that the interface has been deployed according to the recommended configuration. diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md new file mode 100644 index 00000000..24a64425 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-gateway.md @@ -0,0 +1,14 @@ +--- +title: Direct Connect Virtual Gateway +sidebar_label: directconnect-virtual-gateway +--- + +A Direct Connect virtual gateway (sometimes called a virtual private gateway, or **VGW**) is the AWS-managed end-point that terminates a private virtual interface and presents it to your Amazon VPC. It provides the control-plane for routing traffic between your on-premises network and one or more VPCs over an AWS Direct Connect link, removing the need to run VPN hardware or BGP sessions inside the VPC itself. By querying this resource, Overmind can show you which VPCs and Direct Connect virtual interfaces are affected, surface any missing or insecure route advertisements, and highlight configuration drift _before_ changes are deployed. + +For more information, refer to the AWS Direct Connect documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/virtual-gateway.html + +## Supported Methods + +- `GET`: Get a virtual gateway by ID +- `LIST`: List all virtual gateways +- `SEARCH`: Search virtual gateways by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md new file mode 100644 index 00000000..4d7222f1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/directconnect-virtual-interface.md @@ -0,0 +1,41 @@ +--- +title: Virtual Interface +sidebar_label: directconnect-virtual-interface +--- + +A Virtual Interface (VIF) is the logical layer that sits on top of an AWS Direct Connect physical connection and provides Layer 3 access into AWS. Three flavours are available—private, public and transit—each supporting different routing destinations and services. A VIF defines the VLAN, BGP peering IPs, Autonomous System Numbers (ASNs), jumbo-frame settings and, optionally, a Direct Connect Gateway association. +Official AWS documentation: https://docs.aws.amazon.com/directconnect/latest/UserGuide/WorkingWithVirtualInterfaces.html + +**Terrafrom Mappings:** + +- `aws_dx_private_virtual_interface.id` +- `aws_dx_public_virtual_interface.id` +- `aws_dx_transit_virtual_interface.id` + +## Supported Methods + +- `GET`: Get a virtual interface by ID +- `LIST`: List all virtual interfaces +- `SEARCH`: Search virtual interfaces by connection ID + +## Possible Links + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +Every VIF must be created against a Direct Connect physical connection. The link lets you trace which circuit (location, port speed, AWS account) the virtual interface is riding on. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +Private and transit VIFs can be attached to a Direct Connect Gateway to reach multiple VPCs or on-premises networks. This link shows that association, helping you see the downstream network blast-radius of a VIF change. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +The BGP peer IPs configured on a VIF belong to specific IPv4/IPv6 networks. Linking to RDAP IP network objects allows visibility of route-origin information and public registration data for those peer addresses. + +### [`directconnect-direct-connect-gateway-attachment`](/sources/aws/Types/directconnect-direct-connect-gateway-attachment) + +When a VIF is associated with a Direct Connect Gateway, an attachment resource is created in AWS. This link maps the VIF to its attachment object so you can understand and audit that relationship. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Some organisations create multiple VIFs on the same physical connection for isolation (e.g., production vs. test). Overmind links sibling VIFs so you can view parallel logical circuits that share the same underlay. diff --git a/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md new file mode 100644 index 00000000..5b2aa507 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-backup.md @@ -0,0 +1,18 @@ +--- +title: DynamoDB Backup +sidebar_label: dynamodb-backup +--- + +A DynamoDB Backup represents a point-in-time, fully-managed snapshot of an Amazon DynamoDB table, including all of its data and global secondary indexes. Back-ups can be created on demand or retained automatically through continuous point-in-time recovery (PITR). They allow you to restore the table to any state within the retention window, or to clone the data into a new table in the same or another region for testing and disaster-recovery purposes. For further details, see the official AWS documentation: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/BackupRestore.html + +## Supported Methods + +- ~~`GET`~~ +- `LIST`: List all DynamoDB backups +- `SEARCH`: Search for a DynamoDB backup by table name + +## Possible Links + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +Each backup is intrinsically tied to the table from which it was taken; Overmind therefore links a `dynamodb-backup` item to its source `dynamodb-table` so you can trace data-protection coverage, understand restore scopes, and assess the blast radius of table changes or deletions. diff --git a/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md new file mode 100644 index 00000000..85dfe110 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/dynamodb-table.md @@ -0,0 +1,27 @@ +--- +title: DynamoDB Table +sidebar_label: dynamodb-table +--- + +Amazon DynamoDB is AWS’s fully-managed NoSQL database service, providing single-millisecond latency at virtually any scale. A DynamoDB table is the primary container for data, storing items as key–value pairs and supporting features such as on-demand or provisioned capacity, global replication, streams and automatic encryption at rest. +For a full description of table capabilities, limits and API operations, see the official AWS documentation: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Table.html + +**Terrafrom Mappings:** + +- `aws_dynamodb_table.arn` + +## Supported Methods + +- `GET`: Get a DynamoDB table by name +- `LIST`: List all DynamoDB tables +- `SEARCH`: Search for DynamoDB tables by ARN + +## Possible Links + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +When a table participates in a global table configuration, each regional replica is represented as a separate `dynamodb-table` item. Overmind links these peer replicas so that you can see the full set of regions involved in the same globally replicated table. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If server-side encryption is enabled with a customer-managed KMS key, the table is linked to the `kms-key` that protects its data. This allows you to trace encryption dependencies and assess the impact of key rotation or deletion. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md new file mode 100644 index 00000000..62cf7c5e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-address.md @@ -0,0 +1,31 @@ +--- +title: EC2 Address +sidebar_label: ec2-address +--- + +An EC2 Address represents an Elastic IP (EIP) in AWS – a static, public IPv4 address that you can allocate to your account and assign to running resources such as EC2 instances or network interfaces. Elastic IPs let you mask the failure of a single instance by rapidly remapping the address to another resource, ensuring minimal disruption to services that rely on a fixed public endpoint. See the official AWS documentation for full details: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html + +**Terrafrom Mappings:** + +- `aws_eip.public_ip` +- `aws_eip_association.public_ip` + +## Supported Methods + +- `GET`: Get an EC2 address by Public IP +- `LIST`: List EC2 addresses +- `SEARCH`: Search for EC2 addresses by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An Elastic IP can be attached directly to an EC2 instance; this link shows which instance currently holds (or most recently held) the address, allowing you to trace external reachability back to the compute resource. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The Elastic IP is ultimately a routable IPv4 address; this link connects the high-level EIP object to the underlying IP entity so that you can track dependencies and overlap with other networking resources in your estate. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +When an Elastic IP is associated with an EC2 instance, it is actually bound to one of the instance’s network interfaces (ENIs). This link identifies the specific ENI, enabling deeper analysis of traffic flow, security groups, and subnet placement that pertain to the EIP. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md new file mode 100644 index 00000000..51b2b13a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation-fleet.md @@ -0,0 +1,19 @@ +--- +title: Capacity Reservation Fleet +sidebar_label: ec2-capacity-reservation-fleet +--- + +A Capacity Reservation Fleet is an Amazon EC2 resource that lets you create and manage a group of Capacity Reservations in a single operation. By specifying instance attributes such as instance types, platforms and Availability Zones, you can ensure that the compute capacity your workload requires will be held for you ahead of time, even during periods of high demand. This is especially useful when you need to guarantee that a heterogeneous mix of instances will be available at launch, for example during large-scale events or disaster-recovery drills. +For more information, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateCapacityReservationFleet.html + +## Supported Methods + +- `GET`: Get a capacity reservation fleet by ID +- `LIST`: List capacity reservation fleets +- `SEARCH`: Search capacity reservation fleets by ARN + +## Possible Links + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +A Capacity Reservation Fleet is essentially an umbrella object that owns one or more individual Capacity Reservations. Each linked `ec2-capacity-reservation` represents a single slice of capacity that was created as part of the fleet’s allocation strategy, and tracking these links lets you understand which reservations belong to which fleet and how capacity is distributed across instance types and Availability Zones. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md new file mode 100644 index 00000000..1ef498bd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-capacity-reservation.md @@ -0,0 +1,27 @@ +--- +title: Capacity Reservation +sidebar_label: ec2-capacity-reservation +--- + +An Amazon EC2 Capacity Reservation is an AWS construct that sets aside compute capacity for one or more instance types in a specific Availability Zone, guaranteeing that the reserved capacity is available whenever you need to launch instances. Capacity Reservations can be created individually or as members of a Capacity Reservation Fleet, allowing you to reserve capacity across several instance types and Zones in a single request. This is particularly useful for workloads that must start at short notice, seasonal traffic peaks, or disaster-recovery scenarios. +For a detailed explanation, refer to the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-capacity-reservations.html + +**Terrafrom Mappings:** + +- `aws_ec2_capacity_reservation_fleet.id` + +## Supported Methods + +- `GET`: Get a capacity reservation fleet by ID +- `LIST`: List capacity reservation fleets +- `SEARCH`: Search capacity reservation fleets by ARN + +## Possible Links + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +A Capacity Reservation can be scoped to a placement group. When the `placement_group_arn` (or equivalent Terraform argument) is specified, Overmind links the reservation to that placement group so you can see how the reserved capacity aligns with your low-latency or HPC topology. + +### [`ec2-capacity-reservation-fleet`](/sources/aws/Types/ec2-capacity-reservation-fleet) + +If the reservation was created as part of a Capacity Reservation Fleet, Overmind links it to its parent fleet. This lets you trace individual reservations back to the fleet that manages them and understand how they contribute to the overall pool of reserved capacity across instance types and Availability Zones. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md new file mode 100644 index 00000000..a4b31106 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-egress-only-internet-gateway.md @@ -0,0 +1,23 @@ +--- +title: Egress Only Internet Gateway +sidebar_label: ec2-egress-only-internet-gateway +--- + +An Egress Only Internet Gateway (EOIGW) is a horizontally-scaled, highly available AWS VPC component that allows outbound-only IPv6 traffic from your VPC to the internet while preventing unsolicited inbound connections. Unlike a standard Internet Gateway, an EOIGW supports IPv6 traffic exclusively and enforces one-way egress, making it a useful control when you want resources such as application servers to reach external IPv6 services without being directly reachable from the internet. +For detailed information, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html + +**Terrafrom Mappings:** + +- `egress_only_internet_gateway.id` + +## Supported Methods + +- `GET`: Get an egress only internet gateway by ID +- `LIST`: List all egress only internet gateways +- `SEARCH`: Search egress only internet gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An EOIGW is attached to exactly one VPC. Overmind represents this relationship so that you can navigate from a VPC to its associated egress-only internet gateways and understand which networks can initiate outbound IPv6 traffic to the internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md new file mode 100644 index 00000000..9c55da00 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-iam-instance-profile-association.md @@ -0,0 +1,23 @@ +--- +title: IAM Instance Profile Association +sidebar_label: ec2-iam-instance-profile-association +--- + +An IAM Instance Profile Association represents the live binding between an Amazon EC2 instance and an IAM instance profile (which in turn wraps an IAM role). The association determines which IAM permissions the instance receives via its metadata service. Only one profile can be associated with an instance at a time; changing the association effectively swaps the role that the instance assumes. +For further information see the AWS API reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_IamInstanceProfileAssociation.html + +## Supported Methods + +- `GET`: Get an IAM Instance Profile Association by ID +- `LIST`: List all IAM Instance Profile Associations +- `SEARCH`: Search IAM Instance Profile Associations by ARN + +## Possible Links + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +The association points to exactly one IAM instance profile, identifying the set of IAM permissions that will be handed to the EC2 instance. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Each association belongs to a single EC2 instance, indicating which profile (and hence which role) the instance is currently using. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md new file mode 100644 index 00000000..1d1ed76d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-image.md @@ -0,0 +1,17 @@ +--- +title: Amazon Machine Image (AMI) +sidebar_label: ec2-image +--- + +An Amazon Machine Image (AMI) is a pre-configured, read-only template that defines the software stack required to launch an Amazon EC2 instance. It typically contains an operating system, application server, and any additional software or configuration needed for your workload. By selecting or creating an AMI you can reproduce identical instances at scale, roll back to known-good states, or share hardened golden images across accounts and Regions. +For a full explanation of AMIs, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html. + +**Terrafrom Mappings:** + +- `aws_ami.id` + +## Supported Methods + +- `GET`: Get an AMI by ID +- `LIST`: List all AMIs +- `SEARCH`: Search AMIs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md new file mode 100644 index 00000000..0b47e4f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-event-window.md @@ -0,0 +1,19 @@ +--- +title: EC2 Instance Event Window +sidebar_label: ec2-instance-event-window +--- + +An EC2 Instance Event Window is an Amazon EC2 scheduling feature that lets you specify one or more preferred time ranges during which planned AWS maintenance events (for example, a reboot, stop/start or software update) may be applied to your instances. By defining event windows, you retain greater control over when service-initiated interruptions occur, enabling you to align maintenance with your own change-management processes and minimise unplanned impact. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/event-windows.html + +## Supported Methods + +- `GET`: Get an event window by ID +- `LIST`: List all event windows +- `SEARCH`: Search for event windows by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An event window can be associated with one or more EC2 instances. When a linkage exists, those instances will only receive scheduled maintenance events during the time ranges defined in the referenced EC2 Instance Event Window. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md new file mode 100644 index 00000000..8b065a05 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance-status.md @@ -0,0 +1,14 @@ +--- +title: EC2 Instance Status +sidebar_label: ec2-instance-status +--- + +An EC2 Instance Status record summarises the current health of a running Amazon Elastic Compute Cloud (EC2) instance. AWS performs two types of status checks—system checks (that assess the underlying host and network) and instance checks (that confirm the guest operating system is reachable). Together they indicate whether the instance is able to accept traffic and function as expected. +Overmind ingests these status objects so that you can surface potential availability risks (e.g. persistent instance check failures) before promoting or modifying a deployment. +For a detailed explanation of how AWS generates and interprets these checks, see the [official AWS documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/status-checks.html). + +## Supported Methods + +- `GET`: Get an EC2 instance status by Instance ID +- `LIST`: List all EC2 instance statuses +- `SEARCH`: Search EC2 instance statuses by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md new file mode 100644 index 00000000..2d969a8f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-instance.md @@ -0,0 +1,67 @@ +--- +title: EC2 Instance +sidebar_label: ec2-instance +--- + +An Amazon EC2 instance is a resizable virtual server that runs in the AWS cloud and provides the compute layer of most workloads. Instances can be started, stopped, terminated, resized and placed into different networking or storage configurations, allowing you to run applications without purchasing physical hardware. For full details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Instances.html + +**Terrafrom Mappings:** + +- `aws_instance.id` +- `aws_instance.arn` + +## Supported Methods + +- `GET`: Get an EC2 instance by ID +- `LIST`: List all EC2 instances +- `SEARCH`: Search EC2 instances by ARN + +## Possible Links + +### [`ec2-instance-status`](/sources/aws/Types/ec2-instance-status) + +Represents the current state of the instance (pending, running, stopping, stopped, etc.), health checks, and scheduled events. + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +An instance can be launched with an IAM instance profile, enabling the software running on it to assume a role and gain AWS permissions. + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +If the instance is launched into a specific capacity reservation, that reservation object is linked here to show the source of reserved compute capacity. + +### [`ec2-image`](/sources/aws/Types/ec2-image) + +Every EC2 instance is created from an Amazon Machine Image (AMI). This link points to the AMI used at launch time. + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +For Linux and some Windows instances a key pair is specified for SSH/RDP access; the referenced key pair is linked here. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +Instances can be placed in a placement group to influence network performance or availability. This link shows that relationship. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each instance receives one or more private and, optionally, public IP addresses. These addresses are surfaced as separate `ip` resources linked to the instance. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The instance’s primary network interface is attached to a specific subnet; that subnet is linked to reveal networking context. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The subnet (and thus the instance) resides inside a VPC. Linking the VPC shows the broader network boundary and associated routing. + +### [`dns`](/sources/stdlib/Types/dns) + +Public and private DNS names resolve to the instance’s IP addresses; these DNS records are connected through this link. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +One or more security groups control inbound and outbound traffic to the instance network interfaces. Those groups are linked here. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +EBS volumes attached to the instance for root and additional block storage are represented and linked by this type. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md new file mode 100644 index 00000000..209f7d21 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-internet-gateway.md @@ -0,0 +1,23 @@ +--- +title: Internet Gateway +sidebar_label: ec2-internet-gateway +--- + +An Internet Gateway is a highly-available, horizontally-scaled component that provides a Virtual Private Cloud (VPC) with a route to the public Internet. When attached to a VPC and referenced in the route table, it enables resources with public IP addresses—such as EC2 instances, NAT gateways or load balancers—to send and receive traffic to and from the wider Internet. Because it is a managed AWS service, it does not introduce any single point of failure and requires no administration beyond attachment and routing. +For the official AWS documentation, see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html. + +**Terrafrom Mappings:** + +- `aws_internet_gateway.id` + +## Supported Methods + +- `GET`: Get an internet gateway by ID +- `LIST`: List all internet gateways +- `SEARCH`: Search internet gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An Internet Gateway must be attached to exactly one VPC; this link represents that one-to-one relationship. Through it, Overmind can surface configuration drift (for example, if the gateway is detached) and highlight risks such as missing or overly permissive route-table entries that would expose private resources to the Internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md new file mode 100644 index 00000000..dc2d6700 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-key-pair.md @@ -0,0 +1,17 @@ +--- +title: Key Pair +sidebar_label: ec2-key-pair +--- + +An Amazon EC2 Key Pair is a set of cryptographic keys that enables secure, password-less SSH access to your EC2 instances and other compatible services. The public key is stored in AWS, while the private key is downloaded and managed by you. If the private key is compromised or lost, access to the associated instances is at risk, so tracking key pairs is critical for security posture. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html + +**Terrafrom Mappings:** + +- `aws_key_pair.id` + +## Supported Methods + +- `GET`: Get a key pair by name +- `LIST`: List all key pairs +- `SEARCH`: Search for key pairs by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md new file mode 100644 index 00000000..993c306c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template-version.md @@ -0,0 +1,51 @@ +--- +title: Launch Template Version +sidebar_label: ec2-launch-template-version +--- + +An AWS EC2 Launch Template Version is an immutable snapshot of all the parameters that make up a particular revision of an EC2 launch template – such as AMI ID, instance type, network interfaces, storage, tags and user-data. Each version can be referenced directly when launching instances or by services like Auto Scaling, Spot Fleets and EC2 Fleet, giving you reproducible, auditable instance configuration. +For full details see the AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateVersion.html + +## Supported Methods + +- `GET`: Get a launch template version by `{templateId}.{version}` +- `LIST`: List all launch template versions +- `SEARCH`: Search launch template versions by ARN + +## Possible Links + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +The version can embed zero or more network interface specifications, each of which becomes an `ec2-network-interface` when an instance is launched from the template. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +Within the network interface or placement settings the version may reference a specific subnet ID, tying the launched instance to that `ec2-subnet`. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Security group IDs listed in the template control inbound and outbound traffic for instances started from this version, linking it to the relevant `ec2-security-group` resources. + +### [`ec2-image`](/sources/aws/Types/ec2-image) + +Every launch template version specifies an AMI ID, creating a dependency on the corresponding `ec2-image`. + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +If a key name is supplied, the version references an `ec2-key-pair` used for SSH access to Linux instances or password encryption for Windows instances. + +### [`ec2-snapshot`](/sources/aws/Types/ec2-snapshot) + +EBS block-device mappings in the template can point to snapshot IDs, establishing a relationship with the relevant `ec2-snapshot` objects. + +### [`ec2-capacity-reservation`](/sources/aws/Types/ec2-capacity-reservation) + +The template may include a capacity reservation target, associating the version with a specific `ec2-capacity-reservation`. + +### [`ec2-placement-group`](/sources/aws/Types/ec2-placement-group) + +Placement settings in the version can name a placement group, indicating that instances should launch into the linked `ec2-placement-group`. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Static private or public IP addresses specified in the network interface configuration will be materialised as `ip` resources when the template version is used to launch an instance. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md new file mode 100644 index 00000000..81457635 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-launch-template.md @@ -0,0 +1,17 @@ +--- +title: Launch Template +sidebar_label: ec2-launch-template +--- + +An EC2 Launch Template is an AWS resource that stores the complete configuration needed to spin up one or more Amazon EC2 instances, including AMI ID, instance type, network settings, user-data scripts, and optional purchasing options such as Spot or On-Demand. By saving these parameters in a versioned template, teams can reproduce environments consistently, roll back to previous configurations, and simplify autoscaling and fleet operations. +Official documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html + +**Terrafrom Mappings:** + +- `aws_launch_template.id` + +## Supported Methods + +- `GET`: Get a launch template by ID +- `LIST`: List all launch templates +- `SEARCH`: Search for launch templates by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md new file mode 100644 index 00000000..e3450d15 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-nat-gateway.md @@ -0,0 +1,35 @@ +--- +title: NAT Gateway +sidebar_label: ec2-nat-gateway +--- + +A NAT Gateway is an AWS managed network appliance that enables instances in a private subnet to initiate outbound IPv4 (and, in the case of an **NAT Gateway (v2)**, IPv6) traffic to the internet or other AWS services, while preventing unsolicited inbound connections from the public internet. It provides higher bandwidth and easier management compared to NAT instances, and is designed to be highly available within an Availability Zone. +For a full description of its features and limitations, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html + +**Terrafrom Mappings:** + +- `aws_nat_gateway.id` + +## Supported Methods + +- `GET`: Get a NAT Gateway by ID +- `LIST`: List all NAT gateways +- `SEARCH`: Search for NAT gateways by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The NAT Gateway is always created inside a specific VPC; this link lets you trace which virtual network the gateway belongs to. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A NAT Gateway is placed in exactly one subnet. This link shows the subnet that hosts the gateway’s elastic network interface. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +Each NAT Gateway is automatically assigned an elastic network interface (ENI). Following this link reveals the ENI that represents the gateway inside the subnet. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +When you create a NAT Gateway you must allocate at least one Elastic IP address. This link connects the gateway to the public IP(s) it advertises to the internet. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md new file mode 100644 index 00000000..2752cf06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-acl.md @@ -0,0 +1,27 @@ +--- +title: Network ACL +sidebar_label: ec2-network-acl +--- + +A Network Access Control List (ACL) is a stateless, virtual firewall that controls inbound and outbound traffic at the subnet boundary within an Amazon Virtual Private Cloud (VPC). Each rule in a Network ACL is evaluated in order, enabling or denying traffic based on protocol, port range and source or destination IP. Unlike security groups, Network ACLs apply to all resources inside the associated subnets, making them a coarse-grained layer of network security. +For full details, see the AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html + +**Terrafrom Mappings:** + +- `aws_network_acl.id` + +## Supported Methods + +- `GET`: Get a network ACL +- `LIST`: List all network ACLs +- `SEARCH`: Search for network ACLs by ARN + +## Possible Links + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A Network ACL is attached to one or more subnets; traffic entering or leaving those subnets is evaluated against the ACL’s rule set. Overmind therefore links an `ec2-network-acl` to the `ec2-subnet` resources it governs. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every Network ACL exists inside a single VPC. Overmind links an `ec2-network-acl` to its parent `ec2-vpc` to show the broader network context in which the ACL operates. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md new file mode 100644 index 00000000..7eccd3a2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface-permission.md @@ -0,0 +1,19 @@ +--- +title: Network Interface Permission +sidebar_label: ec2-network-interface-permission +--- + +An EC2 **Network Interface Permission** represents the right of an AWS principal (usually another AWS account) to attach a specific Elastic Network Interface (ENI) to an instance in that principal’s account. By creating or revoking these permissions you can share network interfaces across accounts in a controlled manner without transferring ownership. +Further information can be found in the AWS official documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_NetworkInterfacePermission.html + +## Supported Methods + +- `GET`: Get a network interface permission by ID +- `LIST`: List all network interface permissions +- `SEARCH`: Search network interface permissions by ARN + +## Possible Links + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +A network interface permission is always associated with a single network interface; the linked `ec2-network-interface` item is the ENI to which this permission applies. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md new file mode 100644 index 00000000..3379b135 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-network-interface.md @@ -0,0 +1,43 @@ +--- +title: EC2 Network Interface +sidebar_label: ec2-network-interface +--- + +An Amazon Elastic Compute Cloud (EC2) Network Interface – often referred to as an Elastic Network Interface (ENI) – is a virtual network card that can be attached to an EC2 instance. It provides the instance with connectivity within a Virtual Private Cloud (VPC) and, optionally, to the public Internet. Each ENI contains a primary private IPv4 address, one or more secondary IPv4 addresses, IPv6 addresses if enabled, one or more security groups, a MAC address, and, when required, an Elastic IP address or a public DNS name. ENIs can be moved between instances, created in advance, or used for high-availability network configurations such as dual-homed instances. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html + +**Terrafrom Mappings:** + +- `aws_network_interface.id` + +## Supported Methods + +- `GET`: Get a network interface by ID +- `LIST`: List all network interfaces +- `SEARCH`: Search network interfaces by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An ENI can be attached to an EC2 instance, providing that instance with network connectivity. Overmind links the interface to the instance(s) it is or has been attached to. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Each ENI is associated with one or more security groups. These groups define the inbound and outbound traffic rules applied at the interface level. The link shows which security groups control traffic for the ENI. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The ENI owns one or more IP addresses (private IPv4, secondary IPv4, IPv6, and optionally Elastic IP). This relationship exposes the individual IP resources attached to the interface. + +### [`dns`](/sources/stdlib/Types/dns) + +If an ENI has a public IPv4 address, AWS automatically creates a corresponding public DNS name; private DNS names may also be present within the VPC. Overmind links these DNS records to the ENI. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +An ENI is created inside a specific subnet. The subnet determines the address range from which the ENI’s private IPs are allocated and the availability zone in which it resides. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every ENI exists within a single VPC, inheriting that VPC’s routing tables, DHCP options, and network ACLs. This link shows the parent VPC for the interface. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md new file mode 100644 index 00000000..7364f9bc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-placement-group.md @@ -0,0 +1,16 @@ +--- +title: Placement Group +sidebar_label: ec2-placement-group +--- + +An EC2 Placement Group is an AWS construct that lets you influence how Elastic Compute Cloud (EC2) instances are positioned on the underlying hardware. By creating a placement group with a strategy of `cluster`, `spread`, or `partition`, you can optimise for high-bandwidth, low-latency networking, reduce the risk of simultaneous hardware failures, or isolate groups of instances from one another. For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html + +**Terrafrom Mappings:** + +- `aws_placement_group.id` + +## Supported Methods + +- `GET`: Get a placement group by ID +- `LIST`: List all placement groups +- `SEARCH`: Search for placement groups by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md new file mode 100644 index 00000000..26ac06ef --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-reserved-instance.md @@ -0,0 +1,13 @@ +--- +title: Reserved EC2 Instance +sidebar_label: ec2-reserved-instance +--- + +An AWS Reserved EC2 Instance represents a pre-paid or partially pre-paid commitment to run a specific instance type in a given Availability Zone or Region for a fixed term (one or three years). By committing up-front, you can obtain a significant discount compared with on-demand pricing, but you also take on the risk of paying for capacity you might not end up using. Overmind treats each Reserved Instance as its own resource so that you can surface any financial or capacity-planning risk associated with your reservation portfolio before a deployment is made. +For detailed information on how Reserved Instances work, see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/reserved-instances.html + +## Supported Methods + +- `GET`: Get a reserved EC2 instance by ID +- `LIST`: List all reserved EC2 instances +- `SEARCH`: Search reserved EC2 instances by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md new file mode 100644 index 00000000..6b29e850 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-route-table.md @@ -0,0 +1,58 @@ +--- +title: Route Table +sidebar_label: ec2-route-table +--- + +A Route Table in Amazon Virtual Private Cloud (VPC) contains a set of rules, called routes, that determine where network traffic is directed. Each route specifies a destination CIDR block and a target (for example, an Internet Gateway, NAT Gateway, network interface or VPC peering connection). AWS evaluates the routes in the table to decide how packets that leave a subnet are forwarded. A VPC can have multiple route tables, allowing you to implement fine-grained traffic segregation and control. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Route_Tables.html + +**Terrafrom Mappings:** + +- `aws_route_table.id` +- `aws_route_table_association.route_table_id` +- `aws_default_route_table.default_route_table_id` +- `aws_route.route_table_id` + +## Supported Methods + +- `GET`: Get a route table by ID +- `LIST`: List all route tables +- `SEARCH`: Search route tables by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The Route Table is created inside a specific VPC; every table therefore has a one-to-one parent relationship with the VPC in which it resides. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +Subnets are associated with a Route Table. Traffic that originates in a subnet is evaluated against the routes in its associated table. One route table can be linked to many subnets. + +### [`ec2-internet-gateway`](/sources/aws/Types/ec2-internet-gateway) + +A Route Table may contain a route whose target is an Internet Gateway, enabling outbound IPv4 traffic (and inbound responses) for the subnets that use the table. + +### [`ec2-vpc-endpoint`](/sources/aws/Types/ec2-vpc-endpoint) + +Interface and Gateway VPC Endpoints can appear as route targets, directing traffic destined for AWS services or private resources through the endpoint. + +### [`ec2-egress-only-internet-gateway`](/sources/aws/Types/ec2-egress-only-internet-gateway) + +For IPv6 connectivity, a Route Table can include a route to an Egress-only Internet Gateway, allowing outbound-only IPv6 traffic from the associated subnets. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +An individual EC2 instance can be specified as the route target (using its instance ID) when it is acting as a virtual appliance or host-based router. + +### [`ec2-nat-gateway`](/sources/aws/Types/ec2-nat-gateway) + +Routes can target a NAT Gateway, providing Internet access for private subnets while keeping the source IP addresses of instances hidden from the public Internet. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +A specific Elastic Network Interface (ENI) may be used as a route target to forward traffic to appliances such as firewalls or load balancers hosted on that interface. + +### [`ec2-vpc-peering-connection`](/sources/aws/Types/ec2-vpc-peering-connection) + +When traffic needs to flow between two VPCs, a route whose target is a VPC Peering Connection is added to the Route Table, enabling cross-VPC communication. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md new file mode 100644 index 00000000..74c84a62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group-rule.md @@ -0,0 +1,25 @@ +--- +title: Security Group Rule +sidebar_label: ec2-security-group-rule +--- + +A Security Group Rule represents a single ingress or egress rule that belongs to an Amazon EC2 Security Group. Each rule specifies the protocol, port range, source or destination (IP range, prefix list, security group or prefix), and (optionally) a description that determines whether specific network traffic is allowed to reach, or leave, the resources associated with the parent security group. By analysing these rules, Overmind can surface unintended exposure, overly-permissive access, or conflicts before the configuration is deployed. +For full details see the official AWS documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-rules.html + +**Terrafrom Mappings:** + +- `aws_security_group_rule.security_group_rule_id` +- `aws_vpc_security_group_ingress_rule.security_group_rule_id` +- `aws_vpc_security_group_egress_rule.security_group_rule_id` + +## Supported Methods + +- `GET`: Get a security group rule by ID +- `LIST`: List all security group rules +- `SEARCH`: Search security group rules by ARN + +## Possible Links + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Every Security Group Rule belongs to exactly one Security Group; Overmind links the rule back to its parent security group so you can trace how an individual rule contributes to the overall ingress or egress policy applied to your instances and other resources. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md new file mode 100644 index 00000000..ad1fc210 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-security-group.md @@ -0,0 +1,23 @@ +--- +title: Security Group +sidebar_label: ec2-security-group +--- + +An Amazon EC2 Security Group acts as a virtual firewall that regulates inbound and outbound traffic for resources such as EC2 instances, load balancers, and network interfaces within a Virtual Private Cloud (VPC). Rules are stateful, meaning that return traffic is automatically allowed, and can be specified by protocol, port range, and source or destination (CIDR block, prefix list, or another security group). For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html + +**Terrafrom Mappings:** + +- `aws_security_group.id` +- `aws_security_group_rule.security_group_id` + +## Supported Methods + +- `GET`: Get a security group by ID +- `LIST`: List all security groups +- `SEARCH`: Search for security groups by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Each security group is created within a single VPC, inherits its CIDR boundaries, and can only be attached to resources that also reside in that VPC. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md new file mode 100644 index 00000000..513ed38b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-snapshot.md @@ -0,0 +1,19 @@ +--- +title: EC2 Snapshot +sidebar_label: ec2-snapshot +--- + +An Amazon EBS (Elastic Block Store) snapshot is an incremental, point-in-time backup of an EBS volume. Snapshots are stored in Amazon S3 and can be used to restore the original volume, create new volumes in the same or different Availability Zones, and copy data across Regions. They form a key part of disaster-recovery and migration workflows, allowing users to preserve data durability and quickly re-provision storage. +Official documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-creating-snapshot.html + +## Supported Methods + +- `GET`: Get a snapshot by ID +- `LIST`: List all snapshots +- `SEARCH`: Search snapshots by ARN + +## Possible Links + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +A snapshot is created from, and can later be used to recreate, an EBS volume. Overmind links each `ec2-snapshot` to the `ec2-volume` it originated from (and, where relevant, the volumes restored from it), enabling you to trace data lineage and understand the blast radius of any change to the underlying storage. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md new file mode 100644 index 00000000..4822a8cd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-subnet.md @@ -0,0 +1,24 @@ +--- +title: EC2 Subnet +sidebar_label: ec2-subnet +--- + +An EC2 subnet is a logically isolated section of an Amazon Virtual Private Cloud that lets you group resources together and control how traffic flows to and from them. Each subnet resides in a single Availability Zone, inherits the VPC’s CIDR range, and can be configured as public or private depending on whether its routing table points traffic to an Internet Gateway or not. Subnets form the basic building blocks for networking in AWS, determining IP addressing, network reachability, and security-group/network-ACL boundaries. +For full details see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html + +**Terrafrom Mappings:** + +- `aws_route_table_association.subnet_id` +- `aws_subnet.id` + +## Supported Methods + +- `GET`: Get a subnet by ID +- `LIST`: List all subnets +- `SEARCH`: Search for subnets by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Every subnet must belong to exactly one VPC. This relationship allows Overmind to trace how traffic is routed from the subnet through VPC-level components such as Internet Gateways, NAT Gateways, route tables, and network ACLs. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md new file mode 100644 index 00000000..0a8becbb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume-status.md @@ -0,0 +1,19 @@ +--- +title: EC2 Volume Status +sidebar_label: ec2-volume-status +--- + +The EC2 Volume Status resource represents the health information that AWS exposes for every Amazon Elastic Block Store (EBS) volume. Derived from the `DescribeVolumeStatus` API call, it records the results of automated status checks, any events that might affect I/O, and recommended user actions. Monitoring these objects in Overmind lets you spot degraded or impaired volumes before they compromise a deployment. +For a complete description of the data returned by AWS, see the official documentation: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVolumeStatus.html + +## Supported Methods + +- `GET`: Get a volume status by volume ID +- `LIST`: List all volume statuses +- `SEARCH`: Search for volume statuses by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +A Volume Status relates to the EC2 instance that the underlying EBS volume is currently attached to, if any. Overmind links the status object to the instance so you can trace how a failing or impaired volume might impact the workloads running on that instance. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md new file mode 100644 index 00000000..ab7b8ee3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-volume.md @@ -0,0 +1,22 @@ +--- +title: EC2 Volume +sidebar_label: ec2-volume +--- + +An Amazon Elastic Block Store (EBS) volume provides persistent block-level storage for use with Amazon EC2 instances. Volumes can be attached to a single instance at a time (or multiple instances when using Multi-Attach), and retain their data independently of the life-cycle of that instance. Sizes, performance characteristics and encryption settings are configurable, allowing teams to tailor storage to the workload’s needs. Full service behaviour is documented by AWS here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumes.html + +**Terrafrom Mappings:** + +- `aws_ebs_volume.id` + +## Supported Methods + +- `GET`: Get a volume by ID +- `LIST`: List all volumes +- `SEARCH`: Search volumes by ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +A volume may be attached to, detached from or created alongside an EC2 instance. Overmind links the two resources so you can trace how storage changes could affect, or be affected by, the compute resource that consumes it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md new file mode 100644 index 00000000..5a2596b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-endpoint.md @@ -0,0 +1,16 @@ +--- +title: VPC Endpoint +sidebar_label: ec2-vpc-endpoint +--- + +A VPC Endpoint is an elastic network interface or gateway that enables private connectivity between resources inside an Amazon Virtual Private Cloud (VPC) and supported AWS or third-party services, without traversing the public internet. By routing traffic through the AWS network, VPC Endpoints improve security, reduce latency and remove the need for NAT devices, VPNs or Direct Connect links. For full details, see the AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html + +**Terrafrom Mappings:** + +- `aws_vpc_endpoint.id` + +## Supported Methods + +- `GET`: Get a VPC Endpoint by ID +- `LIST`: List all VPC Endpoints +- `SEARCH`: Search VPC Endpoints by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md new file mode 100644 index 00000000..a108c04b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc-peering-connection.md @@ -0,0 +1,25 @@ +--- +title: VPC Peering Connection +sidebar_label: ec2-vpc-peering-connection +--- + +A VPC Peering Connection enables you to route traffic privately between two Virtual Private Clouds (VPCs) without traversing the public internet. Peering can be established between VPCs in the same AWS account or across different AWS accounts, and—subject to region support—across regions. It is commonly used for micro-service communication, shared services networks, or multi-account architectures where low-latency, high-bandwidth connectivity with AWS-managed security controls is required. +For full details, refer to the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html + +**Terrafrom Mappings:** + +- `aws_vpc_peering_connection.id` +- `aws_vpc_peering_connection_accepter.id` +- `aws_vpc_peering_connection_options.vpc_peering_connection_id` + +## Supported Methods + +- `GET`: Get a VPC Peering Connection by ID +- `LIST`: List all VPC Peering Connections +- `SEARCH`: Search for VPC Peering Connections by their ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Each VPC Peering Connection has exactly two endpoints—a requester VPC and an accepter VPC. Linking to the `ec2-vpc` resource allows Overmind to show which VPCs are joined by a given peering connection and, conversely, which peering connections a particular VPC participates in. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md new file mode 100644 index 00000000..54132959 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpc.md @@ -0,0 +1,16 @@ +--- +title: VPC +sidebar_label: ec2-vpc +--- + +An Amazon Virtual Private Cloud (VPC) is a logically isolated section of AWS in which you can launch and manage AWS resources within a virtual network that you define. Within a VPC you control IP address ranges, subnets, route tables, network gateways, security groups, and network ACLs, allowing you to shape how traffic flows to and from your workloads while keeping them isolated from, or connected to, the public Internet and other VPCs as required. For a full overview, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html. + +**Terrafrom Mappings:** + +- `aws_vpc.id` + +## Supported Methods + +- `GET`: Get a VPC by ID +- `LIST`: List all VPCs +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md new file mode 100644 index 00000000..be279e71 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-capacity-provider.md @@ -0,0 +1,23 @@ +--- +title: Capacity Provider +sidebar_label: ecs-capacity-provider +--- + +An Amazon ECS capacity provider tells a cluster where its compute capacity comes from and how that capacity should scale. It can point to an Auto Scaling group of EC2 instances or to the serverless Fargate/Fargate Spot capacity pools, and it contains rules that determine when and how instances are launched or terminated to satisfy task demand. Using capacity providers allows platform teams to separate scaling logic from task scheduling and to adopt multiple capacity sources within a single cluster. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html + +**Terrafrom Mappings:** + +- `aws_ecs_capacity_provider.arn` + +## Supported Methods + +- `GET`: Get a capacity provider by its short name or full Amazon Resource Name (ARN). +- `LIST`: List capacity providers. +- `SEARCH`: Search capacity providers by ARN + +## Possible Links + +### [`autoscaling-auto-scaling-group`](/sources/aws/Types/autoscaling-auto-scaling-group) + +A capacity provider that is backed by EC2 instances references exactly one Auto Scaling group. The link lets you trace from the capacity provider to the group that actually supplies instances, making it easy to understand which fleet of instances will scale in response to ECS task demand and to assess risks such as insufficient instance types, mis-configured scaling policies, or conflicting lifecycle hooks. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md new file mode 100644 index 00000000..937d1c8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-cluster.md @@ -0,0 +1,35 @@ +--- +title: ECS Cluster +sidebar_label: ecs-cluster +--- + +An Amazon ECS (Elastic Container Service) cluster is a logical grouping of tasks or services. It acts as the fundamental boundary for scheduling, networking and capacity management in ECS: every task or service is launched into exactly one cluster, and the cluster manages the resources on which containers run. +For full details see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html + +**Terrafrom Mappings:** + +- `aws_ecs_cluster.arn` + +## Supported Methods + +- `GET`: Get a cluster by name +- `LIST`: List all clusters +- `SEARCH`: Search for a cluster by ARN + +## Possible Links + +### [`ecs-container-instance`](/sources/aws/Types/ecs-container-instance) + +An ECS cluster is composed of zero or more container instances (EC2 hosts or AWS Fargate-managed capacity). Each `ecs-container-instance` record represents a specific compute resource that has registered itself to the cluster and is available for running tasks. + +### [`ecs-service`](/sources/aws/Types/ecs-service) + +Services define long-running workloads that are maintained by ECS within the cluster. Every `ecs-service` is created inside a particular cluster and relies on the cluster’s scheduler to place and maintain tasks according to the service definition. + +### [`ecs-task`](/sources/aws/Types/ecs-task) + +Tasks are the running instantiations of container definitions. When a task is started, it is launched into a specific cluster; therefore every `ecs-task` is linked back to the cluster that provided the capacity and networking for it. + +### [`ecs-capacity-provider`](/sources/aws/Types/ecs-capacity-provider) + +Capacity providers control how ECS acquires compute capacity for a cluster (e.g. Fargate, Auto Scaling groups). A cluster may have one or more `ecs-capacity-provider` resources associated with it, and those associations determine how tasks and services within the cluster obtain the underlying compute resources they require. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md new file mode 100644 index 00000000..0d0c65d4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-container-instance.md @@ -0,0 +1,18 @@ +--- +title: Container Instance +sidebar_label: ecs-container-instance +--- + +A container instance represents an Amazon EC2 host that has been registered to an Amazon ECS cluster and is therefore available for running one or more ECS tasks. Each container instance runs the ECS agent and reports its status, resource availability and running tasks back to the cluster’s control plane. For a detailed explanation of container instances, provisioning requirements, and lifecycle behaviour, see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html + +## Supported Methods + +- `GET`: Get a container instance by ID which consists of `{clusterName}/{id}` +- ~~`LIST`~~ +- `SEARCH`: Search for container instances by cluster + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Every container instance is physically an Amazon EC2 instance. Linking to the `ec2-instance` type allows Overmind to surface the underlying compute resource, including its security groups, IAM roles and network configuration, all of which can influence the risk profile of the container instance and any tasks scheduled on it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md new file mode 100644 index 00000000..4b22455b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-service.md @@ -0,0 +1,43 @@ +--- +title: ECS Service +sidebar_label: ecs-service +--- + +An Amazon Elastic Container Service (ECS) **service** is the long-running, scalable unit that maintains a specified number of copies of a task definition running on an ECS cluster. The service schedules tasks either on EC2 instances or on Fargate, monitors their health, replaces unhealthy tasks and, when configured, integrates with Elastic Load Balancing and AWS Service Discovery. +For a full description see the AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html + +**Terrafrom Mappings:** + +- `aws_ecs_service.cluster_name` + +## Supported Methods + +- `GET`: Get an ECS service by full name (`{clusterName}/{id}`) +- ~~`LIST`~~ +- `SEARCH`: Search for ECS services by cluster + +## Possible Links + +### [`ecs-cluster`](/sources/aws/Types/ecs-cluster) + +The service is deployed into exactly one ECS cluster, so each ecs-service will have a **`parent`** relationship to the corresponding `ecs-cluster`. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +If the service is configured with a load balancer, it registers its tasks as targets in one or more ELBv2 target groups; Overmind creates a **`uses`** link from the service to every target group referenced in its loadBalancer or serviceConnect configuration. + +### [`ecs-task-definition`](/sources/aws/Types/ecs-task-definition) + +A service runs a specific revision of a task definition. There is therefore a **`depends_on`** link from the service to the task definition ARN specified in `taskDefinition`. + +### [`ecs-capacity-provider`](/sources/aws/Types/ecs-capacity-provider) + +When a capacity provider strategy is attached, the service relies on one or more capacity providers for scheduling. Overmind shows a **`uses`** link to each referenced `ecs-capacity-provider`. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +For services that use the `awsvpc` network mode (Fargate or ENI-aware EC2 launch type), the service’s tasks are launched inside specific subnets defined in the service’s network configuration; those subnets are exposed via **`uses`** links. + +### [`dns`](/sources/stdlib/Types/dns) + +If AWS Cloud Map service discovery is enabled, the ECS service automatically creates DNS records (A, AAAA, or SRV) for its tasks. Overmind surfaces a **`creates`** link to the resultant DNS names. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md new file mode 100644 index 00000000..3df84160 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-task-definition.md @@ -0,0 +1,27 @@ +--- +title: Task Definition +sidebar_label: ecs-task-definition +--- + +An Amazon ECS task definition is the blueprint that tells AWS ECS how to run one or more containers. It specifies details such as the container images, CPU and memory requirements, networking mode, logging configuration, IAM roles, and secrets that should be injected into the containers. Each time you register a new version, ECS creates a new immutable revision that can be referenced directly or through the family name. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html + +**Terrafrom Mappings:** + +- `aws_ecs_task_definition.family` + +## Supported Methods + +- `GET`: Get a task definition by revision name (`{family}:{revision}`) +- `LIST`: List all task definitions +- `SEARCH`: Search for task definitions by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +A task definition can reference an IAM role through `taskRoleArn` and/or `executionRoleArn`. These roles grant the running containers the permissions they need to interact with other AWS services or to pull private images and write logs. Overmind links the task definition to the IAM role resources so you can see the exact permissions that will be in effect at runtime. + +### [`ssm-parameter`](/sources/aws/Types/ssm-parameter) + +Environment variables or secrets defined in a task definition can be sourced from AWS Systems Manager Parameter Store. Whenever a task definition lists an SSM parameter (e.g., via the `secrets` block), Overmind surfaces a link to the corresponding `ssm-parameter` item, allowing you to trace where sensitive configuration values originate and assess the impact of changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md b/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md new file mode 100644 index 00000000..1d78ca8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ecs-task.md @@ -0,0 +1,35 @@ +--- +title: ECS Task +sidebar_label: ecs-task +--- + +An ECS task is the fundamental unit of work that runs on Amazon Elastic Container Service (ECS). It represents one instantiation of a task definition: a group of one or more Docker containers that are deployed together on the same host. A task lives within an ECS cluster and may run on EC2 instances or on AWS Fargate. The task record captures runtime information such as status, start/stop times, allocated network interfaces and resource utilisation. +For full details, see the AWS documentation: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_tasks.html + +## Supported Methods + +- `GET`: Get an ECS task by ID +- ~~`LIST`~~ +- `SEARCH`: Search for ECS tasks by cluster + +## Possible Links + +### [`ecs-cluster`](/sources/aws/Types/ecs-cluster) + +The task is launched inside exactly one ECS cluster, so Overmind links each task back to the cluster that owns it. + +### [`ecs-container-instance`](/sources/aws/Types/ecs-container-instance) + +For tasks that use the EC2 launch type, the task runs on a specific ECS container instance (an EC2 host registered with the cluster). Overmind links the task to the container instance on which it is currently placed. + +### [`ecs-task-definition`](/sources/aws/Types/ecs-task-definition) + +Every task is an instantiation of a task definition. Overmind records this relationship so you can trace configuration changes in the task definition that may affect a running task. + +### [`ec2-network-interface`](/sources/aws/Types/ec2-network-interface) + +When a task uses the `awsvpc` network mode (or is a Fargate task), AWS allocates one or more elastic network interfaces (ENIs) to the task. These ENIs are linked so you can observe associated security groups, subnets and IP addresses. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each ENI attached to the task is assigned private (and optionally public) IP addresses. Overmind surfaces these IP resources, allowing you to see which IPs are in use by a given task and how they propagate through your network topology. diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md b/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md new file mode 100644 index 00000000..8a0c8cd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-access-point.md @@ -0,0 +1,17 @@ +--- +title: EFS Access Point +sidebar_label: efs-access-point +--- + +Amazon Elastic File System (EFS) Access Points are application-specific entry points into an EFS file system. Each access point can enforce a unique POSIX user, group and root directory, allowing multiple workloads or tenants to share the same file system while maintaining separation and least-privilege access. Access points are commonly used to simplify permissions when deploying containers, serverless functions or batch jobs that need shared storage. +Official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html + +**Terrafrom Mappings:** + +- `aws_efs_access_point.id` + +## Supported Methods + +- `GET`: Get an access point by ID +- `LIST`: List all access points +- `SEARCH`: Search for an access point by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md b/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md new file mode 100644 index 00000000..90c97b0d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-backup-policy.md @@ -0,0 +1,17 @@ +--- +title: EFS Backup Policy +sidebar_label: efs-backup-policy +--- + +An EFS Backup Policy represents the setting on an Amazon Elastic File System (EFS) file system that turns automatic, daily AWS Backup protection on or off. When the policy is enabled, AWS Backup creates incremental backups of the file system and retains them according to the configured backup plan; when it is disabled, the file system is excluded from automated protection. Managing this resource helps ensure that critical data stored in EFS is covered by a consistent backup and retention strategy, reducing the risk of accidental data loss. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/awsbackup.html + +**Terrafrom Mappings:** + +- `aws_efs_backup_policy.id` + +## Supported Methods + +- `GET`: Get an Backup Policy by file system ID +- ~~`LIST`~~ +- `SEARCH`: Search for an Backup Policy by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md b/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md new file mode 100644 index 00000000..5798dd98 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-file-system.md @@ -0,0 +1,16 @@ +--- +title: EFS File System +sidebar_label: efs-file-system +--- + +Amazon Elastic File System (EFS) provides a scalable, elastic and fully-managed Network File System (NFS) that can be mounted concurrently by multiple AWS compute services, including EC2, Lambda and containers. It automatically grows and shrinks as you add or remove data, removing the need to provision storage up front, and offers high availability across multiple Availability Zones. For a full overview, refer to the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html + +**Terrafrom Mappings:** + +- `aws_efs_file_system.id` + +## Supported Methods + +- `GET`: Get a file system by ID +- `LIST`: List file systems +- `SEARCH`: Search file systems by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md b/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md new file mode 100644 index 00000000..14900d71 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-mount-target.md @@ -0,0 +1,17 @@ +--- +title: EFS Mount Target +sidebar_label: efs-mount-target +--- + +An EFS Mount Target is a network endpoint that resides in a specific subnet inside your VPC and exposes an Amazon Elastic File System (EFS) file system to compute resources such as EC2 instances, ECS tasks, Lambda functions and other AWS services. By creating one mount target in each Availability Zone where the file system will be accessed, you ensure low-latency, highly available access to shared file storage. Each mount target can be associated with one or more security groups, allowing fine-grained control over which clients can connect to the file system. +For further details, refer to the official AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-mount-targets.html + +**Terrafrom Mappings:** + +- `aws_efs_mount_target.id` + +## Supported Methods + +- `GET`: Get an mount target by ID +- ~~`LIST`~~ +- `SEARCH`: Search for mount targets by file system ID diff --git a/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md new file mode 100644 index 00000000..3728b8e9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/efs-replication-configuration.md @@ -0,0 +1,17 @@ +--- +title: EFS Replication Configuration +sidebar_label: efs-replication-configuration +--- + +An Amazon Elastic File System (EFS) Replication Configuration defines how an EFS file system is asynchronously replicated to another AWS Region or Availability Zone, providing disaster-recovery protection and enhanced data durability. By creating a replication configuration you specify the source file system, the destination Region, and the encryption and retention settings for the replica. Replication occurs automatically and continuously, with recovery point objectives (RPO) typically within minutes, allowing you to fail over quickly if the primary file system becomes unavailable. +For full details, refer to the AWS documentation: https://docs.aws.amazon.com/efs/latest/ug/efs-replication.html + +**Terrafrom Mappings:** + +- `aws_efs_replication_configuration.source_file_system_id` + +## Supported Methods + +- `GET`: Get a replication configuration by file system ID +- `LIST`: List all replication configurations +- `SEARCH`: Search for a replication configuration by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md b/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md new file mode 100644 index 00000000..9ddd3e89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-addon.md @@ -0,0 +1,17 @@ +--- +title: EKS Addon +sidebar_label: eks-addon +--- + +An Amazon EKS Addon is an AWS-managed installation of common operational software—such as CoreDNS, kube-proxy, the Amazon VPC CNI plugin or the Amazon EBS CSI driver—onto an Amazon Elastic Kubernetes Service (EKS) cluster. Addons let you declare the component, version and configuration you want, while AWS takes care of deployment, upgrades, security patches and ongoing lifecycle management. Using addons keeps the cluster’s critical services consistent and up to date without manual intervention. +For more information, see the official AWS documentation on EKS Add-ons: https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html + +**Terrafrom Mappings:** + +- `aws_eks_addon.id` + +## Supported Methods + +- `GET`: Get an addon by unique name (`{clusterName}:{addonName}`) +- ~~`LIST`~~ +- `SEARCH`: Search addons by cluster name diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md new file mode 100644 index 00000000..a8fc0c25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-cluster.md @@ -0,0 +1,16 @@ +--- +title: EKS Cluster +sidebar_label: eks-cluster +--- + +Amazon Elastic Kubernetes Service (EKS) is a managed Kubernetes control plane that allows you to run Kubernetes workloads on AWS without the operational overhead of managing the underlying master nodes. An EKS cluster handles tasks such as control-plane provisioning, scalability, high availability and automatic patching, while letting you attach one or more node groups (either managed or self-managed) to run your containerised applications. See the official AWS documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html + +**Terraform Mappings:** + +- `aws_eks_cluster.arn` + +## Supported Methods + +- `GET`: Get a cluster by name +- `LIST`: List all clusters +- `SEARCH`: Search for clusters by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md b/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md new file mode 100644 index 00000000..3653feb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-fargate-profile.md @@ -0,0 +1,26 @@ +--- +title: Fargate Profile +sidebar_label: eks-fargate-profile +--- + +An Amazon EKS Fargate profile tells EKS which pods in a cluster should run on AWS Fargate rather than on self-managed or managed EC2 worker nodes. It contains a set of selectors (namespace and optional labels) and the networking configuration (subnets and the pod execution IAM role) that EKS will use when it launches Fargate tasks on your behalf. See the official documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html + +**Terrafrom Mappings:** + +- `aws_eks_fargate_profile.id` + +## Supported Methods + +- `GET`: Get a fargate profile by unique name (`{clusterName}:{FargateProfileName}`) +- ~~`LIST`~~ +- `SEARCH`: Search for fargate profiles by cluster name + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Each Fargate profile references a “pod execution role”, an IAM role that grants EKS permission to pull container images and publish pod logs when it provisions the Fargate tasks. Overmind therefore creates a link from the profile to the IAM role specified in `pod_execution_role_arn`. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The profile’s `subnet_ids` field defines the VPC subnets into which the Fargate pods will be launched. Overmind links the profile to every subnet listed, helping you trace network reachability and security-group inheritance for the pods that will run under this profile. diff --git a/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md b/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md new file mode 100644 index 00000000..85b58201 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/eks-nodegroup.md @@ -0,0 +1,38 @@ +--- +title: EKS Nodegroup +sidebar_label: eks-nodegroup +--- + +Amazon EKS managed node groups are a higher-level abstraction that simplifies the provision and lifecycle management of the worker nodes that run your Kubernetes pods. Instead of creating and operating the underlying Amazon EC2 instances yourself, you declare the desired configuration (instance types, scaling parameters, AMI, etc.) and EKS creates and manages an Auto Scaling group on your behalf. See the official AWS documentation for full details: https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html + +**Terrafrom Mappings:** + +- `aws_eks_node_group.id` + +## Supported Methods + +- `GET`: Get a node group by unique name (`{clusterName}:{NodegroupName}`) +- ~~`LIST`~~ +- `SEARCH`: Search for node groups by cluster name + +## Possible Links + +### [`ec2-key-pair`](/sources/aws/Types/ec2-key-pair) + +If “remote access” is enabled, a node group references an EC2 key pair to allow SSH access to the worker nodes. This creates a dependency on the specified key pair. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Each node group attaches one or more security groups to the network interfaces of its nodes. These security groups control inbound and outbound traffic to the worker nodes. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +When you create a node group you must provide a list of subnets where the nodes will be launched. The node group therefore depends on, and is constrained by, the networking configuration of those subnets. + +### [`autoscaling-auto-scaling-group`](/sources/aws/Types/autoscaling-auto-scaling-group) + +Behind the scenes, a managed node group is realised as an Auto Scaling group. Changes to the node group propagate directly to its underlying Auto Scaling group. + +### [`ec2-launch-template`](/sources/aws/Types/ec2-launch-template) + +You can optionally supply a custom launch template to define advanced EC2 settings (user data, tags, block-device mappings, etc.) for the nodes. When used, the node group links to that launch template. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md b/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md new file mode 100644 index 00000000..713545df --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elb-instance-health.md @@ -0,0 +1,19 @@ +--- +title: ELB Instance Health +sidebar_label: elb-instance-health +--- + +An ELB Instance Health resource represents the current health status of an individual Amazon EC2 instance as reported by an Elastic Load Balancer. The data is returned by the `DescribeInstanceHealth` API call and indicates whether the instance is `InService`, `OutOfService`, or in a transitional state (e.g. `Draining`, `Unknown`). By tracking these objects Overmind can warn you when a deployment will place traffic on unhealthy instances or reduce overall service capacity. +For full details see the AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-healthchecks.html + +## Supported Methods + +- `GET`: Get instance health by ID (`{LoadBalancerName}/{InstanceId}`) +- `LIST`: List all instance healths +- ~~`SEARCH`~~ + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Each ELB Instance Health object is intrinsically linked to the EC2 instance whose state it describes. Following this link allows you to inspect configuration details (such as security groups or attached volumes) that may be contributing to an unhealthy status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md b/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md new file mode 100644 index 00000000..d32502e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elb-load-balancer.md @@ -0,0 +1,47 @@ +--- +title: Classic Load Balancer +sidebar_label: elb-load-balancer +--- + +A Classic Load Balancer (CLB) is the original generation of AWS Elastic Load Balancing. It automatically distributes incoming application or network traffic across multiple Amazon EC2 instances that are located in one or more Availability Zones, improving fault-tolerance and scalability. A CLB provides either HTTP/HTTPS or TCP load balancing and exposes a single DNS end-point that clients connect to. +Official documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/introduction.html + +**Terrafrom Mappings:** + +- `aws_elb.arn` + +## Supported Methods + +- `GET`: Get a classic load balancer by name +- `LIST`: List all classic load balancers +- `SEARCH`: Search for classic load balancers by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +The load balancer’s endpoint is presented as a DNS A/AAAA/CNAME record (e.g. `my-clb-123456.eu-west-2.elb.amazonaws.com`). Overmind links the CLB to this DNS record so that you can see which hostname is exposed publicly. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +AWS hosts the CLB DNS name inside an Amazon-owned Route 53 hosted zone, and you may also create alias or CNAME records in your own hosted zones that point to the CLB. The link shows every hosted zone that contains records referencing the load balancer. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A Classic Load Balancer must be attached to one or more subnets in each Availability Zone where it is enabled. This link reveals the exact subnets the CLB is deployed into. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +Because the selected subnets belong to a specific VPC, the CLB itself resides inside that VPC. The link allows you to trace the load balancer back to its enclosing network boundary. + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +Backend EC2 instances are registered with the CLB as targets. Overmind lists every registered instance so you can assess what workloads will receive traffic from the load balancer. + +### [`elb-instance-health`](/sources/aws/Types/elb-instance-health) + +For each registered EC2 instance AWS maintains per-target health information (healthy, unhealthy, etc.). This link surfaces those health objects, letting you understand why particular instances may not be receiving traffic. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +In a VPC, a Classic Load Balancer is associated with one or more security groups that govern allowed inbound and outbound traffic. Overmind links to these security groups so you can inspect the firewall rules that protect the load balancer. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md new file mode 100644 index 00000000..eaf7991d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-listener.md @@ -0,0 +1,36 @@ +--- +title: ELB Listener +sidebar_label: elbv2-listener +--- + +An Elastic Load Balancing (ELB) v2 Listener is the component of an Application Load Balancer (ALB) or Network Load Balancer (NLB) that checks for connection requests, using a specified protocol and port, and then routes those requests to one or more target groups according to its rules. Each listener belongs to a single load balancer, can have one default action and multiple conditional rules, and is the entry point for traffic into your load-balancing configuration. +Further details can be found in the AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + +**Terrafrom Mappings:** + +- `aws_alb_listener.arn` +- `aws_lb_listener.arn` + +## Supported Methods + +- `GET`: Get an ELB listener by ARN +- ~~`LIST`~~ +- `SEARCH`: Search for ELB listeners by load balancer ARN + +## Possible Links + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +The listener is directly attached to exactly one load balancer. Overmind uses this link to show which ALB or NLB will be affected if the listener configuration is changed or deleted. + +### [`elbv2-rule`](/sources/aws/Types/elbv2-rule) + +A listener owns a set of rules that determine how incoming requests are evaluated and forwarded. This link exposes those rules so you can trace the impact of modifying conditions, priorities, or actions. + +### [`http`](/sources/stdlib/Types/http) + +If the listener uses the HTTP or HTTPS protocol, Overmind represents the public-facing endpoint as an `http` item. This allows cross-checking of listener ports with accessible URLs and aids in identifying unintended exposure. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +Listener actions forward traffic to one or more target groups. Overmind links these dependencies so you can see which instances, containers, or IPs will receive traffic, helping you assess downstream blast radius. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md new file mode 100644 index 00000000..a60dc818 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-load-balancer.md @@ -0,0 +1,55 @@ +--- +title: Elastic Load Balancer +sidebar_label: elbv2-load-balancer +--- + +Elastic Load Balancers distribute incoming traffic across multiple targets, improving the availability and scalability of applications. The “v2” API covers Application, Network and Gateway Load Balancers, each of which can automatically scale to meet demand and provide a single DNS endpoint for users. Full service behaviour and limits are documented in the AWS Elastic Load Balancing User Guide (https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/). + +**Terrafrom Mappings:** + +- `aws_lb.arn` +- `aws_lb.id` + +## Supported Methods + +- `GET`: Get an ELB by name +- `LIST`: List all ELBs +- `SEARCH`: Search for ELBs by ARN + +## Possible Links + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +The load balancer forwards requests to one or more target groups; each listener rule references a target group that contains the actual EC2 instances, IPs or Lambda functions receiving traffic. + +### [`elbv2-listener`](/sources/aws/Types/elbv2-listener) + +Listeners define the port and protocol that the load balancer accepts and contain the rules that map traffic to target groups; every load balancer has at least one listener. + +### [`dns`](/sources/stdlib/Types/dns) + +ELBs are accessed via a DNS name (e.g., `my-alb-123456.eu-west-1.elb.amazonaws.com`). External DNS records resolve to the IPs managed by AWS behind this name. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +Route 53 alias or CNAME records are commonly created in a hosted zone to point a friendly domain name to the load balancer’s DNS name. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +An ELB is deployed inside a specific VPC, inheriting its network boundaries and able to route traffic only within that VPC (except for internet-facing endpoints). + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The load balancer is placed into one or more subnets; for high availability at least two subnets (usually across AZs) are required. + +### [`ec2-address`](/sources/aws/Types/ec2-address) + +Network Load Balancers can be allocated static Elastic IP addresses, one per subnet, providing fixed public IPs for the load balancer. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each Elastic IP (for NLB) or the dynamically allocated addresses (for ALB/Gateway LB) represent the underlying IP resources that the DNS name resolves to. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Application and Gateway Load Balancers are associated with security groups which control the allowed inbound and outbound traffic to the load balancer endpoints. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md new file mode 100644 index 00000000..d4a0f500 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-rule.md @@ -0,0 +1,17 @@ +--- +title: ELB Rule +sidebar_label: elbv2-rule +--- + +An ELBv2 listener rule specifies how an Application Load Balancer (ALB) or Network Load Balancer (NLB) should handle requests that arrive on a particular listener. Each rule has a priority, a set of conditions (for example, host-based or path-based matches) and a set of actions (such as forwarding to a target group, redirecting, or returning a fixed response). When traffic reaches the listener, the load balancer evaluates its rules in priority order and executes the actions associated with the first rule whose conditions are met. Refer to the official AWS documentation for further information: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#listener-rules + +**Terrafrom Mappings:** + +- `aws_alb_listener_rule.arn` +- `aws_lb_listener_rule.arn` + +## Supported Methods + +- `GET`: Get a rule by ARN +- ~~`LIST`~~ +- `SEARCH`: Search for rules by listener ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md new file mode 100644 index 00000000..789a5692 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-group.md @@ -0,0 +1,32 @@ +--- +title: Target Group +sidebar_label: elbv2-target-group +--- + +An Amazon Elastic Load Balancing v2 (ELBv2) target group is a logical grouping of targets—such as EC2 instances, IP addresses, Lambda functions or Application Load Balancers—that a load balancer routes traffic to. It contains configuration such as the protocol and port to use, health-check settings, stickiness, deregistration delay and slow-start settings, all within a single VPC. Listeners on an Application Load Balancer (ALB) or Network Load Balancer (NLB) forward requests to one or more target groups based on listener rules. +For full details see the official AWS documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html + +**Terrafrom Mappings:** + +- `aws_alb_target_group.arn` +- `aws_lb_target_group.arn` + +## Supported Methods + +- `GET`: Get a target group by name +- `LIST`: List all target groups +- `SEARCH`: Search for target groups by load balancer ARN or target group ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +A target group is always created within a specific VPC, and all of its registered IP addresses or instance-based targets must reside in that VPC. Therefore the target group is linked to the VPC where its network resources live. + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +Load balancers reference target groups in their listener rules. This link shows which load balancers are configured to forward traffic to the target group, or conversely, which target groups a given load balancer depends upon. + +### [`elbv2-target-health`](/sources/aws/Types/elbv2-target-health) + +Each target group has a corresponding set of target-health descriptions indicating the current health status of every registered target. This link surfaces those health objects so you can see whether the targets in the group are healthy, unhealthy, initialising or unused. diff --git a/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md new file mode 100644 index 00000000..45e3ae77 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/elbv2-target-health.md @@ -0,0 +1,33 @@ +--- +title: ELB Target Health +sidebar_label: elbv2-target-health +--- + +Elastic Load Balancing (v2) distributes traffic across multiple targets such as EC2 instances, IP addresses, and Lambda functions. +The ELB Target Health resource in Overmind represents the status of a single target as returned by the AWS `DescribeTargetHealth` API. +It shows whether the target is healthy, unhealthy, initialising, or draining, together with any failure reasons, so you can spot issues before a change is deployed. +Official documentation: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeTargetHealth.html + +## Supported Methods + +- `GET`: Get target health by unique ID (`{TargetGroupArn}|{Id}|{AvailabilityZone}|{Port}`) +- ~~`LIST`~~ +- `SEARCH`: Search for target health by target group ARN + +## Possible Links + +### [`ec2-instance`](/sources/aws/Types/ec2-instance) + +When the target group’s type is `instance`, each registered EC2 instance appears as an ELB target. The target-health record shows whether that particular EC2 instance is currently considered healthy by the load balancer. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +For target groups of type `lambda`, the Lambda function itself is the target. The target-health item reports the invocation health of the function as assessed by the load balancer. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +If the target group is of type `ip`, every registered IP address becomes a target. The target-health entry records the health of that IP address, enabling you to see whether traffic will be routed to it. + +### [`elbv2-load-balancer`](/sources/aws/Types/elbv2-load-balancer) + +The load balancer associated with the target group uses these health results to decide where to send traffic. Linking to the load balancer lets you trace how a target’s health status could affect overall load-balancer behaviour. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-group.md b/docs.overmind.tech/docs/sources/aws/Types/iam-group.md new file mode 100644 index 00000000..d3500c2e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-group.md @@ -0,0 +1,16 @@ +--- +title: IAM Group +sidebar_label: iam-group +--- + +An IAM (Identity and Access Management) group is a logical collection of IAM users within an AWS account. Permissions—attached to the group via policies—apply to every user who is a member, making it easier to manage access at scale. Because groups do not have their own security credentials, they cannot be used to log in directly; instead, they serve solely as a mechanism for permission inheritance and simplified administration. For full details, refer to the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_groups.html + +**Terrafrom Mappings:** + +- `aws_iam_group.arn` + +## Supported Methods + +- `GET`: Get a group by name +- `LIST`: List all IAM groups +- `SEARCH`: Search for a group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md b/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md new file mode 100644 index 00000000..f261d8dc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-instance-profile.md @@ -0,0 +1,26 @@ +--- +title: IAM Instance Profile +sidebar_label: iam-instance-profile +--- + +An IAM Instance Profile is a logical container for an IAM role that you can attach to an Amazon EC2 instance when it is launched. The profile passes the role’s credentials to the instance so that applications running on the instance can securely call other AWS services without embedding long-lived access keys in the code or configuration. For full details see the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html + +**Terrafrom Mappings:** + +- `aws_iam_instance_profile.arn` + +## Supported Methods + +- `GET`: Get an IAM instance profile by name +- `LIST`: List all IAM instance profiles +- `SEARCH`: Search IAM instance profiles by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Every instance profile contains exactly one IAM role (though a role can exist without an instance profile). Overmind links the profile to the role it encapsulates so that you can see which permissions will be passed to the EC2 instance. + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +Policies are not attached directly to the instance profile but to the role inside it. Overmind surfaces these indirect relationships so that you can trace what policies – and therefore permissions – will ultimately be available on the instance through the profile. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md b/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md new file mode 100644 index 00000000..9a122311 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-policy.md @@ -0,0 +1,33 @@ +--- +title: IAM Policy +sidebar_label: iam-policy +--- + +An IAM policy is a standalone document that defines a set of permissions which determine whether a principal (user, group, or role) is allowed or denied the ability to call specific AWS APIs. Policies are expressed in JSON, may be created and managed by customers or AWS, and are attached to identities or resources to enforce least-privilege access. See the official AWS documentation for full details: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html + +**Terrafrom Mappings:** + +- `aws_iam_policy.arn` +- `aws_iam_user_policy_attachment.policy_arn` + +## Supported Methods + +## Supported Methods + +- `GET`: Get a policy by ARN or path. `{path}` is extracted from the ARN path component. +- `LIST`: List all policies +- `SEARCH`: Search for IAM policies by ARN + +## Possible Links + +### [`iam-group`](/sources/aws/Types/iam-group) + +An IAM policy can be attached to an IAM group to grant all members of the group the permissions described in the policy. Overmind therefore links a policy to any groups to which it is attached. + +### [`iam-user`](/sources/aws/Types/iam-user) + +An IAM policy may be directly attached to an individual IAM user, granting that user the specified permissions. Overmind surfaces this relationship so you can see every user that inherits rights from the policy. + +### [`iam-role`](/sources/aws/Types/iam-role) + +IAM roles often receive permissions through attached policies. Overmind links a policy to any roles that reference it, allowing you to trace which compute workloads or federated identities can exercise the policy’s privileges. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-role.md b/docs.overmind.tech/docs/sources/aws/Types/iam-role.md new file mode 100644 index 00000000..e4dbad1d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-role.md @@ -0,0 +1,23 @@ +--- +title: IAM Role +sidebar_label: iam-role +--- + +An AWS Identity and Access Management (IAM) role is an identity that you can assume to obtain temporary security credentials so that you can make AWS requests. Unlike users, roles do not have long-term credentials; instead, they rely on trust relationships and attached policies to define who can assume the role and what they can do once they have it. IAM roles are typically used for granting permissions to AWS services, cross-account access, or federated users. +For full details, see the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html + +**Terrafrom Mappings:** + +- `aws_iam_role.arn` + +## Supported Methods + +- `GET`: Get an IAM role by name +- `LIST`: List all IAM roles +- `SEARCH`: Search for IAM roles by ARN + +## Possible Links + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +An IAM role is functionally useless without one or more IAM policies attached to it. Overmind links an `iam-role` to the `iam-policy` resources that 1) are attached as inline or managed policies granting permissions, and 2) define the trust relationship (the role’s assume-role policy). This allows you to trace which permissions the role grants and who or what is allowed to assume it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/iam-user.md b/docs.overmind.tech/docs/sources/aws/Types/iam-user.md new file mode 100644 index 00000000..d34c4c60 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/iam-user.md @@ -0,0 +1,23 @@ +--- +title: IAM User +sidebar_label: iam-user +--- + +An IAM user is a discrete identity within AWS Identity and Access Management that represents a human, service or application which needs to interact with AWS resources. Each user has its own credentials and permissions that determine what actions it can perform in an AWS account. For full details, refer to the AWS documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html + +**Terrafrom Mappings:** + +- `aws_iam_user.arn` +- `aws_iam_user_group_membership.user` + +## Supported Methods + +- `GET`: Get an IAM user by name +- `LIST`: List all IAM users +- `SEARCH`: Search for IAM users by ARN + +## Possible Links + +### [`iam-group`](/sources/aws/Types/iam-group) + +IAM users can be members of one or more IAM groups, inheriting the group’s managed and inline policies. Overmind therefore links an IAM user to the `iam-group` type whenever the user is listed as a member of that group. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md b/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md new file mode 100644 index 00000000..51dd4e6b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-alias.md @@ -0,0 +1,23 @@ +--- +title: KMS Alias +sidebar_label: kms-alias +--- + +An AWS Key Management Service (KMS) alias is a human-readable pointer to a specific KMS key, allowing you to reference that key without exposing its full KeyID or ARN. Aliases make it simpler to rotate keys and update applications, because you can move the alias to a new key rather than changing code or configurations that use the key directly. They are unique within an account and region, and can reference either customer-managed or AWS-managed keys. +For further details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/kms-alias.html + +**Terrafrom Mappings:** + +- `aws_kms_alias.arn` + +## Supported Methods + +- `GET`: Get an alias by keyID/aliasName +- `LIST`: List all aliases +- `SEARCH`: Search aliases by keyID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +Each alias is a shorthand reference that maps to exactly one KMS key; the link shows which underlying `kms-key` the alias currently points to, enabling you to trace risk and usage back to the actual cryptographic key material. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md b/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md new file mode 100644 index 00000000..edb3cb89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-custom-key-store.md @@ -0,0 +1,16 @@ +--- +title: Custom Key Store +sidebar_label: kms-custom-key-store +--- + +A custom key store in AWS Key Management Service (KMS) enables you to back your KMS keys with your own AWS CloudHSM cluster rather than with the default, multi-tenant KMS hardware security modules. This gives you exclusive control over the cryptographic hardware that protects your key material while still allowing you to use KMS APIs and integrations. You can create, connect, disconnect, or delete a custom key store, and any KMS keys that reside in it remain under your sole tenancy. See the official AWS documentation for full details: https://docs.aws.amazon.com/kms/latest/developerguide/custom-key-store-overview.html + +**Terrafrom Mappings:** + +- `aws_kms_custom_key_store.id` + +## Supported Methods + +- `GET`: Get a custom key store by its ID +- `LIST`: List all custom key stores +- `SEARCH`: Search custom key store by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md b/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md new file mode 100644 index 00000000..63c2197a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-grant.md @@ -0,0 +1,30 @@ +--- +title: KMS Grant +sidebar_label: kms-grant +--- + +AWS Key Management Service (KMS) grants are lightweight authorisations that give a specified principal permission to use a particular KMS key for a defined set of operations (such as Encrypt, Decrypt, GenerateDataKey or RetireGrant). Unlike key policies and IAM policies, grants can be created and retired programmatically and have an optional time-to-live, making them ideal for short-lived workloads or delegated access. For a full description see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/grants.html + +**Terrafrom Mappings:** + +- `aws_kms_grant.grant_id` + +## Supported Methods + +- `GET`: Get a grant by keyID/grantId +- ~~`LIST`~~ +- `SEARCH`: Search grants by keyID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +Every grant is created against exactly one KMS key. The grant specifies which operations are allowed on that key, so the relationship is “KMS key ­— has → grant”. + +### [`iam-user`](/sources/aws/Types/iam-user) + +An IAM user can appear as the grantee principal or the retiring principal in a grant. If the user is referenced, the link shows which grants give that user access to which keys. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Similar to IAM users, an IAM role may be listed as the grantee or retiring principal. The link reveals the grants that permit the role to use or retire access to specific KMS keys. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md b/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md new file mode 100644 index 00000000..ff75bb7f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-key-policy.md @@ -0,0 +1,23 @@ +--- +title: KMS Key Policy +sidebar_label: kms-key-policy +--- + +AWS Key Management Service (KMS) key policies are the primary access-control mechanism for customer-managed KMS keys. A key policy is a JSON document attached directly to a KMS key that defines which principals can use the key and what cryptographic operations they may perform. Every customer-managed key must have exactly one key policy, and this policy is evaluated in combination with IAM policies to determine effective permissions. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html + +**Terrafrom Mappings:** + +- `aws_kms_key_policy.key_id` + +## Supported Methods + +- `GET`: Get a KMS key policy by its Key ID +- ~~`LIST`~~ +- `SEARCH`: Search KMS key policies by Key ID + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +A KMS key policy is attached to exactly one KMS key; this link represents that one-to-one relationship. Following the link from a policy to its `kms-key` will show the cryptographic key whose usage and management are governed by the policy. diff --git a/docs.overmind.tech/docs/sources/aws/Types/kms-key.md b/docs.overmind.tech/docs/sources/aws/Types/kms-key.md new file mode 100644 index 00000000..df127d19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/kms-key.md @@ -0,0 +1,31 @@ +--- +title: KMS Key +sidebar_label: kms-key +--- + +An AWS Key Management Service (KMS) Key is a logical representation of a cryptographic key used to encrypt and decrypt data across AWS services and your own applications. Each key is uniquely identifiable by its Key ID and Amazon Resource Name (ARN), can be either customer-managed or AWS-managed, and is stored within an AWS-managed hardware security module (HSM) cluster or, when using a custom key store, in an AWS CloudHSM cluster that you control. KMS Keys are central to implementing envelope encryption, controlling access to encrypted resources, and meeting compliance requirements related to data protection. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html + +**Terrafrom Mappings:** + +- `aws_kms_key.key_id` + +## Supported Methods + +- `GET`: Get a KMS Key by its ID +- `LIST`: List all KMS Keys +- `SEARCH`: Search for KMS Keys by ARN + +## Possible Links + +### [`kms-custom-key-store`](/sources/aws/Types/kms-custom-key-store) + +A KMS Key may reside in a custom key store backed by your own AWS CloudHSM cluster. This link is produced when the key’s `KeyStoreId` attribute is set, allowing Overmind to trace the relationship between the key and the custom key store that physically holds its material. + +### [`kms-key-policy`](/sources/aws/Types/kms-key-policy) + +Every KMS Key has exactly one key policy that defines which principals are authorised to use or administer the key. Overmind links a key to its policy so that you can quickly inspect who can access the key and identify potential misconfigurations or excessive permissions. + +### [`kms-grant`](/sources/aws/Types/kms-grant) + +Grants provide time-bound or scoped permissions for principals to use a KMS Key without modifying its key policy. Overmind records links from a key to all active grants, enabling you to see what temporary or delegated access exists and to assess the risk of unintended key usage. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md new file mode 100644 index 00000000..8696a046 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-event-source-mapping.md @@ -0,0 +1,35 @@ +--- +title: Lambda Event Source Mapping +sidebar_label: lambda-event-source-mapping +--- + +AWS Lambda event source mappings are configuration objects that connect an event-producing resource (for example, an SQS queue, DynamoDB stream, Kinesis data stream or Amazon MQ broker) to a Lambda function. They tell Lambda from which resource to poll, what batch size to use, whether to enable the mapping immediately, and numerous advanced options such as filtering and batching windows. In essence, an event source mapping is the glue that turns an upstream stream or queue into invocations of your function. +Official documentation: https://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#event-source-mapping + +**Terrafrom Mappings:** + +- `aws_lambda_event_source_mapping.arn` + +## Supported Methods + +- `GET`: Get a Lambda event source mapping by UUID +- `LIST`: List all Lambda event source mappings +- `SEARCH`: Search for Lambda event source mappings by Event Source ARN (SQS, DynamoDB, Kinesis, etc.) + +## Possible Links + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +Every event source mapping targets exactly one Lambda function. The mapping’s `FunctionName` points to the ARN of that function, so Overmind will create a link from the mapping to the lambda-function resource it invokes. + +### [`dynamodb-table`](/sources/aws/Types/dynamodb-table) + +When the event source ARN refers to a DynamoDB stream, the underlying DynamoDB table is important context. Overmind links the mapping to the dynamodb-table that owns the stream so that you can trace how table updates lead to Lambda executions. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +For SQS, the mapping’s `EventSourceArn` is the ARN of an SQS queue. Linking to the sqs-queue resource lets you understand queue configuration (visibility timeout, encryption, redrive policy) and how it might influence Lambda processing. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +If the event source is an Amazon RDS for PostgreSQL or MySQL DB cluster emitting events through Amazon RDS for PostgreSQL logical replication slots (via the `RDS Data API` or Aurora’s `MysqlBinlog` integration), the mapping may reference the cluster’s ARN. Overmind links to the rds-db-cluster so you can assess the impact of database changes on the Lambda workflow. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md new file mode 100644 index 00000000..4352b000 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-function.md @@ -0,0 +1,44 @@ +--- +title: Lambda Function +sidebar_label: lambda-function +--- + +AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources for you. A Lambda function is the fundamental execution unit: it contains your application code, runtime settings and configuration such as memory, timeout and environment variables. For a full description see the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html + +**Terrafrom Mappings:** + +- `aws_lambda_function.arn` +- `aws_lambda_function_event_invoke_config.id` +- `aws_lambda_function_url.function_arn` + +## Supported Methods + +- `GET`: Get a lambda function by name +- `LIST`: List all lambda functions +- `SEARCH`: Search for lambda functions by ARN + +## Possible Links + +### [`iam-role`](/sources/aws/Types/iam-role) + +Each Lambda function is executed with an IAM role (its “execution role”). Overmind links the function to that `iam-role` so you can immediately see what permissions the function has and what downstream resources could be affected by its actions. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +A Lambda function can be triggered by S3 events (e.g. object creation) or load its deployment artefact from an S3 bucket. Overmind links the function to any referenced `s3-bucket` so you can assess event-driven couplings and code-package storage risks. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Lambda functions may subscribe to, or publish messages to, Amazon SNS topics. When a function is configured as an SNS subscription target, Overmind links it to the relevant `sns-topic` so that you can trace message flows and understand failure blast-radius. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +Lambda can poll SQS queues as an event source. Overmind establishes a link between the function and the `sqs-queue` it consumes so that queue backlogs, permissions and dead-letter configurations are visible in the dependency graph. + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +A Lambda function can synchronously or asynchronously invoke another Lambda function (for example, in micro-service fan-out patterns). Overmind links calling and called `lambda-function` resources to expose these internal service dependencies. + +### [`elbv2-target-group`](/sources/aws/Types/elbv2-target-group) + +Application Load Balancers (ALB) can forward requests to Lambda targets via an ELBv2 target group. Overmind links the function to any associated `elbv2-target-group`, allowing you to see inbound HTTP pathways and evaluate scaling or security implications. diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md new file mode 100644 index 00000000..fdcf79b1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer-version.md @@ -0,0 +1,17 @@ +--- +title: Lambda Layer Version +sidebar_label: lambda-layer-version +--- + +AWS Lambda Layer Version represents an immutable, version-numbered snapshot of a Lambda layer—an archive of shared code, libraries, custom runtimes or other assets that can be attached to multiple Lambda functions. Each time you publish a layer you create a new layer version, referenced in the form `arn:aws:lambda:::layer::`. Using layers helps decouple shared dependencies from individual function packages, streamline updates and encourage code reuse across your serverless estate. +Further details can be found in the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html + +**Terrafrom Mappings:** + +- `aws_lambda_layer_version.arn` + +## Supported Methods + +- `GET`: Get a layer version by full name (`{layerName}:{versionNumber}`) +- ~~`LIST`~~ +- `SEARCH`: Search for layer versions by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md new file mode 100644 index 00000000..1641736c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/lambda-layer.md @@ -0,0 +1,19 @@ +--- +title: Lambda Layer +sidebar_label: lambda-layer +--- + +AWS Lambda Layers are a packaging construct used to share code, data, and runtimes between multiple Lambda functions. A layer is published once and can then be referenced by any function in the same AWS account (or, if shared, by functions in other accounts), keeping deployment packages small and ensuring that common dependencies are managed in a single place. Overmind surfaces Lambda Layers so that you can see which functions depend on them and understand the blast radius of any proposed change. +For full details, see the official AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html + +## Supported Methods + +- ~~`GET`~~ +- `LIST`: List all lambda layers +- ~~`SEARCH`~~ + +## Possible Links + +### [`lambda-layer-version`](/sources/aws/Types/lambda-layer-version) + +A Lambda Layer can have multiple immutable versions; this link shows the individual versions that belong to the parent layer. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md new file mode 100644 index 00000000..bea09a33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall-policy.md @@ -0,0 +1,31 @@ +--- +title: Network Firewall Policy +sidebar_label: network-firewall-firewall-policy +--- + +An AWS Network Firewall Policy is the central configuration object that tells the AWS Network Firewall service how to inspect, filter, and log traffic that flows through a firewall. The policy groups together references to stateless and stateful rule groups, sets default actions for traffic that does not match a rule, and can optionally attach TLS inspection configurations. Multiple firewalls can share the same policy, making it easy to apply a consistent security posture across different VPCs or accounts. +For full service documentation, see the official AWS docs: https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-policies.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_firewall_policy.name` + +## Supported Methods + +- `GET`: Get a Network Firewall Policy by name +- `LIST`: List Network Firewall Policies +- `SEARCH`: Search for Network Firewall Policies by ARN + +## Possible Links + +### [`network-firewall-rule-group`](/sources/aws/Types/network-firewall-rule-group) + +A firewall policy is essentially a collection of references to stateless and stateful rule groups. Each rule group defined under the policy dictates how specific traffic patterns are handled. Overmind links a policy to its rule groups so that you can quickly understand which inspection rules are being applied. + +### [`network-firewall-tls-inspection-configuration`](/sources/aws/Types/network-firewall-tls-inspection-configuration) + +If the policy includes a TLS inspection configuration, encrypted traffic can be decrypted, inspected, and then re-encrypted. Overmind links the policy to any associated TLS inspection configurations to show whether the firewall is capable of deep packet inspection for TLS flows. + +### [`kms-key`](/sources/aws/Types/kms-key) + +Firewall policies may specify a KMS key for the encryption of log data or stateful rule group data at rest. Overmind surfaces this link so that you can assess the cryptographic controls protecting your firewall’s sensitive data. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md new file mode 100644 index 00000000..f185d8a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-firewall.md @@ -0,0 +1,42 @@ +--- +title: Network Firewall +sidebar_label: network-firewall-firewall +--- + +AWS Network Firewall is a managed, stateful, layer-4 and layer-7 firewall service that you deploy inside your own Amazon Virtual Private Cloud (VPC). It lets you inspect and filter both inbound and outbound traffic by applying rule groups that you author or obtain from third-party providers. Because the service is fully managed, AWS handles availability, scaling and patching, allowing you to focus on writing network-security rules rather than on the underlying infrastructure. For a full overview, see the official documentation: https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_firewall.name` + +## Supported Methods + +- `GET`: Get a Network Firewall by name +- `LIST`: List Network Firewalls +- `SEARCH`: Search for Network Firewalls by ARN + +## Possible Links + +### [`network-firewall-firewall-policy`](/sources/aws/Types/network-firewall-firewall-policy) + +Each Network Firewall is associated with exactly one firewall policy, which defines the stateful and stateless rule groups, default actions and logging configuration that the firewall must enforce. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A firewall is deployed into one or more dedicated subnets—known as firewall subnets—within the VPC. These subnets host the firewall endpoints that inspect traffic traversing the Availability Zones. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The firewall operates inside a specific VPC, inspecting traffic that enters, leaves or moves within that VPC according to the routing configuration you set up. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +You can configure Network Firewall to export alert and flow logs to an Amazon S3 bucket for long-term storage, auditing or further analysis; the bucket therefore becomes a downstream logging destination for the firewall. + +### [`iam-policy`](/sources/aws/Types/iam-policy) + +Creation, modification and deletion of Network Firewall resources are controlled through IAM policies. These policies grant or deny the required `network-firewall:*` permissions to principals such as users, roles and service accounts. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If you choose to encrypt log data that Network Firewall delivers to Amazon S3 or CloudWatch Logs with a customer-managed key, the firewall references an AWS KMS key. The key is used for server-side encryption of the exported log objects. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md new file mode 100644 index 00000000..3de4cd07 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-rule-group.md @@ -0,0 +1,30 @@ +--- +title: Network Firewall Rule Group +sidebar_label: network-firewall-rule-group +--- + +AWS Network Firewall Rule Groups are reusable collections of stateless or stateful inspection rules that you attach to a Network Firewall policy. They let you define, version, and manage traffic-inspection logic independently from the firewalls that enforce it. A rule group may contain Suricata-compatible stateful rules, 5-tuple stateless rules, or a combination of both, and can optionally be encrypted with a customer-managed AWS KMS key. See the official AWS documentation for full details: https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html + +**Terrafrom Mappings:** + +- `aws_networkfirewall_rule_group.name` + +## Supported Methods + +- `GET`: Get a Network Firewall Rule Group by name +- `LIST`: List Network Firewall Rule Groups +- `SEARCH`: Search for Network Firewall Rule Groups by ARN + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +If the rule group was created with an `EncryptionConfiguration`, the ARN of the customer-managed KMS key used for encryption is stored in the resource metadata. Overmind therefore links the rule group to the corresponding `kms-key` item. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Operational teams often configure CloudWatch alarms on Network Firewall metrics that publish to an SNS topic; the alarm definition contains the rule group ARN as a dimension. When such a relationship exists, Overmind links the rule group to the `sns-topic` so that users can trace alerting pathways. + +### [`network-firewall-rule-group`](/sources/aws/Types/network-firewall-rule-group) + +Firewall policies can reference multiple rule groups, and a single rule group can be associated with several policies. Overmind records these associations, allowing one rule group to be linked to other rule groups that are attached to the same policy or that replace it through versioning. diff --git a/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md new file mode 100644 index 00000000..f447983d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/network-firewall-tls-inspection-configuration.md @@ -0,0 +1,13 @@ +--- +title: Network Firewall TLS Inspection Configuration +sidebar_label: network-firewall-tls-inspection-configuration +--- + +An AWS Network Firewall TLS Inspection Configuration represents the collection of certificates and related settings that AWS Network Firewall uses to decrypt, inspect and, when appropriate, re-encrypt TLS-encrypted traffic flowing through a firewall. The configuration is referenced by a firewall policy and allows the firewall to analyse traffic that would otherwise be opaque, enabling the detection of threats hidden inside encrypted sessions. +For full details, see the AWS documentation: https://docs.aws.amazon.com/network-firewall/latest/developerguide/tls-inspection-configuration.html + +## Supported Methods + +- `GET`: Get a Network Firewall TLS Inspection Configuration by name +- `LIST`: List Network Firewall TLS Inspection Configurations +- `SEARCH`: Search for Network Firewall TLS Inspection Configurations by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md new file mode 100644 index 00000000..e2c5a117 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-attachment.md @@ -0,0 +1,21 @@ +--- +title: Networkmanager Connect Attachment +sidebar_label: networkmanager-connect-attachment +--- + +A Network Manager Connect Attachment represents the logical connection used to link a third-party SD-WAN, on-premises router or other non-AWS network appliance to an AWS Cloud WAN core network. It enables you to extend a core network beyond AWS, transporting traffic through GRE tunnels that are established and maintained by a subsequently created Connect Peer. +For full details see the AWS documentation: https://docs.aws.amazon.com/network-manager/latest/cloudwan/cloudwan-network-attachments.html#cloudwan-attachment-connect + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network.id` + +## Supported Methods + +- `GET`: + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Connect Attachment is created inside a specific Cloud WAN core network, referenced by its `CoreNetworkId`. Consequently, Overmind links a connect attachment back to the corresponding `networkmanager-core-network` so that you can trace how external connectivity feeds into, and potentially affects, the wider Cloud WAN topology. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md new file mode 100644 index 00000000..92a76c89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer-association.md @@ -0,0 +1,31 @@ +--- +title: Networkmanager Connect Peer Association +sidebar_label: networkmanager-connect-peer-association +--- + +An AWS Network Manager **Connect Peer Association** records the relationship between a Transit Gateway Connect peer and the on-premises device and link through which that peer reaches the AWS global network. It lets you see which Connect peers are presently attached to which devices and links inside a particular Global Network, and in which state the attachment currently is (for example, _pending_ or _available_). +For full API details, refer to the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_ConnectPeerAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Connect Peer Association +- `LIST`: List all Networkmanager Connect Peer Associations +- `SEARCH`: Search for Networkmanager ConnectPeerAssociations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +The association is scoped to a single Global Network; every Connect Peer Association includes the `GlobalNetworkId` that ties it back to this parent resource. + +### [`networkmanager-connect-peer`](/sources/aws/Types/networkmanager-connect-peer) + +The association identifies the specific Connect Peer (`ConnectPeerId`) whose attachment details are being tracked. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +If the Connect peer terminates on a particular on-premises or edge device, the association includes the `DeviceId`, linking it to this device resource. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Where applicable, the association also records the `LinkId`, showing which physical or logical link is being used by the Connect peer to reach AWS. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md new file mode 100644 index 00000000..8ec2f5b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connect-peer.md @@ -0,0 +1,40 @@ +--- +title: Networkmanager Connect Peer +sidebar_label: networkmanager-connect-peer +--- + +An AWS Network Manager **Connect Peer** represents one end of a GRE tunnel that is established over a Network Manager _Connect attachment_ (for example, between an AWS Transit Gateway/Cloud WAN core network and an external router). +The peer stores the tunnel’s **inside and outside IP addresses**, BGP configuration (peer ASN, BGP addresses and keys), the subnet in which the tunnel terminates, and the current operational state. Creating the peer is the final step that brings a Connect attachment into service, enabling traffic to flow between AWS and on-premises or third-party networks. +For full details see the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_ConnectPeer.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_connect_peer.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Connect Peer by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +A Connect peer ultimately belongs to a core network; through its parent Connect attachment it is associated with a specific core network ID, so the peer can be traced back to the Cloud WAN or Transit Gateway core it serves. + +### [`networkmanager-connect-attachment`](/sources/aws/Types/networkmanager-connect-attachment) + +Each Connect peer is created **within** a single Connect attachment. This link identifies the attachment that houses the peer and through which the GRE tunnel is terminated. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The peer exposes both _inside_ and _outside_ tunnel IP addresses. These addresses are modelled as IP resources and linked so you can see which IPs are consumed by the peer. + +### [`rdap-asn`](/sources/stdlib/Types/rdap-asn) + +When BGP is enabled the peer records the remote BGP ASN. Overmind links that ASN so you can quickly inspect public registration information for the autonomous system you are peering with. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +The peer must be associated with a specific subnet that contains the tunnel’s AWS endpoint. Linking to the EC2 subnet shows the precise network segment in which the peer resides, helping to check routing and security settings. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md new file mode 100644 index 00000000..f24ff668 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-connection.md @@ -0,0 +1,30 @@ +--- +title: Networkmanager Connection +sidebar_label: networkmanager-connection +--- + +An AWS Network Manager Connection represents the logical relationship between two network devices (for example, a branch router and a transit gateway) inside an AWS Global Network. It stores metadata about how the two endpoints are linked, enabling Network Manager to map, monitor and troubleshoot your private WAN from a single view. See the official AWS documentation for full details: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Connection.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_connection.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Connection +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Connections by GlobalNetworkId, Device ARN, or Connection ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every connection is created within exactly one Global Network. Overmind follows this link to understand which overarching corporate network the connection belongs to and to enumerate all other resources that share the same scope. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +A connection is realised by one or two underlying Links, representing the actual circuits or VPN tunnels that carry traffic. Linking to these allows Overmind to surface characteristics such as bandwidth, provider and health for each side of the connection. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Each connection terminates on two Devices (the `SourceDeviceId` and `DestinationDeviceId`). From a connection, Overmind can pivot to the involved devices to reveal their configurations, attached links and any downstream dependencies that could be affected by changes to the connection. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md new file mode 100644 index 00000000..2dfff562 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network-policy.md @@ -0,0 +1,22 @@ +--- +title: Networkmanager Core Network Policy +sidebar_label: networkmanager-core-network-policy +--- + +An AWS Network Manager Core Network Policy represents the set of declarative rules that describe how traffic may flow within and between the segments of an AWS Cloud WAN core network (for example, how on-premises VPNs, VPCs and Transit Gateways are connected, and which segments are allowed to communicate). Each policy is versioned and attached to a single core network, allowing you to stage, validate and apply changes safely. For further details see the AWS documentation: https://docs.aws.amazon.com/network-manager/latest/cloudwan/cloudwan-policy-operations.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network_policy.core_network_id` + +## Supported Methods + +- `GET`: Get a Networkmanager Core Network Policy by Core Network id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every core network policy is bound to exactly one core network; therefore, Overmind links a `networkmanager-core-network-policy` item back to the corresponding `networkmanager-core-network` to show which core network the policy governs and to make it easier to assess the blast-radius of changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md new file mode 100644 index 00000000..bb9905ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-core-network.md @@ -0,0 +1,27 @@ +--- +title: Networkmanager Core Network +sidebar_label: networkmanager-core-network +--- + +An AWS Network Manager **core network** represents the logical, centrally-managed backbone created by AWS Cloud WAN. It defines the global routing fabric, network segments, and edge locations that connect your AWS Regions and on-premises sites. Once a core network is in place you can attach VPCs, VPNs, Direct Connects and third-party SD-WAN devices, and let Cloud WAN automatically propagate routes between them according to the policy you supply. +For further details see the [official documentation](https://docs.aws.amazon.com/vpc/latest/cloudwan/what-is-cloudwan.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_core_network.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Core Network by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network-policy`](/sources/aws/Types/networkmanager-core-network-policy) + +Every core network is governed by a **core network policy** that declares its segments, attachment permissions, and routing intent. Overmind links a `networkmanager-core-network` to its current `networkmanager-core-network-policy` so that you can inspect or diff the policy that is actively controlling the network. + +### [`networkmanager-connect-peer`](/sources/aws/Types/networkmanager-connect-peer) + +A **Connect peer** represents a GRE/BGP session that terminates on a Connect attachment belonging to a core network. Overmind exposes this link to show which Connect peers (and therefore which on-premises routers) are logically attached to the given core network. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md new file mode 100644 index 00000000..b96cef18 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-device.md @@ -0,0 +1,39 @@ +--- +title: Networkmanager Device +sidebar_label: networkmanager-device +--- + +An AWS Network Manager Device represents a physical or virtual network appliance (e.g. router, firewall, SD-WAN box, software VPN endpoint) that you register with a Global Network in AWS Network Manager. Once registered, the device becomes a first-class object that can be linked to Sites, Links and Connections, allowing you to model and monitor your entire hybrid network topology in AWS. +For full details see the AWS API reference: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Device.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_device.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Device +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Devices by GlobalNetworkId, `{GlobalNetworkId|SiteId}` or ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A device is always created inside a single Global Network. This link shows which Global Network the device belongs to so you can understand its administrative domain. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Each device is associated with one Site (for example, a particular data centre or branch office). The link reveals the physical location context of the device. + +### [`networkmanager-link-association`](/sources/aws/Types/networkmanager-link-association) + +A device can have one or more Link Associations that describe the physical or logical circuits (Links) terminating on that device. Following this link surfaces the underlying connectivity for the device. + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +Connections model the logical relationship between two devices. This link lists all point-to-point or multi-point Connections in which the device participates. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +This link captures any additional resource relationships (for example, Transit Gateway attachments or VPNs) that reference the device, providing a holistic view of dependencies and potential blast-radius. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md new file mode 100644 index 00000000..af31ebf5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-global-network.md @@ -0,0 +1,51 @@ +--- +title: Network Manager Global Network +sidebar_label: networkmanager-global-network +--- + +An AWS Network Manager Global Network is the top-level container that represents your organisation’s private global network within AWS. It groups together sites, on-premises devices, AWS Transit Gateways, and the connections between them, allowing you to view and manage the entire topology from a single place. You must create a global network before you can register any resources with Network Manager. +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_GlobalNetwork.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_global_network.arn` + +## Supported Methods + +- `GET`: Get a global network by id +- `LIST`: List all global networks +- `SEARCH`: Search for a global network by ARN + +## Possible Links + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +A Site is created inside a Global Network. Each `networkmanager-site` record therefore links back to the Global Network that owns it. + +### [`networkmanager-transit-gateway-registration`](/sources/aws/Types/networkmanager-transit-gateway-registration) + +Transit Gateways must be registered with a specific Global Network before they can be visualised or managed by Network Manager. These registration objects reference the parent Global Network. + +### [`networkmanager-connect-peer-association`](/sources/aws/Types/networkmanager-connect-peer-association) + +A Connect Peer Association represents the attachment of a Connect peer to a Global Network. The association record points to the Global Network in which the peer is enrolled. + +### [`networkmanager-transit-gateway-connect-peer-association`](/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association) + +Similar to the above, but for Transit Gateway Connect peers. The association is made within the scope of a single Global Network. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +This type models relationships between any two resources (devices, links, TGWs, etc.) that are part of the same Global Network. Each relationship object is tied to the Global Network it belongs to. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Links represent the physical or logical connections at a Site and, by extension, sit within the Global Network that the Site is part of. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices (routers, switches, firewalls, etc.) are registered to Sites, and consequently to the parent Global Network. Each device record references its Global Network identifier. + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +Connections join two Devices over one or more Links inside a Global Network. The connection object therefore includes the Global Network ID to denote its scope. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md new file mode 100644 index 00000000..14b54762 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link-association.md @@ -0,0 +1,29 @@ +--- +title: Networkmanager LinkAssociation +sidebar_label: networkmanager-link-association +--- + +A Network Manager **Link Association** represents the attachment of a specific physical or logical network **link** (for example, a DIA, MPLS or broadband circuit) to a **device** (such as a router, firewall, SD-WAN appliance) that resides at a site in an AWS Cloud WAN / Network Manager **global network**. +Each association records which device terminates the link, the site it belongs to, bandwidth details and the operational state of that attachment. +Official AWS documentation: +https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_LinkAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Link Association +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Link Associations by GlobalNetworkId and DeviceId or LinkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every Link Association is scoped to exactly one Global Network; the GlobalNetworkId is part of the composite key for the association. Following this link lets you see all other resources (sites, devices, links, transit gateways, etc.) that belong to the same overarching global network. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +The association couples a device to a particular LinkId. Traversing this link shows the underlying circuit or connectivity object that is being attached, along with its provider, bandwidth and cost details. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +The DeviceId in the association identifies the hardware or virtual appliance that terminates the link. Navigating this link reveals the device’s interfaces, status and any other links or connections it participates in. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md new file mode 100644 index 00000000..2c44c83a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-link.md @@ -0,0 +1,35 @@ +--- +title: Networkmanager Link +sidebar_label: networkmanager-link +--- + +An AWS Network Manager **Link** represents a physical or logical connection (for example, an MPLS circuit, Direct Connect connection, broadband, or internet link) that provides connectivity at a specific site within a global network. Links are used by Network Manager to calculate network health, aggregate telemetry and visualise topology. Each link is created inside a Site, and therefore inside a Global Network, and can later be associated with one or more network devices. +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Link.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_link.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Link +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Links by GlobalNetworkId, GlobalNetworkId with SiteId, or ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Link is a component of a single Global Network; this edge points from the Link to the Global Network that owns it. + +### [`networkmanager-link-association`](/sources/aws/Types/networkmanager-link-association) + +A Link can be associated with one or more devices. These associations are represented by Network Manager Link Association resources, which reference the Link as their parent. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Every Link resides in exactly one Site; this relationship shows which Site the Link belongs to. + +### [`networkmanager-network-resource-relationship`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Network Manager records discovered relationships between Links and other network resources (for example, AWS Transit Gateway attachments). This edge captures those discovered `network-resource-relationship` objects that involve the Link. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md new file mode 100644 index 00000000..3c38c328 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-network-resource-relationship.md @@ -0,0 +1,43 @@ +--- +title: Networkmanager Network Resource Relationships +sidebar_label: networkmanager-network-resource-relationship +--- + +Represents an association between two AWS Network Manager resources within a single Global Network. A Network Resource Relationship records how different components—such as devices, links, connections and Direct Connect objects—are connected, enabling topology visualisation and impact analysis. Each relationship object identifies a **source resource**, a **destination resource**, and the **type of relationship** (for example `CONNECTED_TO` or `CHILD_OF`). +For full field-level details see the AWS API reference: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_NetworkResourceRelationship.html + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager NetworkResourceRelationships by GlobalNetworkId + +## Possible Links + +### [`networkmanager-connection`](/sources/aws/Types/networkmanager-connection) + +A Network Manager connection (for example a VPN or Transit Gateway attachment) can appear as either the **source** or **destination** in a relationship, indicating that it is logically connected to another resource—most commonly a site, device or Direct Connect virtual interface. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices (routers, firewalls or SD-WAN appliances) are frequently linked to links and connections. When a device participates in a relationship, the record shows which link it uses or which connection terminates on the device. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +A link represents physical or logical connectivity (for example an MPLS circuit). Relationships illustrate which device, site or Direct Connect virtual interface is using, or is reached through, a given link. + +### [`networkmanager-site`](/sources/aws/Types/networkmanager-site) + +Site resources group devices and links. Relationships referencing a site capture a **CHILD_OF** type association, showing that a particular device or link belongs to, or is located within, the site. + +### [`directconnect-connection`](/sources/aws/Types/directconnect-connection) + +Direct Connect connections are mapped into the global network; relationships show how a Direct Connect line is attached to a Network Manager link or gateway, providing visibility of dedicated connectivity paths. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When a Direct Connect gateway is part of a global network, relationships identify which connections or virtual interfaces are routed through the gateway, enabling you to trace traffic flows. + +### [`directconnect-virtual-interface`](/sources/aws/Types/directconnect-virtual-interface) + +Virtual interfaces (private, public or transit) may be related to Direct Connect connections, gateways or Network Manager links. The relationship clarifies which physical connection a VIF is presented on and how it integrates with the wider network. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md new file mode 100644 index 00000000..5a3f8b7c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site-to-site-vpn-attachment.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager Site To Site Vpn Attachment +sidebar_label: networkmanager-site-to-site-vpn-attachment +--- + +A Network Manager Site-to-Site VPN attachment represents the connection of an AWS Site-to-Site VPN to an AWS Cloud WAN / Network Manager core network. By creating this attachment you allow traffic from a remote on-premises site, carried over an IPsec VPN tunnel, to be routed through the core network alongside other AWS and on-premises connections. +Further information can be found in the [official AWS documentation](https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_SiteToSiteVpnAttachment.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_site_to_site_vpn_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Site To Site Vpn Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Each Site-to-Site VPN attachment is created inside a single core network, so the attachment item is linked to the `networkmanager-core-network` that owns it. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md new file mode 100644 index 00000000..b905c942 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-site.md @@ -0,0 +1,30 @@ +--- +title: Networkmanager Site +sidebar_label: networkmanager-site +--- + +An AWS Network Manager **Site** represents a real-world location—such as a corporate office, data centre or colocation facility—that forms part of an organisation’s Global Network. It provides the context in which devices and network links are deployed, enabling AWS Network Manager to map physical geography to logical connectivity. For a full description of the resource and its attributes, see the official AWS documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_Site.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_site.arn` + +## Supported Methods + +- `GET`: Get a Networkmanager Site +- ~~`LIST`~~ +- `SEARCH`: Search for Networkmanager Sites by GlobalNetworkId or Site ARN + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Site is always created within a single Global Network. The `GlobalNetworkId` on the Site identifies its parent `networkmanager-global-network`, forming a one-to-many relationship (one Global Network, many Sites). + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +Links represent individual network connections (e.g., MPLS, broadband) that terminate at a Site. Each `networkmanager-link` includes the `SiteId` of the Site where the connection is installed, so multiple Links can be related to one Site. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +Devices such as routers, firewalls or SD-WAN appliances are housed at a Site. Every `networkmanager-device` records the `SiteId` where it resides, creating a one-to-many relationship between a Site and its Devices. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md new file mode 100644 index 00000000..f74b6ec8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-connect-peer-association.md @@ -0,0 +1,29 @@ +--- +title: Networkmanager Transit Gateway Connect Peer Association +sidebar_label: networkmanager-transit-gateway-connect-peer-association +--- + +A Network Manager Transit Gateway Connect Peer Association represents the connection between an AWS Transit Gateway Connect peer (a GRE tunnel endpoint created as part of a Transit Gateway Connect attachment) and a site that you have modelled inside AWS Network Manager. +The object records which Global Network the peer belongs to and, optionally, which on-premises device and physical/virtual link it should be mapped to. Maintaining this mapping allows Network Manager to draw accurate topology diagrams and to include the GRE tunnel in route analytics, performance monitoring, and policy assessments. + +Official documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_TransitGatewayConnectPeerAssociation.html + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Connect Peer Association by id +- `LIST`: List all Networkmanager Transit Gateway Connect Peer Associations +- `SEARCH`: Search for Networkmanager Transit Gateway Connect Peer Associations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +Every Transit Gateway Connect Peer Association is scoped to a single Global Network. The `GlobalNetworkId` on the association points to the corresponding `networkmanager-global-network` item, indicating which overall corporate network the peer is part of. + +### [`networkmanager-device`](/sources/aws/Types/networkmanager-device) + +The association can specify a `DeviceId` to indicate the on-premises or edge device (for example, a customer router or firewall) that terminates the GRE tunnel. Linking to the `networkmanager-device` item shows where the peer logically lands in your topology. + +### [`networkmanager-link`](/sources/aws/Types/networkmanager-link) + +If the Connect peer is tied to a particular circuit, VLAN, or VPN link at the site, the association includes a `LinkId`. This links the peer to a `networkmanager-link` item, allowing you to trace the physical or logical connectivity that underpins the GRE tunnel. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md new file mode 100644 index 00000000..3ef718ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-peering.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager Transit Gateway Peering +sidebar_label: networkmanager-transit-gateway-peering +--- + +An AWS Network Manager Transit Gateway Peering represents a peering attachment between an AWS Cloud WAN _core network_ and an existing AWS Transit Gateway (TGW). Creating this peering allows traffic to flow transparently between VPCs or on-premises networks connected to the Transit Gateway and the segments that make up the Cloud WAN core network, extending the reach of both fabrics without the need for additional VPNs or direct-connect links. +For more information, see the [AWS documentation](https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_TransitGatewayPeering.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_transit_gateway_peering.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Peering by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Transit Gateway Peering is created **within** a specific Cloud WAN core network; the core network is the logical container that owns the peering attachment. Consequently, querying a `networkmanager-core-network` item allows you to enumerate or drill down to its associated Transit Gateway Peerings, and conversely, each Transit Gateway Peering stores the identifier of the core network it belongs to. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md new file mode 100644 index 00000000..d1f19264 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-registration.md @@ -0,0 +1,18 @@ +--- +title: Networkmanager Transit Gateway Registrations +sidebar_label: networkmanager-transit-gateway-registration +--- + +A Network Manager Transit Gateway Registration represents the association of an AWS Transit Gateway with an AWS Network Manager Global Network. By registering a Transit Gateway, you enable Network Manager to map its attachments, monitor routing changes and performance, and include the gateway in your overall network topology visualisation. For more information, see the official AWS documentation: https://docs.aws.amazon.com/vpc/latest/tgw/register-transit-gateway.html + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Registrations +- `LIST`: List all Networkmanager Transit Gateway Registrations +- `SEARCH`: Search for Networkmanager Transit Gateway Registrations by GlobalNetworkId + +## Possible Links + +### [`networkmanager-global-network`](/sources/aws/Types/networkmanager-global-network) + +A Transit Gateway registration is always scoped to, and therefore linked with, a single Network Manager Global Network. This link indicates the parent Global Network that owns the registration, allowing Overmind to traverse from the high-level network to the individual Transit Gateway associations. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md new file mode 100644 index 00000000..779efbdd --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-transit-gateway-route-table-attachment.md @@ -0,0 +1,27 @@ +--- +title: Networkmanager Transit Gateway Route Table Attachment +sidebar_label: networkmanager-transit-gateway-route-table-attachment +--- + +The Network Manager Transit Gateway Route Table Attachment represents the binding between an AWS Transit Gateway (TGW) route table and an AWS Cloud WAN (Network Manager Core Network) segment. Creating this attachment allows routes that exist in the TGW route table to be advertised into the Cloud WAN segment and, conversely, permits segment routes to be propagated to the TGW. In effect, it provides a controlled integration point between an existing TGW-based topology and a Cloud WAN fabric. +Official API documentation: https://docs.aws.amazon.com/networkmanager/latest/APIReference/API_CreateTransitGatewayRouteTableAttachment.html + +**Terrafrom Mappings:** + +- `aws_networkmanager_transit_gateway_route_table_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager Transit Gateway Route Table Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every Transit Gateway Route Table Attachment is created inside a specific Core Network and targets one of its segments. Therefore, the attachment is a child resource of the Core Network and inherits its administrative domain and policy constraints. + +### [`networkmanager-transit-gateway-peering`](/sources/aws/Types/networkmanager-transit-gateway-peering) + +Before a TGW route table can be attached, a Transit Gateway Peering must exist between the TGW and the Core Network. The attachment references that peering to determine the underlay connection over which route exchange will occur. diff --git a/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md new file mode 100644 index 00000000..cf4ac041 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/networkmanager-vpc-attachment.md @@ -0,0 +1,23 @@ +--- +title: Networkmanager VPC Attachment +sidebar_label: networkmanager-vpc-attachment +--- + +A Network Manager VPC attachment represents the logical link between an Amazon Virtual Private Cloud (VPC) and an AWS Cloud WAN / Network Manager **core network**. By creating an attachment you allow the sub-nets inside the VPC to participate in the global routing domain managed by Network Manager, making it possible for traffic to reach other VPCs, on-premises networks, or SD-WAN devices that are also attached to the same core network. +For a detailed explanation of the resource and its properties, see the [official AWS documentation](https://docs.aws.amazon.com/vpc/latest/cloudwan/what-is-cloudwan.html). + +**Terrafrom Mappings:** + +- `aws_networkmanager_vpc_attachment.id` + +## Supported Methods + +- `GET`: Get a Networkmanager VPC Attachment by id +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`networkmanager-core-network`](/sources/aws/Types/networkmanager-core-network) + +Every VPC attachment is created inside a specific core network and inherits its routing policies. The `core_network_id` field on the attachment identifies that parent, so Overmind can follow this link to reveal the wider network fabric that the VPC will join. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md new file mode 100644 index 00000000..643876bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster-parameter-group.md @@ -0,0 +1,16 @@ +--- +title: RDS Cluster Parameter Group +sidebar_label: rds-db-cluster-parameter-group +--- + +An RDS Cluster Parameter Group is a named collection of engine configuration values that are applied to every instance within an Amazon RDS or Aurora DB cluster. By adjusting the parameters in the group you can fine-tune settings such as memory management, logging, and query optimisation, and have those settings propagated consistently across the cluster. If you do not specify a custom group when you create a cluster, AWS assigns the default engine-specific parameter group. For details, see the AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithParamGroups.html. + +**Terrafrom Mappings:** + +- `aws_rds_cluster_parameter_group.arn` + +## Supported Methods + +- `GET`: Get a parameter group by name +- `LIST`: List all RDS parameter groups +- `SEARCH`: Search for a parameter group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md new file mode 100644 index 00000000..d9e128c8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-cluster.md @@ -0,0 +1,51 @@ +--- +title: RDS Cluster +sidebar_label: rds-db-cluster +--- + +Amazon Relational Database Service (RDS) Clusters provide a managed, highly-available relational database running on multiple Availability Zones. An RDS Cluster contains one or more database instances that share storage, backups, and endpoints, and can be configured for automatic fail-over and read-scaling. Aurora MySQL and Aurora PostgreSQL engines run exclusively within clusters, while other engines (e.g. MySQL, PostgreSQL) can participate in global database topologies through cluster links. +Official documentation: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html + +**Terrafrom Mappings:** + +- `aws_rds_cluster.cluster_identifier` + +## Supported Methods + +- `GET`: Get a cluster by identifier +- `LIST`: List all RDS clusters +- `SEARCH`: Search for a cluster by ARN + +## Possible Links + +### [`rds-db-subnet-group`](/sources/aws/Types/rds-db-subnet-group) + +Each RDS Cluster is associated with a DB subnet group that defines the set of subnets (and therefore Availability Zones) in which its instances can run. + +### [`dns`](/sources/stdlib/Types/dns) + +The cluster exposes an endpoint such as `mycluster.cluster-123456789012.eu-west-2.rds.amazonaws.com`; this hostname is represented as a DNS record linked to the cluster. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +Clusters can reference other clusters as replication sources or targets (e.g. in an Aurora global database), creating a dependency link between the participating RDS clusters. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +Traffic to and from the cluster’s instances is controlled by one or more EC2 security groups attached to the cluster. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +Organisations often create Route 53 records (A/AAAA or CNAME) in their hosted zones to provide friendly names for the cluster endpoint, linking the hosted zone to the RDS Cluster. + +### [`kms-key`](/sources/aws/Types/kms-key) + +If storage encryption is enabled, the cluster uses a customer-managed or AWS-managed KMS key; compromising or deleting the key will render the data inaccessible. + +### [`rds-option-group`](/sources/aws/Types/rds-option-group) + +Certain engines allow additional features to be enabled via option groups (e.g. Oracle options); a cluster may reference an option group to configure those extensions. + +### [`iam-role`](/sources/aws/Types/iam-role) + +An RDS Cluster can assume IAM roles for tasks such as exporting snapshots to S3, publishing logs to CloudWatch, or accessing AWS services like Kinesis; these roles are linked resources. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md new file mode 100644 index 00000000..7f02149b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-instance.md @@ -0,0 +1,55 @@ +--- +title: RDS Instance +sidebar_label: rds-db-instance +--- + +Amazon Relational Database Service (RDS) DB instances are the managed compute and storage resources that run your relational database engines in AWS. An instance encapsulates the underlying virtual hardware, disk, network interfaces, and database server software that form a single, addressable database node. Full service description and behaviour are documented in the AWS RDS User Guide: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Overview.DBInstance.html + +**Terrafrom Mappings:** + +- `aws_db_instance.identifier` +- `aws_db_instance_role_association.db_instance_identifier` + +## Supported Methods + +- `GET`: Get an instance by ID +- `LIST`: List all instances +- `SEARCH`: Search for instances by ARN + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Every RDS instance exposes an endpoint such as `mydb.abc123.eu-west-2.rds.amazonaws.com`. Overmind links the instance to the corresponding DNS record so you can trace how applications resolve and reach the database. + +### [`route53-hosted-zone`](/sources/aws/Types/route53-hosted-zone) + +The automatically-created DNS record for an RDS endpoint lives inside an AWS-managed Route 53 hosted zone, and private zones in your account may contain CNAMEs pointing to it. Overmind surfaces these zones to show where the endpoint is published and overridden. + +### [`ec2-security-group`](/sources/aws/Types/ec2-security-group) + +In a VPC, an RDS instance is attached to one or more security groups that define its inbound and outbound traffic rules. These links let you audit which networks and EC2 instances are permitted to reach the database. + +### [`rds-db-parameter-group`](/sources/aws/Types/rds-db-parameter-group) + +A DB parameter group controls engine-level configuration such as `max_connections` or `log_min_duration_statement`. Each instance references exactly one parameter group (or the default), so Overmind links them for configuration drift and compliance checks. + +### [`rds-db-subnet-group`](/sources/aws/Types/rds-db-subnet-group) + +The subnet group lists the subnets (and therefore the AZs) where the instance may be placed. Linking highlights the network reachability and resiliency zone choices for the database. + +### [`rds-db-cluster`](/sources/aws/Types/rds-db-cluster) + +For Aurora and other clustered engines, individual DB instances are members of an RDS DB cluster. Overmind links them so you can see the relationship between writer/reader nodes and the cluster-level endpoints. + +### [`kms-key`](/sources/aws/Types/kms-key) + +When storage encryption is enabled, an RDS instance uses an AWS KMS key to encrypt its underlying EBS volumes and snapshots. The link shows which key protects the data and who can decrypt it. + +### [`iam-role`](/sources/aws/Types/iam-role) + +Features such as S3 import/export, AWS Lambda integration, and CloudWatch Logs require the database service to assume an IAM service role. Overmind lists these roles so you can review permissions the database can exercise in your account. + +### [`iam-instance-profile`](/sources/aws/Types/iam-instance-profile) + +RDS Custom instances (and certain on-host integrations) run on dedicated EC2 instances within your account and therefore use an IAM instance profile. If present, Overmind links the profile to reveal any additional permissions granted to the underlying host. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md new file mode 100644 index 00000000..ad921286 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-parameter-group.md @@ -0,0 +1,17 @@ +--- +title: RDS Parameter Group +sidebar_label: rds-db-parameter-group +--- + +An Amazon RDS DB parameter group is a container for engine configuration values that determine how a database instance or cluster behaves. By attaching a parameter group to one or more RDS resources you override the engine’s built-in defaults with your own settings, allowing you to tune performance, security and operational behaviour. Changes made to the group are propagated to every associated instance; static parameters take effect after the next reboot, while dynamic parameters may apply immediately. +For a full explanation see the official AWS documentation: [Working with DB parameter groups](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html). + +**Terrafrom Mappings:** + +- `aws_db_parameter_group.arn` + +## Supported Methods + +- `GET`: Get a parameter group by name +- `LIST`: List all parameter groups +- `SEARCH`: Search for a parameter group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md new file mode 100644 index 00000000..011d6ec6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-db-subnet-group.md @@ -0,0 +1,27 @@ +--- +title: RDS Subnet Group +sidebar_label: rds-db-subnet-group +--- + +An RDS DB subnet group is a named collection of one or more subnets that belong to a single Amazon VPC. When you create an Amazon RDS DB instance in a VPC, the subnet group tells RDS which subnets, and therefore which Availability Zones, it may use to provision and maintain the instance. Subnet groups are essential for ensuring high availability and proper network isolation of database workloads. +For full details, see the AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.Subnets.html + +**Terrafrom Mappings:** + +- `aws_db_subnet_group.arn` + +## Supported Methods + +- `GET`: Get a subnet group by name +- `LIST`: List all subnet groups +- `SEARCH`: Search for subnet groups by ARN + +## Possible Links + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +The DB subnet group is created within exactly one VPC; its subnets must all belong to this VPC, so the group inherits the VPC’s routing and network-security boundaries. + +### [`ec2-subnet`](/sources/aws/Types/ec2-subnet) + +A DB subnet group is a container for multiple EC2 subnets, typically spanning at least two Availability Zones. Each listed subnet in the group contributes one possible placement zone for RDS DB instances. diff --git a/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md b/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md new file mode 100644 index 00000000..57217f31 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/rds-option-group.md @@ -0,0 +1,17 @@ +--- +title: RDS Option Group +sidebar_label: rds-option-group +--- + +An Amazon Relational Database Service (RDS) Option Group is a logical container that lets you enable and configure additional features—known as “options”—for an RDS DB instance or cluster. Typical options include Oracle Transparent Data Encryption, SQL Server Audit, MariaDB Audit Plugin and many others that are not activated by default with the engine. By assigning an option group to one or more databases you ensure that each instance inherits the same, centrally-managed configuration, simplifying governance and compliance. +For complete details see the official AWS documentation: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithOptionGroups.html + +**Terrafrom Mappings:** + +- `aws_db_option_group.arn` + +## Supported Methods + +- `GET`: Get an option group by name +- `LIST`: List all RDS option groups +- `SEARCH`: Search for an option group by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md b/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md new file mode 100644 index 00000000..a73f438b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-health-check.md @@ -0,0 +1,23 @@ +--- +title: Route53 Health Check +sidebar_label: route53-health-check +--- + +Amazon Route 53 health checks continuously monitor the availability and latency of your application endpoints (such as web servers, API gateways or other resources) and can automatically trigger DNS fail-over when an endpoint becomes unhealthy. Each health check can also be configured to integrate with Amazon CloudWatch, enabling alerting and automation based on the current health state. +For full details, refer to the official AWS documentation: [Amazon Route 53 Health Checks](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html). + +**Terrafrom Mappings:** + +- `aws_route53_health_check.id` + +## Supported Methods + +- `GET`: Get health check by ID +- `LIST`: List all health checks +- `SEARCH`: Search for health checks by ARN + +## Possible Links + +### [`cloudwatch-alarm`](/sources/aws/Types/cloudwatch-alarm) + +A CloudWatch alarm can be created that uses the `HealthCheckStatus` metric emitted for a specific Route 53 health check. This allows the alarm to publish notifications or trigger automated responses whenever the health check reports an unhealthy or healthy state. Overmind therefore records a link from a Route 53 health check to any CloudWatch alarms that reference its ID so you can immediately see which alarms will fire if the check changes status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md b/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md new file mode 100644 index 00000000..b8afe159 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-hosted-zone.md @@ -0,0 +1,25 @@ +--- +title: Hosted Zone +sidebar_label: route53-hosted-zone +--- + +An Amazon Route 53 hosted zone is a container for all of the DNS records that belong to a single domain (for example `example.com`) or a sub-domain. It represents a DNS namespace within Route 53 and is the primary object you create when you want AWS to answer queries for your domain. Hosted zones can be public (resolving queries on the public Internet) or private (resolving only within one or more associated VPCs), and support advanced features such as DNSSEC signing and alias records to AWS resources. +For full details see the AWS documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html + +**Terrafrom Mappings:** + +- `aws_route53_hosted_zone_dnssec.id` +- `aws_route53_zone.zone_id` +- `aws_route53_zone_association.zone_id` + +## Supported Methods + +- `GET`: Get a hosted zone by ID +- `LIST`: List all hosted zones +- `SEARCH`: Search for a hosted zone by ARN + +## Possible Links + +### [`route53-resource-record-set`](/sources/aws/Types/route53-resource-record-set) + +Each hosted zone contains one or more resource record sets. Overmind establishes a link from a Hosted Zone item to the `route53-resource-record-set` items that reside within it, allowing you to explore every DNS record that will be created, modified or deleted as part of a deployment. diff --git a/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md b/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md new file mode 100644 index 00000000..89aca6f3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/route53-resource-record-set.md @@ -0,0 +1,27 @@ +--- +title: Route53 Record Set +sidebar_label: route53-resource-record-set +--- + +A Route 53 Resource Record Set represents a single DNS record (or a group of records with the same name and type) that lives inside a specific hosted zone. It defines how Amazon Route 53 answers DNS queries for the associated domain name, including the record type (A, AAAA, CNAME, MX, TXT, SRV, etc.), routing policy (simple, weighted, latency, geolocation, fail-over, multi-value, or alias), time-to-live (TTL) and, optionally, a linked health check. +For full details see the AWS documentation: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RRSet.html + +**Terrafrom Mappings:** + +- `aws_route53_record.arn` +- `aws_route53_record.id` + +## Supported Methods + +- `GET`: Get a resource record set. The ID is the concatenation of the hosted zone, name, and record type (`{hostedZone}.{name}.{type}`) +- `LIST`: List all resource record sets + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +Because a Route 53 record set ultimately becomes a DNS record that can be queried on the public or private internet, each record set naturally maps to an Overmind `dns` item. Following this link lets you see the vendor-agnostic representation of the record (name, type, TTL and value) and how it is consumed by other infrastructure components. + +### [`route53-health-check`](/sources/aws/Types/route53-health-check) + +If the record set is configured with a fail-over, latency, or weighted routing policy that refers to a Route 53 health check, Overmind links the record set to that `route53-health-check` item. This shows the dependency between DNS resolution and the health status of the monitored endpoint, helping you understand how an unhealthy resource could affect name resolution. diff --git a/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md b/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md new file mode 100644 index 00000000..afcfb3f3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/s3-bucket.md @@ -0,0 +1,55 @@ +--- +title: S3 Bucket +sidebar_label: s3-bucket +--- + +Amazon S3 (Simple Storage Service) buckets are globally-unique containers used to store and organise objects such as files, logs and backups. Each bucket is created within a specific AWS Region, can be configured with fine-grained access controls, lifecycle rules, encryption, versioning and event notifications, and can serve as the origin for many other AWS services. Full service documentation is available in the AWS User Guide: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html + +**Terrafrom Mappings:** + +- `aws_s3_bucket_acl.bucket` +- `aws_s3_bucket_analytics_configuration.bucket` +- `aws_s3_bucket_cors_configuration.bucket` +- `aws_s3_bucket_intelligent_tiering_configuration.bucket` +- `aws_s3_bucket_inventory.bucket` +- `aws_s3_bucket_lifecycle_configuration.bucket` +- `aws_s3_bucket_logging.bucket` +- `aws_s3_bucket_metric.bucket` +- `aws_s3_bucket_notification.bucket` +- `aws_s3_bucket_object_lock_configuration.bucket` +- `aws_s3_bucket_object.bucket` +- `aws_s3_bucket_ownership_controls.bucket` +- `aws_s3_bucket_policy.bucket` +- `aws_s3_bucket_public_access_block.bucket` +- `aws_s3_bucket_replication_configuration.bucket` +- `aws_s3_bucket_request_payment_configuration.bucket` +- `aws_s3_bucket_server_side_encryption_configuration.bucket` +- `aws_s3_bucket_versioning.bucket` +- `aws_s3_bucket_website_configuration.bucket` +- `aws_s3_bucket.id` +- `aws_s3_object_copy.bucket` +- `aws_s3_object.bucket` + +## Supported Methods + +- `GET`: Get an S3 bucket by name +- `LIST`: List all S3 buckets +- `SEARCH`: Search for S3 buckets by ARN + +## Possible Links + +### [`lambda-function`](/sources/aws/Types/lambda-function) + +An S3 bucket can invoke Lambda functions through S3 event notifications (e.g. when an object is created, deleted or restored). Overmind surfaces this relationship so that you can identify deployment risks such as circular triggers or permissions gaps between the bucket and the associated Lambda execution role. + +### [`sqs-queue`](/sources/aws/Types/sqs-queue) + +Buckets may be configured to send event notifications to SQS queues. Overmind links the bucket to any target queue, allowing you to assess the impact of queue deletion, encryption settings or IAM policies on the integrity of the event pipeline. + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Similar to SQS, S3 buckets can publish object-level events to SNS topics. Overmind records this connection so you can verify that topic policies permit delivery and that message fan-out will still function after your planned changes. + +### [`s3-bucket`](/sources/aws/Types/s3-bucket) + +Buckets are often paired through cross-Region replication or configured as website redirects to one another. Overmind creates links between the source and destination buckets to highlight dependencies such as replication roles, encryption configuration compatibility and versioning status. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md b/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md new file mode 100644 index 00000000..93e4c5db --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-data-protection-policy.md @@ -0,0 +1,22 @@ +--- +title: SNS Data Protection Policy +sidebar_label: sns-data-protection-policy +--- + +Amazon Simple Notification Service (SNS) is a fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication. SNS topics allow you to fan out messages to a large number of subscribers, including distributed systems, and serverless applications. The SNS Data Protection Policy provides a mechanism to ensure that the data transmitted through SNS is compliant with your organisational and regulatory requirements. This policy is used to define and enforce encryption, data retention, and access control practices on SNS topics. For more details, you can refer to the [official AWS SNS Data Protection documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-data-encryption.html). + +**Terraform Mappings:** + +- `aws_sns_topic_data_protection_policy.arn` + +## Supported Methods + +- `GET`: Get an SNS data protection policy by associated topic ARN +- ~~`LIST`~~ +- `SEARCH`: Search SNS data protection policies by its ARN + +## Possible Links + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +The SNS Data Protection Policy is directly related to SNS topics as it outlines the security measures and data management practices that are applied to messages sent through these topics. By associating a data protection policy with an SNS topic, users can ensure that their SNS workflows adhere to the necessary data protection and compliance standards. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md b/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md new file mode 100644 index 00000000..308162a5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-endpoint.md @@ -0,0 +1,12 @@ +--- +title: SNS Endpoint +sidebar_label: sns-endpoint +--- + +The SNS Endpoint resource represents a single destination—typically a mobile device, browser, or desktop application instance—that can receive push notifications through Amazon Simple Notification Service (SNS). Each endpoint is created under a specific Platform Application and is identified by a unique Amazon Resource Name (ARN). Managing endpoints correctly is crucial, as inactive or mis-configured endpoints can lead to failed deliveries, increased costs, or even unwanted data exposure. For full details see the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/mobile-push-send-devicetoken.html + +## Supported Methods + +- `GET`: Get an SNS endpoint by its ARN +- ~~`LIST`~~ +- `SEARCH`: Search SNS endpoints by associated Platform Application ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md b/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md new file mode 100644 index 00000000..f04a1f58 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-platform-application.md @@ -0,0 +1,23 @@ +--- +title: SNS Platform Application +sidebar_label: sns-platform-application +--- + +An Amazon Simple Notification Service (SNS) **platform application** represents a collection of credentials that allow SNS to send push notifications through a specific mobile push service, such as Apple APNS, Google FCM or Amazon ADM. Once you create a platform application, you can register individual mobile devices (platform endpoints) under it and publish messages that will be delivered to those devices by the relevant push provider. +For a full description see the AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/mobile-push-send.html#mobile-push-sns-platform. + +**Terrafrom Mappings:** + +- `aws_sns_platform_application.id` + +## Supported Methods + +- `GET`: Get an SNS platform application by its ARN +- `LIST`: List all SNS platform applications +- `SEARCH`: Search SNS platform applications by ARN + +## Possible Links + +### [`sns-endpoint`](/sources/aws/Types/sns-endpoint) + +Each platform application can have many child **SNS platform endpoints**—one per registered device. Linking the application to its endpoints lets Overmind surface which devices are affected by configuration changes or credential mis-configurations in the parent application. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md b/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md new file mode 100644 index 00000000..82565304 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-subscription.md @@ -0,0 +1,26 @@ +--- +title: SNS Subscription +sidebar_label: sns-subscription +--- + +An Amazon Simple Notification Service (SNS) subscription represents the association between an SNS topic and the endpoint that receives the messages published to that topic. Each subscription specifies the delivery protocol (e-mail, SMS, HTTP/S, Lambda, SQS, Firehose, etc.), the endpoint address, and optional delivery policies or filter policies that control how and when messages are delivered. For full details see the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-subscription.html + +**Terrafrom Mappings:** + +- `aws_sns_topic_subscription.id` + +## Supported Methods + +- `GET`: Get an SNS subscription by its ARN +- `LIST`: List all SNS subscriptions +- `SEARCH`: Search SNS subscription by ARN + +## Possible Links + +### [`sns-topic`](/sources/aws/Types/sns-topic) + +Every subscription belongs to exactly one SNS topic. The subscription’s ARN embeds the topic ARN, and deleting the topic automatically removes the subscription. Overmind links the subscription to its parent `sns-topic` so you can trace message flow from publisher (topic) to consumer (subscription endpoint). + +### [`iam-role`](/sources/aws/Types/iam-role) + +If the subscription delivers to an AWS resource in another account (e.g., cross-account SQS queue, Lambda function, or Kinesis Data Firehose), SNS must assume an IAM role that grants it permission to publish to that resource. Overmind links the subscription to any `iam-role` referenced in its delivery policy to help you verify that the correct cross-account permissions are in place. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md b/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md new file mode 100644 index 00000000..317b0b13 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sns-topic.md @@ -0,0 +1,22 @@ +--- +title: SNS Topic +sidebar_label: sns-topic +--- + +An Amazon Simple Notification Service (SNS) topic is a logical access point through which publishers send messages that are then fanned-out to subscribed endpoints such as email addresses, HTTP/S webhooks, Lambda functions or SQS queues. Topics can be configured with attributes such as delivery policies, access control policies and optional server-side encryption using AWS Key Management Service (KMS). For further details refer to the official AWS documentation: https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html + +**Terrafrom Mappings:** + +- `aws_sns_topic.id` + +## Supported Methods + +- `GET`: Get an SNS topic by its ARN +- `LIST`: List all SNS topics +- `SEARCH`: Search SNS topic by ARN + +## Possible Links + +### [`kms-key`](/sources/aws/Types/kms-key) + +If server-side encryption is enabled for the SNS topic, it references a KMS customer master key (CMK). This link allows Overmind to surface the relationship between the topic and the key that protects its message payloads in transit and at rest. diff --git a/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md b/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md new file mode 100644 index 00000000..14493b73 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/sqs-queue.md @@ -0,0 +1,27 @@ +--- +title: SQS Queue +sidebar_label: sqs-queue +--- + +Amazon Simple Queue Service (SQS) provides fully-managed message queues that decouple and scale micro-services, distributed systems and serverless applications. A queue acts as a buffer, reliably storing any amount of messages until they are processed and deleted by consumers. Two delivery modes are available – standard (at-least-once, best-effort ordering) and FIFO (exactly-once, ordered). Queues can be encrypted, configured with dead-letter queues, and integrated with other AWS services such as Lambda or SNS. +For a comprehensive description see the official AWS documentation: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html + +**Terrafrom Mappings:** + +- `aws_sqs_queue.id` + +## Supported Methods + +- `GET`: Get an SQS queue attributes by its URL +- `LIST`: List all SQS queue URLs +- `SEARCH`: Search SQS queue by ARN + +## Possible Links + +### [`http`](/sources/stdlib/Types/http) + +Each SQS queue is identified by an HTTPS URL of the form `https://sqs..amazonaws.com//`. Overmind represents this URL as an `http` item, so the queue is linked to the corresponding `http` item that models the endpoint used by the AWS API. + +### [`lambda-event-source-mapping`](/sources/aws/Types/lambda-event-source-mapping) + +When a Lambda function is configured with an event-source mapping that pulls messages from an SQS queue, Overmind creates a `lambda-event-source-mapping` item. The mapping item is linked to the SQS queue it reads from, allowing impact analysis when either the queue or the Lambda configuration changes. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md b/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md new file mode 100644 index 00000000..7f776d0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ssm-parameter.md @@ -0,0 +1,31 @@ +--- +title: SSM Parameter +sidebar_label: ssm-parameter +--- + +AWS Systems Manager (SSM) Parameters, stored in the Systems Manager Parameter Store, provide a centralised, version-controlled repository for configuration data such as plain strings, SecureStrings (encrypted secrets), and hierarchical documents. They allow you to decouple configuration and secrets from code, share settings across services and accounts, and take advantage of fine-grained IAM access controls. See the official AWS documentation for full details: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html + +**Terrafrom Mappings:** + +- `aws_ssm_parameter.name` +- `aws_ssm_parameter.arn` + +## Supported Methods + +- `GET`: Get an SSM parameter by name +- `LIST`: List all SSM parameters +- `SEARCH`: Search for SSM parameters by ARN. This supports ARNs from IAM policies that contain wildcards + +## Possible Links + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +If a parameter’s value represents an IP address or a list of addresses (for example, a whitelist used by a Lambda function or security group rule generator), Overmind will surface a link to the corresponding `ip` entity so that you can trace where the address originates and what else depends on it. + +### [`http`](/sources/stdlib/Types/http) + +Parameters often store URLs for upstream APIs, S3 buckets, or internal services. When the value of a parameter matches an HTTP or HTTPS URL, Overmind creates an `http` link, enabling you to follow the dependency chain from the configuration to the external or internal endpoint. + +### [`dns`](/sources/stdlib/Types/dns) + +Likewise, when a parameter’s value contains a hostname or FQDN, Overmind links it to the relevant `dns` record. This makes it easy to assess the impact of DNS changes on applications that retrieve their endpoint addresses from Parameter Store. diff --git a/docs.overmind.tech/docs/sources/aws/_category_.json b/docs.overmind.tech/docs/sources/aws/_category_.json new file mode 100644 index 00000000..3e7fb1b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "AWS", + "position": 2, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your AWS" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/account_settings.png b/docs.overmind.tech/docs/sources/aws/account_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a244a522c60a7a30943e11292545c4fc31820953 GIT binary patch literal 45804 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 UaN%#Yb>Qb<=VbfX=1kOo0KsHH^8f$< literal 0 HcmV?d00001 diff --git a/docs.overmind.tech/docs/sources/aws/aws_manual.png b/docs.overmind.tech/docs/sources/aws/aws_manual.png new file mode 100644 index 0000000000000000000000000000000000000000..9a01d077f0fc07abb4f27f5c9c5fcfbf43224853 GIT binary patch literal 1896097 zcmeFZXHb)0yElp`SP&6Q(9nxu37}NzqVy_7X@MZU_ue8P0>)4Uq)P{t-fQR`BE1uO zhe!<&NY3rv^M2j$exCVz&dizT17u*BSz+C4UDvN&34N_1OL+}^jf8}RQeI9m(#hB=S;{njQvQ)2rUJI&Qi6pd?o3*K1G3 zR-Ye@8+!+|)T2+(Sl^x9UKiD(Ri&vRwOhQBEauO>XA(w|6asSbX_!KmIE73kz`E_1=TOy=ym###Dd# z&M-NkfdjB}|LZ#SU%oUvY{V}8WUtWU5-y6h4a7Q;Ph5ywS0Nj>ciKD;01St;k;tZSOAJ6bt+YE49 z7T{Zddx`%vnt$u;|EJOXi{0-(jpkq6u>a~W|G#Q9b4?5Ip>>yQ5`P=g+UqjixmLdm z!q5^MNn(yMiL(Yi41agg_tyZl#`qt!_AdbZ|Dd&hhpGGrt^KRO`#)&y-vPw`AIJhL zWZ48NX6=9HR{Sq8+}z~{0BXPNS}mmWr%3q+-?`EZZvtN-5nmdaEB!n4*P|3**Rwwb zQ$PNB1ivo=BnWr0;W$b7Z#1JuDg!_=qVl64dEYogp_HR>HRz?g#4}!&-(lC*#Fz14bVIh?pY)WY2nSH&UhjVNvE6Si>2@0%{zB?XezG! zon)nYm<|E2Oeb&qD5VBI4J( zZi#q_aGw|*b01GM!!MCwx6;XWQ78E2@5hX|p5$U)YEpZs;WI+zG5vi=2)0$w&+_=i zzW_4+7li@%69y=7a{^(cAO{#}9I2W60We=9qHv!vn^Fxo!V)18L2< zJjs)4tTMYE(1qQvt;WS!X(*hSi_4>Q_g6L)TVlVh9cR4q%{N&HxJfNpEQKnT+MXq4 zwaY>@YQVyVmCg;P$ifh(Jw3zyuKb4nEHxG{wm(^Q#zJae3_QBZ?pR{lfi=AiMF^m77HGE3V?s>~qbQT3?3FZcQp1`|b}Z#SM?!q+t1T zI7eaLYwBOIt{JvXCqStP3Ro8J42!3tA%l;;*U2A^u&U=8(S7x?CGEN_>%c=p2q+p^ z{zj*=*vOF=NgKq^w0f}*eIouLyr9;vvf5(5cHI>>Xe*9=gRLK5F7)CcGF*X7O@Rp% zFXOjWWH*r6!yC$Zg5ao-4s%@hq{74wBkk^2spfY&pc~$dJMllqf9`UF9w+$k**n$6k$fiH)lGk}K-R`TnZ}^Jm{dTR6Ky z4y26YMql>urgC0Va+l@uw9Mm}FCDd00&eaVY>^>~ERjX{hqpp|7vRF~PceA#4LzSx z$c)IoU|Ta%0K3-!_Ql}WYpPhxFj{e#zoDAi{n@6Kw{`mrmjXy;2lLLyhm>FXp5o&! zzTaP$5X9T0(2|qEQicfau#M%1^P8=c)77;^PtS6DtuxgYwVHO79M)i24krf}mSEYA z!Sy4li{|wPFr(t!EgSo;{?TmUML{4sR+GmvveW}2K=D<-VRYwvT^L5prbAEFQ@?Ic zAi58mEohsDLmKFdc@5=x+@`lXjC7Dof9HOE^f-IWowg&`3;SjlrIPbx#7p0*{A1FX z73f?LZKJtO?y?n!p;iO4in>S%oE>jA{@DHVnw9Bb%foFU~GGXe@ZoycyV9lAFo=o z7lEFRZIbpGQ+=m%%=J7c@gp$+hrUb#bK2#o?Gb`iPB>d8*L+((L?I04FG81??%e(G znSCg)Y>IwD;`x{Q7FOUT>TNCOwwu;ua={1IcN0)pX>oBe`?8Uw0--Z==%5G>Q)1#Oa6Xu4b@@I#xDt~`q1n3d7tY-&yzwvK47>yr8c(G{$}hq zK9yK2+yZR#+gjGu)i**v-ex%(B)3W?`;?#j0{`#o76wE0=^#Zd@Ko zM4J8JCh^vRjn3tbzllk|0!Q@YZju{e1nSu#&aK2&&Q(;h~3k0(6(OQs9 zo)@sVx$^_Ji!1mzEuS=0%k^ylRZLE7Hx>z#F}{!N-~l7q=&mV3K!>th z>Q|mtrLGe3?@8ur8)&uvc{QpBaG{EM8I+i=ge&ujW=mLLx(lO%q*xtUB45j~%x~^n zNXpth)u z&V3?@s;lm@l}B^?>D2DC14aFt9SyU;V`}aY6P0X``EGhgM^vWRxnE%;y82d!K$oGU zpto#=+ynDW%}kUOt8>Xiz%dqlRvmM9KZH#`n(h5f$qPMkbem!4O3Cr|Sm>At$oCoH z-pJeFcl91JTs@HeKUsin=^4cuiES)){!*Ou)kEh4I+>Ikly8-@CKwenBq}lpDv$0) z8$Ev9rrCRxzSC6{hSy#aOTr~4If;|pn=SI z{n&_YdEC$8RM+smnwwwYvFIx_4MidHvwdOr!^Q50eFWiYuLloesWK(*jjOd(=9#>V z>dIt`z8czLbPLh8WN*_0ZC@6~C7SAI-vfi3y2`RDO%6 zeIwY9&YIem7xkm&oz#ZKEv`xA&?KEI&?`u7$f4u8VJlvkx+Q zI*rqHzB6XJ+~+?-8;+3o%*^;w=FP!yRT1Jvy;SE^2=l&u{3Invyig)=Leu^Es&b#* ze#6OeDj}tPxTZdZFM*Tx(CZZdGe9Jw;zBWQIZsk#_{DHrQ`&kleTV!5cp2mPHlVl% z1>CtNTX9|TaZJP$)tnHQ3Cg(X8U4FvFaLKG=3h9+ z{!;)Vu0?{d=LV#bwd^627NXmAykGW@I>W$9LmnvvZ&7fplQ7q^l2Z5jSBt00aLj`> zC>`e&lCb(b=~EYr5BY=O?!2&-#+65z=~1d}Y^k3V1gK-6JVqTw5;-b1yAqVl6KYap zAgLMQSE^RqX~V*-v|wszUz_QJoJ~aSw{MJhlgPdlTtBRGwNA>NrB|#MiPE)a+pZ2t z@;fqPX4X6TV6$=?1RgHW>+|_$Ik4}opY#AF%J}*kL6j~mbHq-ubSC!g56+I9yF$X^ z<==Gb2kzK!b&b>ZjPILdX+|BE-_?QX$CQth{ESsBQRmXT^7NMD>Jp2zFqdrHY=y@_ zmP3ktmSdNd`I3yg33WZwTCSRyFBwV@?gsufGzWVOic}JXWV!Ty+;N`KYbs9>^mruC zkE3Z4qlPx?bnbOiIu@h(9rCkqI1=la}{;&EG*J%wU?{bcG&5L>)b?}BBXrn-xToO1Xn10wbmzgi)#)s9_B z*;Wknb#`n&@%5cq(w;&`s}F0@_fKO9XoNNN%yTyd zr{~(keR$@(4X>OvQ-!+2d{aGRQ-&6JR}#qQqK;N zH7i+T`H`O&+u=LH! zrD%EYok3;Cj7+VoKm1%?C`mvn${PpVN9MOZ_`WMajYwK&v{8{L$+5OR>JYG+iKH9D z6Yj~eQ%BA?DY$8=m%LHZy~BAsd|Zt+=u{URbZx;rEkcQ1L&);Ra;8v3^k^fg6D4uK zhE-KuSGl%0Hr7aCG1$oY@-C|F`LL^G`q<2uo_@(iJun|_m@EKdmgatIAKeHuAr&p-&F%_77MXTprY@L|oW#}FnloQn*}S#Ne3 z*+923YjNGC(Behm2+o8retcFJ)HEa(uPVcq8lwAEoi$wG+SRUB{+*SMYSOqiX|4}M z5U%;h0(>8*0_ThLvNxetpoc%V=)$^e339fdly;Dv zsmzHz$9xq;_G8p4N+s(-w7fif3?LXRCewSC)pFKO=>e?yL8Qa0miYaBbU$94fz(~K z)I|o#B#>xpS)n^=xY>FbjpXREwcmOQRY0Ah-2u}tZ^u+vg-3FJdGGq;sgq4- z{d?F!p%be3#Bp>X+|Pd9J++(IkoF|qih7TU7YN_9%u(s(9ay;N?d^+?x z6;Xi9n!MjFn4^EPQfB<>mGx%O2PD#Gkkoogn&$3j*=Eqo61gc&sajvjvqJt~)iUba zVVOWHnbpxUIA)Cw>0T@^6V+ALxm1)CswhNXnx<%#7m4X@#tPSe#S6m{ABj1vO zOy_#zYD-f@e;j3sA&AJf#7|^T0Rngk{K*t@SYKAucAN^A;BK&YQ*lmpUKBbv??tNZ93v6)N;5sPCM+XXed&g) zBMEK9ErO6xx!l)}PxV7MA#HPh1BNT3+0r{D6Q2R(7TTfzocBskABMxeM{NIbKNHJ$ zNhxHgKq6-ze}cL_U*>>~eC1R$pu8m8jWxqla*SDq)}@-$ihO0))ReECB;C2@`3MBH zDbs`~RBIMBCN~ry8tcneAvK^62P>t%G}t<>7w(n~v>%BpV;*n#+^Scl@Adm;82AYG zOXRp>T!pzZbJe8JC)(Iqr-N2lqvp0N)4lhZ0rSZdh=YDA|MGqZJ2rcG?r-hh>^xPju7T8p(-8*!t) zIH}^Y)8wGg<-Kiry#}$J&dLs3`F7Tl(%4viBr!kJwDN1&ZtmAFnxamofZ7E23Y#@~ z9Jt%se?mc=K6DmlRpm2zo}jdhWn5m)38&Z_lJ%VEq)Y|0jiju|1@V;obqH{d_P-*g7kah`2^;& z7yaRB4Rstc3BNDH#kDDfl3;B$PN1`P=$GaW?G`%GMq3dq%Rs1ujDm!KG<5Al3v=xG_I^?^L_?)WPDa)$vf z9n*y6{5XBE3aB;PZXIE(Bj5j9u^fK+zGYsHyd{l(C@=G8enIDC_otuvv02c^T?*9? zfjT^x_eF~T*1gbPQg=j;?-c8M@R_yyY@n=)(?)XAz%)WD8(wpsEiPaLFuGJE^UK8~w`5apd5{C&z?0BAEYm_wSHc|gB z26}~M_DG;spKkYb;xGZqkKw_`rKmdFdp#n!XV30a91Q2IVxldSgd91ADyD_vL5|04Q|y>iL%n+{tq)kRB}EgW9??M!}@;LgN)MS)PCC5PQH}Pc~qoeWTaquCuWr!ymw;o zN*q4$WpU*N`D|z1o%i)ll-hmO9G-*_`ol>< zZ)3|N*}?l>B`L@rT`uO$>MBvLtR-1%%M%ZNPR5_vu}KwExHuN?vACmYzpFd+5_j_O zjyh-9N?_}43N7)2i6PsP2%;FL0^$2{s;vvnfivEh%e%tHcK;1uWffv<0mWJSyYTBi zyGC8t`JF9O{i9#u`1Rtq_MN+y(g62%QqPmin23_eu#`obf%b%I1wRGV+1UQ<8O^2z z_kHd#0#zcUGZa;qZ9l;|DSoc=&TrKT)2@!xNK2uA@LQHoEm&6B@f2r>MYp`WibsF! zGIiW>{u&!b7 z3DMvJhaHmWJ|y7^U9R8pt?IR$aHX8BJhtaPCm_-5d!J}l$IzA)lOV!Sni~2XP>@BQ zm;C89vb9rt9_D(05z&j0IdMAU9uM9T88~+j#ba}|B(^Zw-bB|zZdCBUleGAs?p`DQ zkLBQtYPN4W>FXf62uap${p9@u^Bs$TN&M(3Oc^odi&q#9C+3?i{;B9Ecv-TO^~E~2 z4C;8LoKli+?L@x899bm6*LgPD9$m9{Xerw;ZGTeSklo7H{8njlTt~S_LhMl|@HMIS z$`|M~d=HP)N8@M?8$?AmyHisHmk>0rA|EpV@mzUoTwk##N5u1_s~{}tj>a<@;!y4! z^t!UM0{EQvvlzu0Ki>ERj>_vI?kkUk1^4;4U8v6!$sr!f>k!5`p1Ap_tH_Jqimf6i zVSQJdPDM|oAbqtTSpX1uKws9kup+*;Dz6!)Ri-^{P|sspHd0e)3)r%=*SWMl&|lFO z&POo!FLZG56QpvR!GTbZ3AklTSpv?z#+?>KLKnutbHL6+r}`lEbEg>>vtD)8n!%{G z#PIQ=iP5{RjP@5{2^aIqJ^~D`OQF3UXpl8=;xmr(inXK8oLm%JCu#MGDU}5nGA{kK z!5KKkE^0?^BRSII4C_qqt-K3#Eg~6f>SqOZ>SAidM|1D#mv5SC?5ZnFy*<;!DvlSG zRq^>=7$X~{!ZaCz3Lk;y5{c>)Cl@Cx3F|V>7$x1$oY4wh(H*q{DdMv+PI)HO#U?CV zp;AVmZYYSwoAzdG&?Fz&%fZ7&l27dI+0AbVqU)Jo=2XUjoK*TiE~T&dTB(WX(biY_ z94Onf)Y7lXLSlo(#sXM;k1rK;dW>dQLX{k3KtH~VhWWA*&qoCJRtr%vesHF#L|TE0 z3IXVsk?>!pJeINoxL-ietha;ix||z&X>d4?|(Y7|JD^aD+1uuc~Jo_FlpO$7ljcWI)=US zJFm0zFSB4XG3LJ=hk}2JLjebxk+{z)3uYbkh;Jtz9RHk*<_??MgK^NGY?QHbjHDnJ zbyXF;qU74xQa*Pph%*z9tC)xu7a9YCPFdz7q+T))WY7BCQC@_59|8h%YO?#Xs<-mV zf^r<@uPK3E0Aac=4at;^O_%he)?xv3_tWl z|CJy7SKXaFJ^2n30OX}zXx!C=?Bu?}H99&QE|&2RQ{FORS(X=iM4Y?K%};89A8HP< zL!mK>AX++A2z%bX72Qx`2z^FmQR`Vm_D+t)a~r$@&a?=9=zKXNbhw69P?+^3nUR3E zK|PA@()8PvRP)=-wCFfFhWUsSIE=8#N;Br(;OgP=G{#p+Sex}~wsdBM4m;pE+JW|5 zHWOqIhI=$hLlGQa8uw3}$&G5bhox7Cn9|634(VJC#J*2W{niB$H(hGge+b%WTDuB~ zUw!#LXK?%Ij4-JQ&%FB^vmoa8O0p= zZd2(N0QxU+cVO!?>q&_1XtV-klFTqex^ED2OGBwIwQU>lF{)_x$t8$-(MeIfRgd$It!ld>K~* z(?4+2Ia4)``D5u{DC6H?*hS0Y^29XG8;ep9deM4Xh747h_OCdWe?DB0NDZXk%#z^0 zQk~wMVe5EHekaTP<|EC$P|8H8j0)9zjpYE7O!vw2fT}|ldik5n&i75Tp8OG*7;hv1 z$L6qO5F4H)_}e7&LUBar^E**4JdcUrb|q*%2hhJr;%^}1*LzNlnIjdA z6>`9~1kH_~$14caM=J@lFGHIbK1ld#7A=}_*;y;MHM=FD?fFlQg=`L%bA=+#yB=4iJu6n8GUQ&YAOvhG1{zvHbiVt$<}FI*X_d~ zKn}C4E8!wK{)#$=4%@NP#8GuW^3+w)A^El6;xXCz9VhEFN!KlJ&BT$Pw&v?F>)6rf z^U^Qdu{%flPj}z$F@#%nsK8<8?6)IF4I%l2@v!qqily~m+6I7hZ32Phc}NWwhh!dF zu!_?3-NM(OLID8i2lO%|R3&E*ks~b_+eSP?=8Er>a`1pvvm{c+YbGzEM+Hz?{4a8; zo1i?8Po!G=(JzCjX|@vKmcvDYdp1aL|JHR;ODeHMEe_X#7sZ%9_VlPwS-^mr^t^0$ zYTu+dwo5YAfr8PvM`|x|g_K$zs9W2J9~&r;uQ1s3t0AC8xT2O&EuV(_ z+5NVcbm8)_+n}?`7}FX1=3H%&CUeM6IA08KsHvFq;#hblf4VumDE-{dYBfz#+MA(L zch3jg*p8^bc>hLlX?>{xaf@?>laMoNl@QY5%Rv(}gmxdZV0)!ks#kbJzfkPP8F2(N z;*gat={yMR_t3E$OY^d*oGxZ&XSBGy2fqYcg8h(l0e0~|PRER1N3z6Me6+wka zxo~@?)P6K}H*gboT{t+{FGzpQCiE_x=(rzScsb!&xlE$fguEml8`0g07*2EvM8n%m#83d1FF4QGVJ{@RMAV4q1DhGxRYHDeSa# zRr%~iq)ZyQlZOw><=_s1$E&M@+lMv>USf}y_PI>nx-pmLBVB0|X*WSUgSpW%B>^52 zXRGz=-zETz0~53EEKD8aT=+VYtKY1Tn=yLnSC$YNJ~jvn4{M!dSDW| z?o?Ibz15*GAJ4%^Cw6w!*|&$N<03W*Z`~r_ya93Ku_bOGvFdj!IROG5XXw^3XS1a= z2;)d^s`D2tKaW$a>NH@!8)NiXK%}4M8{bc@%`hlQ5pUE~D{;lXrKvM|)`I0cjhrZ| zr#C_cwvu}4vF=-!fb6kix06MhSZ(nzsokFZKUn~3q;{)4VPxP}YC9#~c)%gMpmfqz z7GV(d@)l(h#xXzcbj z#wy<~W7I-{E3x9K9#>+q!&LuKHh}lLjz(7?aw|r*Tv5VAuSFb!cv2HKS9+M6*79Eh zj%@!Ae$R$VtnB*H_5^k7v3l2spL+LP`WC*$D@{u807_Jqa?WiE4Hb^t-r2#N{WKNd z63I!U{T{x)=m4Z7bo0Z+;sQ`(fMdQetnkX=bi<`)aK~*s7o7?fupW(f@d8~NM*mvX zb-1yuGUiwDaLUkde=SLvkMApbolZxDoVj8Sqd$x$n|29PspQZDTt|XJs zLD_zZNdz1x$!r1A5&k+p>2o65eZcd-m1j56|C0P~d$U_F6+j=KW|$O93GNAm_b004 zuq7)f3>$^ot({3~s-i2Ww&UXp^v7togneP`$t9by$~m@N?)taV{Y7Rd3U3#VU~kNb zfV+oXd*QO$HouGzSNF%PGsg@tj7TYSW|?yz{?8}OsN&VG7lB_0{7L%3cG#$TzgR*+ z8=Z!H>D8nI>?OGHZ~ z86?no%vX@YP$kFB)e-7uL`hOiyGiwK?6&eQ?kvbsWB=GRe+Dd}9*@+Zk@TR`YoSCz1t z%SrSduE_X!3K710Lnjw&U+KKJ?q(o9)77=8KA3K1JyG>#=$VA(T6$10(*JB7kZtKI zmr<(1POq^&@A5f<4;v2JZzG7(>F)q-b`K3E;)^UgWZN=ZS>J`qzPvR=I{mU&3GO!} zS!(*Mfr;dKya#AsSXik1H-<1DnUs_wpKnm9mM;iV((E*Skc#dCFk`KV-kycIBA`8R z24wF3WcpQZgF|(Fn~2#(Dab+yxSw2bQ5(fa+brH$nwlwJH1-yepU*tRSQ+!PxszX7 zf7?YxCY1*}P1E%rp=K$-8$)XuVX5%fUzAuL>riU4Lo=0(eb0+I>gT0Y^!_kpP>jt{ zdj*PXn9o?0b(1@ z%_?{$3jtSntFjt^>4uyK`7OqaX6bvX83{ufXMJ@`YY9NjR&M+0OrvY!_*dQtiO?W})(2SL zgD;Baby_a>vKZ`o42OB4v|nrugw61RnD=END=EhNxDMKSt(9i@{^l=!zC=!n`=c9_ zBC<$g{kTZPD*p1VtHI{}0+a*WHnzHRufr#7+QSR7Ak*2Z8Z@oWc-0}Zrhx#5w1{LB z`CP#Jbn+p-!4;N(os(OwM3-xbOIA+{Mk-t_=Kl8?B(^ZNdqaB!V9-ycI|+lLQ4 zMzH_~XC@(tcPTs_GYrRMTkM4oz#IE&Eq=m$R$~?wHfu@X^U`O(jxpm@r`6a9CPI#^ z>mwS9ic$6Ax!F}J%BAGD{kGV(XXAS2t8eay*H_QM`qQI?KSl^*E>*B6Xh?)ef(4UaZ5FD6RR!dIiJ6$?BBuMd zb0fPb0AAERTI|wg_5zDJ=w)Ij0QmQ?Xz<>TRaZ{{D+E7z zbW)U5Vq7i~i~TNkQZX4V1BVce#aM~Xti~P5MlS`N?bcTNP>;9ML<@1t()rxzA7X!` z2Uv>B(vRpcUo%l~1&QushDeUAOhA`~oaGEuax7+7lhQ0O}0Zf7p zjFl#-y{K9fSnPsNIRKKT92)nn&N=9uc2QKH_PG7dR3#LF`*-2sK36`DeK)gq_7UR3 zi*ua}DXwdR#lK_QvmdX%aX$!@iz)pc6lL@b`LqopTwY$xrI)zrHk_+26R5Uc7DN*0 zF=5oYr7Yc7@R^u=x{F$MJyAsxv(iB+z)2-8)*QpEAI9`|F2GT~1s9ZPnuZN$P<~s$ zkHZ5jdwq3qmqM_7n_v}%drl!oSrcCso1a1=FmK zDH}V3xrV+bi&Inx3T}BuDCDqFyQ8mB4(MMZPJgU~q`>P!NL8|Q^YPSe8i*Sk<`#y^ z;o$Wl^VbiT(o2#H5aa7=sSJ*!!U%#BG)B|7(S$nunRV&oF~vc*_kleDbUBgN2ExhmPW9aq2hW!^SKOr9~Njuw10f9a>HrjbSVe z=Fql&EAg>82vzRrV1q|DCyPvh>dJqoG7^V8M+T0jENuMc1;K7&wSHHM03q#Vcm+rE zyIR2o0xpk?tmgsZgO!IGk@L79S?uN+{yhWCx8I{3VRND693hwCNUZyG0Q{Ab(uzf2P#tSvzn8E#buXM%1(_>7S;6p_S2bB zezSj!DuNpa2fTK>UB~<(jLv-t5gkTXNxql4wBQ5UD3!>8%vQ#$LZgN|C--$#Q^rm~ zcg}VIlJa(1Z7e=a8v2!!Ari`CC0Pb#vD!ah2BK$9Po6LqW^-x^99*=-q~MnR_&(bP zEu(Z)!EDqwCLHIKGoSsoIj9Rg>j{>oAvpjl46V=90{p_b&m{-(&P--bq^V_Hg3god(87qk^rJ-B_(1xtLp2uIVZE42Y~#A3TZplhm>dTKS1b zY+LRtK>v~8Kf|3EID?4zlc4}I5()rE3^X{%WsbO~V!$4IXLV07DThl_r3Ew20}h=p z;lhAsvO6qI!cTJVTJV36UU-#1MaIif>n*Zo-IbH#Vi0sFUZqJnlEc#Sw zEV_aDM1fh2V)4s-b{SN2yf>skJK?aL>ci=I9YWD<5~YZ;LHm{ClB__2s2 zWhc@?DII)g7=x5z1K{k=QLO0XM9K4Hab2U%m{5;qBd?l|rZ}l0`xRXDP9n|aU^fFGALe(R_xMKW>6n1sSVq=7>h07= znal1Kf~M})Jv-F(aESOlq+I^&0d65r`VoJPAUJ$0cWmfd#!l#bGXgyFSqids&qvkL zLUo4kAooI#0h}qOly}OR0}#j>3xWV|bZ-%E_0t;&yl!}%f;3z%yrcwJ@axrIST#zS zFd4$+bbsaVENRAbtnAEKjrk$rPaeXlm}$LChQak1^NNChSa6r#D-}(*BJ)qDcjxr) z0CFmqLAkq4-&h+_d_Tk7YdANRtbv6YXoP#Wk;Lt#<)InGacauxuSB_g-Y6?~oB0w6 zhH}W;qX~nS04s1Wew7OP9Gl5sG&hSq(gWSr>uERPq^;bYS5Oew>FMOyypOohk5ZX@ z^e#k=H(>`W=}X~(m3>y z@su2Urdh`RFOhBM9g>OyC5{uFt)^nM(UF2NtjMPFVD)XJ&9)l@W6YA{vq!sf+6w~_ z^oQlYu)7&%RHe@ft|eH~b+>(z0lk}?lK4`8kzRJtrb6nK8KCzA(>-4h(vRYoJt{@h;dGMXns$ghmct8N>Gppa6Zf(K&47>_AkJ7BmmJ}6r<%+oOesTU!$==x=6&_y->*< z_qmPjJa_EX^fLrb@pq}rTDAsdo^sZJ)yccMPf1CdOFwmEO7|1!`R#HplmlZH@h%E~ z2<5{gz|~gBN@u$bu2R&t=oNdyg`LCK!3q#W%kiB*VNa#s5MVLIdH zP`%J@9b#?LkV%hrb8j(RB1+ZoAPp!5v0fR>hcZK9;Pau}sR^AoR>*d!6dJSYm(-W& z;buFSS4MxBB<`^%C+>4PzTgFRwk{VD;Cp|()z*MK4}2Bcv2bhX^*e~!Dt#-FuSAd` zeCD?(1%=fwJ9lplU`*(N)iVh(RIZqPDt5bPpXTzO5wJZp-qe!V4_(s91i;L4nA!a7<)Jb2~ox!UCJT*jo!$E1cF->c2UGjZ?@-9sg{dHL< zt6h|{Q28;Yn2dtzMmSrf+<3}hnqwQ&S&pO}rMO$I*t&&H$xyk&GUEJ|$j0IDy|f)I zUzy&^O50#dronxi8Sq_~)Zz!5KYoDNAb0qbzM@RF`_VIMxDzFh5OYXs=szv;HctJC zEZCx6Vw+Cw0PXp?_D!N*SL+f-t>=jghkb|U7R#ByJ=LY$XAwZc1~BDMN(N(y&eO3H z^p(IoQ4h*jGYXE+wUI^s)PdNd9$=E(AKXQZL%X!KfsAwy%-(dlu+r(s+?PzP2&ja~ zi>*Vfh=QaTlcr(MaAHb93V;B?ODzRj9Zv}RxVpBnn8{0+ zRY25TF!Sj@7UB97#Pbc$78chDsa6>->f*Jej}PDn#8PLBz0(`>Ni4QoD7c_shm_Wa zyr}8D`E_S~>`4atlK0B^MRGa^(mZGPK*fK{l{I+?S7ohUvtc2<8(K^*W=s^ z%Mq#K@k}?=&1@%-RSexTS09U&endSyG!=8md}VNf@hY+2_nzRl(OW!riLZQ|hMICJ zZO={%SgpsgZVCL`uGf}ikGYqeuR^pJ6SkC>>Y+L0OU{elderv|?PMdaR1Gln|6))v zWk1Dr+0ruJCh9?U`{##tC3Z%9tl1vI8u;6#ZQqw#F|LmIH~P|2r*2X8Z^<%6Ys26? zabh~#%4+MXzq`#YNlb=cC6OyDfkcA)a5p}PNWE(^WGi{y*Fl)>$?FJP+bupPj}xAu*CotJ$bd1rrzuKLgXM?BW>)mU+|;didMUh)<`Rxt9)uzOK0>uZdMXe@f+L_m*| z!XCm5gIs%R$?C@tei_jQy;;-d!;E`R34DM7G`xxEW)jws9wNmOBGY{3IMg|zyrjWw z3vb;5PpWofFn}&>Ehv!4SC1{`(IIdntA|(EJ9p|XXQWlrOepEjwFTDeuS>C^`I0I6 z%>&C~tn?eDxv8bYWyOP;Qh!pv(EZF|?9VbG9HYb(Blq5cF-^BXFTGsK-rBX);0np! z?4FO1L^Dx9XE@;Ej7~hrG0Lp7-rN{Na&<8lz#2wh<4k6H9&O zZ7ldn&@J7=l2z(r_e?LH;iz)_Q~c&MH_hR^?B!>5^^7!z z`Om&445Tg;h($S94k;{&)q&s&@ z1e5y7p0L%Oe4CcdmymRR8q*|Rd)0a7!|_@f^{u4amEeAdx3mkNy|V=x<=H=sxwClq zay&+%>&23|Ow2G6FjEBCGpEJk@pT=OoX7C2_A{aXHr z_~a%7OtpA1W%J^ZrFRgAX(4C2WxWC8i~+1jC_O&FJhpqy#hdgq^?3r?oOIQ6P=4ac zS?PPD^~~OBTJmb%7C$i{R=-ppKYQcOCsQbeTpZ0=;e<`v01wGS|BCZu+pTRIiu72y z)<~t+2&GoCclgKL3}cOOa^h{s&Y60UhL>wDapRI`w;lVk?TfIGX_*McCeip6RUn>p5*R&rZaah52oA{ z5|>MtSDmX<1KLKwgU$By@PN7|W?TYFB);!;q2TM%UsAJLeg*zBx8&Lv%nbay&r*n{5I1`1<=aF0wz5O`qJOT&zT zBU$O%pxkwI`)$txAI8)}I?>sFw;LjQm#_k4y^Fret&3NJ16Hb!Eus@#t+s;>u{T+K zKM?!53(1_980HVJJY72khdjF(vB@0!@ZP+Sza~#u-}J{9p`xn>@b?BN?!YRuYTetH zie}bXWWGGLYOp?zSD_iZK88^*==xl7rvIEy>fya8%nW7*G##;TR1Iy49T9#fd-2$j zpm@xPyL%#SL?9A^g zz({yV%`Y7#v^;{|2yV2r$9DW4^`qOK*SuLA9CLw-_TuoV#_(kg$qTB3lKy?b$Vkau z|Dmt10!(9SZ$tTmtJrwP?=4h3YgUflzquYkgPFO$7z4d0;wa&lj9t)jr@x;U@&6F_ z-qCFT|NnOqL2N*Ys1d5f7GhM5 z1Vs?Z_3Gz$zSnjAd;RhKT<18a|2a+_k31gt`|W;z;`PJX=cm(QQg&kB8UjGl-1>8#t~tlJf+%k%7sE`HNb9QriH8IFlksh5yS zJ}-SLCdApa^8$oJcPx`xsJ|34k(oRbPYzXIDVq*pcCk8sF0C+*t8)Iq}FQhd{rn9;b5ISqoE%=gDfg<##j-MA(AmV0xv>@Y*_e|)n1(q zyd^;}tHCfo2Q!W8#nnr6a_zgS^Xh&z$??H$2Dfc$O8*LzTndrg2xDWVc>Bej{F=|f z%whNYr%YJ)Ep+|W+anCkT5g^*Q>{}6Do{Y4Cyo*)?c7z~cAzhInrA?W>>9swKK6PE zTkOs&G3@keu05swp&Uw4Ec@2sdcJ`JoUZtS@fs1qZCSA4jER`ytkn)l!Ipg5)+Fyf zSf#`08neDiTMOOKO(P(Pd)$%>je!bDR=P>Hs<1ljSsXtGdp5;EM)U=06<3F3oPiN; zxB2Sx@?}bM!O@u3v=Z&ydu&v zcHZYPO+`#}qFPLFUd|-n=-SQk_WKDb4lIY~uGzMS2|llXo$+obR<Fs%4Q43Acr$wbZNS8#^2Zn z0rE$RpnFp>;{8ZF5m1}P5F1ekyrfr0=Bw= zf7qM1+3^rcM&K=-mlE)rv>xvFdEB&RUCG4Z;?Xs3P?pAsW(f*S2jj=4cL5fS7G>~|psTvifTnW_lb)orcs zIP9)*wrp+(#dcgz1iNOd0x(E=#ued4e${ROr!oW-p?-3!#v%&%)-kg^0c`gNR@kn1 zD8aWIE8*LHR>{pwz$v5Vq|8Wf^vTE$5TdHUL#2ht2>74gsKt9Re)4G2Qitv*j)8wU z106*cON_JjqDMs+PI#-0>#aF&Kr2MQ7|&S0cg}e`1YI7oD{h;g2`Bpq`B8T-@|ZN_ zcC-fiy@dp~>__sdKMRW(dwTL`$VHSS+O@sq;$YjFh3S+{nMXNkZqr}uY&!{DO=?#D z=e+noXvdAN*x)Udepov(3QB&i??(OO!eFu-7dc@nx6yu4PQgujBPYbgQvA49@9DZx z1=rj8*15|IiP0DHq~i{9jbR*^!@i#@DLnh`jZ7&I^O*bY z5maB7+<9`}Y?*zD-%L}OUxrkX;L()X?J7ww$dCgv5(0Qg&$f_sktAsKx^ttul$?Tt zA%9M|XnVXT{`4Zk6HU8cwmEJ+?96J5r=;gCGH$CnG5KpAa>?A&0v9wbzs*= z=I~Z^+8u?3#oxjLcXZHt`^o3ona=-VW=J}3#z5^RLgu%PgSi>mA3-37Jd8_5vd=8D zFvZeA7I92)vqEe;t^BhYB*UEhFqW82D(H4wFP>g^F?MpUdfB-gOk9q}My$=TIJO|A zOs0ojOUF7k?>iPx=QlRZjE)^l@=_#Pk2k`?!$KM%l95X2xQ!Un66_$8i$nBu^weow z0`qEaL}0k2Phx~Do`IR9hEH-BrIq{ATWfP$^lYq+Zv6L_oWZljlzE+lxmdR$szd_) zaMn7jyJX(na5HC`)41h9zu~JP#@b*#QOL9AZ>M@b<-?o43H1biGks%Z@y4j*en^sr zt82}h_=lEjptzW4ZPAu2O|@bPxJ}JYqIr!DuvBV2hQOxWbhhejze2aX&QrQ?v9;Fw z_FZvYR~5>t651;nFS_R@wJQFn0jePLqE=|HkK}tHM&UqCkxJ5FqgT7s1u)Qb^}Z0V%OdoAYxy5fti-{lizvv^UBS~u zYQ8nugNWCcVBc3xh2ky;a8(~VAdE`_lO~q#hK0yS%kpQyu2^La#)n_bJ1o5YDZ{fd z(eqQry|M(Ly-%Zpn4~})NkI5YLj0g>Xiqwar5ix(r(5tIDp%veS{z`ky=n_u@bYT1 zeqny`BYsdseE``!96&k$9TS0c2CeL^rg`u^ku_V_{<1O^GQRiz#ziwwDt*#fakW2R zq@51if@{0;Mw5*@>FFohnpg1q^D`uzLp=X+Ow zy&%u))9UJ)WxZu^W9iVS6R`5<@h;xT*d&hB#mAkJWFrW(zhB2f==Z~5^4-9Y{_)5M zEgf_;_%6{wVCCEX?PxEt)!6JJOqBnBOMSUVn7(Ja-5Rj|IyRwvozz3GErrAdQD!)m z_}gL{_ z$tDobu8e_S6@dz_7rH+9`lf~GUqqadCP-{cC47L%$mT%Qn!PC!YK`+e^n7L2SA&e; zrF&PBo2?!-r7#Yh)J8zKQAH@-+=YZy-{b@=l0+x^J>c^kM%84&=8|@kY3W&fZ zg0B_DDBEQ6R?uyh#f??=Jchbsbo-K^pqb2L#_q4o2}ARzzEM68UwCT(XV1p>>A`nc z{|K=KMl8MDCx1H&9eGfxbKD)at5n%Y&$ldVe&JCoyuDk`1(lUn)fTTHm`KG#XX}&e zb5n}o#+T(5As>QTxo=H>3Dg32h?;k>D|odQdL*k%7ag9vIIJZVW&R6Rnm6*Ixh0d% zQfUcN<$oUSO+;zay`1`v!X_w18N;(HY!MOGRMMqF9pzj$f7cY{T<2H=`}~R7xh@Mi zhB~b5u*gE~ny(zUgqiVTp!uS&QfA5nT|V=dflj(f-n!{lU2SPpopXNZ+CApr`}v^KWRCtxe$;>!}K)>rA8Ez0~a+9 zg!17zc;W)={35Co{$-7Cf4`Qo36M1yP5wk0k)xU*PE)qxxaGwoi}Llw-?syeLX1Kp z48;pX#Xd8UI2Nw7uBsWPbzNBRDJ1gJF1J`q@3*h0NMw` zoZv5nXu+mrAmZ&mzDQySF~=8M2dJ?fKCc(9%pF9&#i*Q#%|r6WHPuM)vTp(&NMx&# za3XfQwf~4acMq)Wb6)fx2&E5L@bxYdj;G<&a_9Kk`BRMwNI7)5P=w#(zL?a$n1nvp z(}ej?_Hey%r1m-VJ-9`Po*0@fE_AfrvWx!YeiKUN5tnt2`u_i%I1%R86E|hb#)n?5 z@70vQ$Es?m&9m!UgnpRxFP&5`wJV2#ZYw-nCM|#bJTL29TkR45Z-y^Zl{a5kn=TC5 z{4ZtRTOS+vtu0X9vTfa5ed)QGX2lP&(jWP~hE5PLTS_`*CIYx;@U?m+6X{3L%sCg8s0eQiS(3|ax|9~u!njurlRKYdXwm6AYb9PH+KsC{_kcM|jK{-3oN0Yb<``kB&(97>~ zCRrDyujALk8J`gcic;x)hm;dVhb(+|tB!$`GOG?P5}ZL6mLE-xobP)x zU|e*^*19mCkd3AX&8{xbi+9qYHCJK&{kX*_BZJmwE5tr1aogN8cQh3cY;@fexb2K= zCR6Bh%in(Odkj8Cn?cWm^Fk)mKeLrQ##%Bzp*JgV#Tjw^m(H66`rh9$YS&L*aG4koy3`L0Sms=<=o?_3~bp^A_g$NC{ z-z2m$-(mpycT*!`b)MjnB@rGP>ApXcGg1U5Q@F|&uC!J4_t>ta9f1UDB$a&_MBhRv zrhzSdv;KYMVmO!KvD}>)&c)y>BHq#~JviF0cV|He z$@T-15QUVVPM`GVU0v{2inv)eZdx;E`u3jX+uP@B?(xc`MerwM*UkK;6)E#Wo}81u zQ_dDcBb?pLLlw&jaxo%Dp_mf4SfeGo_B1 zgNjYmr=6tfx3r3Nn{2*nuE`}!FR2wf4HNldo|5Nr755|3RHav8SUm7J?tF2JGrjlR81h1S`piXvF4~^awpOp$ zO{ZFu)L7PkkokZgbn&IN>M*~yy3hv6-wlPdH2t>cQ+Z%machg{)IooDdnAj3y@MI@Y$q));nvd4r*Q?_$I!Rb7CNL!jQTJU-uG5v4(iF{Z>) zMYnFD6PR?DS#!J!O+zk&g|X(smTv8I&a*Rt0Z5T-Ti8M%3}}ar*D>bx5zTna$iz7; z!LF7+1dYnU^ZerS6AVP0hK<8PfMjYm$9cd=_tBn4)8YJ0O%10)vteT3&do9o@UAX2 zW8hs?wl&*B@6wPYS(WktXo*4Vx9nY_nTO)pT*x}hm(`%o<Q$Rb$Cbi6;qZ>K(%gL=V@YOr``ue8!`)_0di`wZH6VtOD$gQHq zx5x6j6Vq^+@y*~q!y+7GqKtQM^Lh5`w!LYW8%*{|kI~>61HF!uQMbI#H(3sugqqP0 z1Qg_OcIjH#*vP}Wk%!17+7y|vww?W%V=lT-VjHViUL=y}$U~o=pM0?^8?QiiQct~k zCc*kWTREP2Rp3WK{6Bi6R7BE_*n=`BJr$p;+#yS0FORkP5w=-v&Zb-IBeX)(T+8HR zz!4r}uBH|K?^@6QuT?3c2beL@JqdfnZl$O6st1U~eDEHPPh$S-c3+tN%32Id2g*N} zp)boa-cW=t4f-j98yBuo2_5Wg;g0rQ(~-L9a&uUm=06%Xz~r2BGV8iQE?BlVs50U#6z~ zkRgGwRpB6LOt5V|yu2q!KSc-WMrn8Co1cF2*|vk`p1C^(l9&%A_fKmB`P~Xa>w!%U zOC(#^rsvn*kpeIE)3|N<= zw{0zZ$(66xiil^Ti?D$v#0K5VLdbjjW)8MiizxeRMKf-g{?i9PAJcVn8+pOt27w69 zfQ@c*qKg;-R6-i`-Iyan9X_# zk!uo9%`ie>%-1M8Ap)T9_=U)jvJW!6Amx*<&l%^2%0cRdG$IHVUCF*(X%WM_tND+z z3qebTKvy_VFA-<{9abG=5_%k~DSY;4{#EW`BBG@y4x- zE1E^q@6Qs_Df7!UpG`#MJkE;eC@9Vo_aX*)LSA~ka;STR-)<*X7SNZRSQX#`yFWphe@;jO9y`JlP}q7@RMN3L z_Dk|cjRz5Rhrh(GOlx>66iSy4jfjd#iIz{U+%`#Iv2^=$d~)hrL|Bf1U=~ICO@H%i zzkgEbvg}VLsa)W6fbT=lL`@7}4!7S2b|_WMp14mqQFcfATCO&|#%G`N-U#6F+t z$1?3l?tg8T3`0~b0N2Mc{g_;4oYpEpuU=>c$cdK&FT6S5dH<2G_sL*?hIAVS6t^zO z4YP_il=%Q!TmUVeXaP6NSBW19&Aje~zK&Fq2DAv(Q*AE#DWx|-?2`Uf7`45F9+4gZp?M`DZIl<|P$1XK0GEL%yWa5R zdV|!ix2CogJ`_^d-`r80at^L^2>=>kkf|(Qu*~tEA-Be{9Y**3uFg7TKKagB@UC$` zazXmfui&=TaF{VNi*g+dYwTJ;&{>Mb~B2%;lPB4s-nDW-J!CMCk3(iAAD*Y zVg)H7p%7L zVc5-e4loHQMF6u`3oU7@Pt5eb%I;@fl>2vXs?TKe62%~CopR;yGNlMlgi}cqfLy2j zHbbb*S(Wc+Mj~Z#EW>OU_G|ktwnxqN?E*KIzXh9jaAD{=4RzWUv7k4!5Ximu49s!p z!0UiHh*HmvzW=o_ClwWgc*X;=AZ6{ye*Gifh~%2;mH=Uk4R0ghhrT zH+LJ@7asv_{HjLUX%_~+EhEJ1JLsQ}`=fm-#=@>_D?4=^j%g_?2e@-^Se|DGyyM5x zKro$!dhlbdiV*c!)tMM!u7w+_AZOu2<4(_?ZxWv~D2Y6^Xmhl}Yug}Ub($l2# ztn7fFo|P_L+)J_=iYYpI-P^q5-F*G_GKyHa2dGCgpD&h+wc2dKT?RwXNUq!bH`SFU z!M`hs+E!=XkrK_{7J2)BSb*D9+%n{;nTq3lMtNmZyF2Hi*Yc1cK0`7thqS13$Qc)c zf7HLm?XCn5WfNZs1u08iK*Q9^-jt-|!MS}o-g|R;rBG%e)ypNu8*wdznZOL$eG7GxKBHRzh^NZr;wh{p{w#IpTLvJ#1Uo{9bb0B~VH#4{`dp5Z zk^GXmBA92-L2MMh`#msEKKu~)LpsS3kO1bMiH6DV!7cML;X%3I8FC+Mm{{Mcvvz+O z=rQ7uFF#wnq zG8;_)6=(SIVhLgbf}poXRzY7lgei!d`^O+}r>Q%~AowjfmbB zo%{^IZ9p2N@ILT43k_(@nZI~X^?@P(d0jp}8D&0B*6RvBn|;hoh)M@dVKrQ>Gk00I z$W=&US*B)h?xh`H_cZqz!ux_l_CIijKZi?AhmBo4sn8vLi3Jq6fnS79j27nD-B_+~ z=qEm2UsF~Q6t6(}j1o{AuLs{{UwsdUF{z$1&HgpRx0;J*`rcU2L&e~Ha{Fa9vTvh< zMVY>90Q8nUNXjcWt?NSv*u?Y4rpH(A_}&(8Htx#z zg6h$Nx;+WKA6Y6Dk5FI01|p#&&d-m2{MX1@fFxD`Y~i#ZiIy?1?dRmyb#1vc*%@nQ z2Wy1T4(s2g4|Xq6$Y1z;`w!*y^M;@Iw=4DA5`TltD%eEe73t<}yMl)9zM{#X;4Z9i zXC5d%qy6S^TNdt7G44@lug@FrYk)f0>0R4)D?|Z=zAV9Bi~f-50tU$%jj z%ZIb`{4m~d#y@F{GbzVL>ZGm{-HXgYPgz`5w*C^QWmvW6>xZ6;D9k^fx?N9K{`57@ zZJ6^ezFt;(Pugyb`|HPht25lsQj4#t5-bh*PsLyE*?;F8-&GF$E>*vnsYT})+E6f9 zDqt4Vfu|N8E>xf6+Ky*aYZcj!7h&!LISxGn{@7ra9}pj#`yP@VdCQjcB@vY7vXped zf?bZCOu1vB{tJF{Sp0Bb@MK#war=sGQ{BEBWx-6;WCr76u)elTYA^sYV)yGNb7CQ1^QBhR@w#2asEWnKzM zr$kVfPe!93g0TQ--z6hkw&)***p22!XAnk(3 ze9YmK*nPbGarZZ6+T`4)g4AJng|x$+k2kN$Kq*zE<$q~_`eG}hVxaEiQ z82_XO?L+zHbzYu*yLztYeoG4TDigiGf^5RoypfS{SKoHR0F;AqB$Qj~wGeb?<+f*eTDvoXw2FMqLEol0I2l?8sAg$(ZQ<*X z>bvtWd!Cw^$+>f9o}21lG*wIRuTxp=9v8st2x72;89jm-@;mn%8qCNVi4NLqnCY_< zK<(|*-JF^py1lZ;>j|=C<8I;Ob6_+1y|@4Cg#Q06leDE>1H57ZqH)Y8+@i|=A-zU> zqV3KT?rC+{ajmkqXT|xTs#GBlWEGEV(2OiW9Go4pMk{%+JV2;fu z-P6oWw%mC zqG-8>#nxlqY)D+s>3u=tvUOww2% zd;#(w3}AeFPu1z0~*jraYJWP?mxflF}4CbDIhrZ1+* zm(iN9@=P2{H%y|BlUqv}3PiWqbbkjLb}**??hE;7Vp=`c(>=y9RKXr^sgN1(qzRC& z!r5MT&h)_rW`>NpZm;fU4U2n09w$*LGtxgT$(Vnoi!z{aKUfx4I?8G$NHLe!Zus$x zgTKJeA#k8wm^pPB(#cpaM+HOC!14m-;5t|q`tDUuKn=cN92TMmsOPcn=U$cf&|VE- zYB!i;<1~&=9iJpiF5EWz<35l*_>S=(X_?+tioU*Ag&$4a$+!p)ig&CisAfkI)MmzL zmoy?uPK>-v4LDshA#lgp-exiOmYJ_I)=Gx0CvFfjxFzi2Mkc+9e~6N zz+K!At~gQ?+q)FrTD+esG(l@<)aO zUiv4_z?ZDTR8k?Z@o&TB7o#JL%NF>`B(uNdImZGA53`Z-Uk9dHnPO!9Gy2BQBVIPT z2w}7_Gu`C*u_A?Zp^IMlI9g2&EDLe+47HV_-YGA(`&c~YFH-ESEns!}^(EH0XZ@bg z;wRV=h*gl>y`^9{QN1&667h3)PRT|$P0@JE>@9}g@Q~Q~i63YSG^MKiMoRxi0wTtq z6_upR`>e+DN`Rp~3~`#U?8jWR+z31V9JT<&2oC{3AE5CG1`XUpK1_v6Q_|tcyE?mm zZVD;$)5%?jJi1nl?y-!`*3c$pz9~1p`tfP$NtIT`Kc7dsF&JJ-ZjKD?Fp1&>m^J6d z>AqE6031wRdW3!5i`PbIk)%`l*3hk27$-|wUo>K8_yjJP^pa2>CR zi;Cmq6T#?|U<(;G7uZ4?8lNa4J13*XBeeE3yr zg&H4U)v!?MgjoL-D#{QNHJV31CNqDfZ@uQ=Y{esxcO72f zC;9gZ{~df?{kI0*g29c*TxV2YglJ!qP!f)UM5DjW?liKs&+zkacoU54`ZH`5;%!w^ zoK+RhN)$LPE*vuMbboOU1jH>MQkaS1tgNm%O{xQ%o=Z<$uqxIy?jKSvzny63rr@1v zc4g3lmF3iqp+}is!P~3%$EseZMnZ0MOS-GMOB}8cU3n<9h+F{??vp0tbD2IS>`*3R+ z_Kxpn5o&~k%RWP0LL3&k0>!rD>o0x6Wyt?T$DkF ze_V>(^@?up^<5}JvC*){z(XvLa<}dR9%N=Q%5jSYV|ly9IlKSu48H%92v|(7`q;TQ zR{PoR((1+~Iw0%SzndufC7pe#B&HzpN+Qp>K#Pt<6*$Py=tXol$)E$Zft#17VP9~* zXUMx5T$2n;(SPSd)i1QzJ#7zb$04Yb4{I2YdAYBH+Bo!d=0Z5jb@mH~QUr&Vyx#Y- z##)Xv_ITSN|f8cETfnhs21kz4_nH9{xX@4cpzno6SybOr!Phiocsp`|$X7 zt9x(SU$gi>p0O0we{wfGQy2cWed{mK|ULTRr3zN51!YmTiMMr!1=(%YQR~ zJIj9q*Axf&!dsP6Amu}!u_4kUPs_ToF#7`xgOE>Vpf$B(3VYWtzSA8fQSgr3-YhdW zg)!OloWwUhUEG-eBBSSO-bMfILz$OH)5Es0!CVpra(6(81Z~VHxZ)9Pr?gPVxi&5v z<8l>}0Urv6ow-$p2iojxKaE{exCK=MazN&n$!9T^l`?4i<3@vt`Dr*POOWrH($G4Y z7vn>xhM8S(;-IgyQU$gfxc3?uiM&)5fu~v*(=*s*SNj2P>3-dI|9Y)6W)zqT#>~KD z=b5|_Bvs|gCBKlQ#&N3^ARQIww6Vz5tqiO++nfunozOYGCq&&-nplCD;j?zd>H7Fh z5La1y>>rvD!yVds^FIVs=^CVT&B0RysF1mJIgYmA$Y(NitUllg*sxewS*$SBln%SEd9G5aiRm>XG&^U$n)_O~4EOf~ z?)}T$9A6QQ*WW&2_7Z@A_@Br|RuE9)D&e&u2pTdtBL21CB8q8)c%>^;v(VX4ygzo~ zcoQe-y?PMIh?Q2Ye?G117INO!)a(d)HVmd|`7>LiD0;*js>Vi_hRQ2Aw4~f?POz>^ zvwknnJ-a{ysMUQSfGmaPdF3_FBY=J64>g@Fr*MsCLSA#^%vq*yiPwEw)1>R9`1y(9n ztAAhoBA$%$HkpISf9$qQg8HLMA4QeA9BnrrmN-}%ieE4UiWh?NPuv3nSdHz<$8A51 zyUW$~lCHrP1#v>jf~E)h{}7I;-0kPl%WP{U8-*Dc-;PCJpz* z=w>u;6UsFqTK-~t&!uLT@69Z%vuZub@i58Uv+$qNW2i$v&w?PE6A3gLA-9aV~F&;XRE->W= zrqXPGFYI2I7^6XJ=J;x8(1w3)#`&=8jcR{-G(s&kLa`wZI*%#$fkdRhH_D9<$vNyU z?ZpEzP5}QuvHBv<*}1UP=-gWaGaucY5mD=Hz;y1xaJmAtnp)x6+llonfA1r_eyPxw z2U_4G=i7b3w_A2cw~j@5o0{Mq z2FWZ<(SlgTe3!}oq{<~qk8SjZM`R2tOoDFITFJSvjoa;pq#cUx9-bqZbolNo-1&&G zyQ6$Z?bQ`RP$T1anR|5&X3vxqHAYNa9lN$W7T&wPcfvbXiIh3j#VW7kSY45S!-6xX z0;6+ty|96rPt8DC-g&sy#QF6{I8siLUP>99Ud@cEb!jj~#9uj+s%fcy`m98u2vtSp zFJlnn$W>dXL?!*=Zk*0nY>?8@Uz4xjpmaw9vL4--;t|zwfHmWna+ci-EH0iz>WO zRCsYleK&;V9TOD_Vz9%~(FyVKXg{`7GDk=tI6!f#&@qj_dJU`lVcb@ZSop78RjUt>321_h~PPR680 zq0mvNCl)BQ312jYQk!VwKZSnVjC60SL}GXdk=&PjGCk2x0RO~plj!0B>hK5Flx-T2 zoZ>o=k=g$>RTfgUl}kXKoc?W8h=X!{WT5^oPqf+k2KfjVSfcdxr6tpi9z^%F{3ljyM#V?D`T zbXFsCfQqB~gFy`zfYwWX?O+#Z>_SpXeR}XZYyPTR%BLRN@oJ&-;Is4x@_)%c%3Qd4 zn|>VwQ|U%J<0tCO}7UEDk7 zu*J<>71NaNw_uQBGs_oAl`k$_Ux3T!`(tvjvQoWxc6+0CPZK(EE2 zvBVd6#fEHRl)eWFM%A%**0%56X5selj)8zU--ocvv$4Mm356;Zl;{=o&F`MXXxGnL zAL?u^L5iXlWiAF(|{Re$kZfTR|(8c)JcNZ1#eEXZfL(*BkfY z=WmF|ZJ)5ihOvkJ1AEpo8K%NPRlaf=8NR|Id77@*&iTYKEFQ+%7;RRBw0Hpj$KGxA7HlySX-bpkIVkl2Lkvq9P4S=fb2=Vx0K z98MV&b3$t1MoLr zKYwS%I!T;=M>_DXLFWDP@d2Yx<4XsTaZD$>9mMT6w>>SyXYg8t5|}4-SeD&9H>H~! zBhod*MB4~+tGGXedi3*?kOw)<^bwrNiQ#{Vy8WAwZfS~NM19jQj7+F_LLbb%3^bNR7+K3CiYJV=G{QTO z+8vbGs^lWT*l^H(TwlZjxUwn7liUDi5BSOxm!Vmy5sY+c;jfkdsz{xaX@za=0Zk+p z8o;M~!q2$4kq#AQ4h0I3po#nI%^tXsqa6eXR({zCcxSweuoEYM!EKkEoORj{$ALhy zsQW+|$ad%g4c8PQXDC49SWsW|?V^j{W~TfBMoKFERumctRaifrxXeTd(kIBqt*A## z9&HyIu4uHr@nJ^DjLBGxJuJN)P;C3L*zT`711s$2U9RE9PR-4GrYE$e%@VL&*=fjW(e~BFZyG+Ny-nEMOj=*>g;@CF zo7n$h0nl4vOS&3=)pks`IL+?&o0-?W#`}>4WR@AvVKYY&aLd}y|KKpW^~of^Bq-$B zNgqxWXS;R9_E);CH)ce5Xz3&g&>xsBavq81;&t*>ve+ZDS)<%n_Nr!)!IFnmEfVfX zI;a*)JCbPeC`_Vn7#WCiFrJl0^qia{q*Xdp-Rud}f zEBdLx;#f$V!>p{oCLkdo(y%hx14}|13qSexNW>})i6FqBG)AY3jbX0jWy7p=v-(obPmw|7n?_R zOXwBBn>9$Kh~sXl8VUIy=IQ>9FzoR*gg?tl-hsz|-6x?OvEu=D$oUJiOnL?(hj2>6&sOf#dC%tBY|bgt0-{ zYd3GkMCyreq4f8cgNVB9?650P?dMm%$J#py?8mCkKd>R79!U+U1Ox_!NpqJ@TF$7P zZ%9^o%|kahN~1_rluoY$UwsZAZfC2!0E~KozP-Kv1n-Ng_^wkMro0nd>%cnXeLly0 zFJ^HX_ znpB<^;gw$+U|{|DAmKS$@m?Y>YcL~YtVI>~2HQH~m_(Xr-5n_j82zug*7Fnp>4OTG z)miPrZ?}ete9(Kjcn9FzkIK`4#D8Vm^8dxSXOqrJCN!oLz?1JWclf6Nx^OOayg?@aj>4h^W=gVRw)G zT7ch5Sm3B=-&~$D|4yDiTbaz?0jUkVrs7kZwHxJ;7Of|x>f+_DYF2_i9Jd=<_@-xG zW^#&xTMq8MqHDXq6AEa^5Y!jF)x?{imxrf`Ynta$& zIb0?FZMHBzIO(VV?ghR}9&BbBtCt*neH6Va2WEO$cQ47fqMMt49eXryj#jF51UAPx zaW^?ur~t2|L|0cc=X4l91)+VJ1AZgD8M$#)-xE zFMJI&WrlT$Nr(ZwoKxVpZFdMHMRd=?jqx}9O^(+mL$6%4Ay4A&cXF>9Y-bw@GZ7vb zjt@#UKi)?|x1>{aUteWZDX{i}^a@=mkB=&SAQqiI;x1M6;2`6Il;S^Gc9L*?@#K0J zlxka93=`hh40O4j(b6qpEj`=IEYinR8o#dp{iXGLDE;z#p5Q{KQ*@%Zq@z_=R=ra8 z6_J6Nsey%Ov6y`zPQ-$lE>7*@9Xu#&_~c;VYt5aVCDmFDJO3rR;od3RVJ2Na(6FJ7 zDEVHIWlHw2#I@!@2Y!*sKdi;3JZ zYh3OBCo-2x-`S`lF2;i>uaR3r zJFX2NenjQ}#ol{|HI=UI;(#DVL>O>D0VyLzM`;#11aP*9s9>cxQHqgHKnNrtAkHYD zbfp9b5s?yF=p+;crPl-q5ReieKp+r85|W(dKHokwv(ImS-<*B<{WpK)x&nliwcd3< z_j^D0`@GLO&MH1rcf8?7&da(GNjipQm!@rguUb4F}kSWEtL-&B38!bz(J^-!t5uX(^({u~41Pa#&T#w1v*tkBk6UFU$m{>gIVCJit4DZ$BFS z+zsa}lpEeuZ3}13c2Nm(&LeKrHY04KL-hLf>y|>*-4IN~?%T)C$}W@*a)@In+G)z? z!3gbzXyuexq0lP72R9L;qmqvI0EvSC|0OB%<|xO#yq8epVg)es-z zk7E{&bvEaF+6}J`!jG&vb7hj?3kHLa2mwZ)b!V3<=hF>01bFlr{oGRLThredSs5<=0^+8aT3%C27^kszNL zC40r;X`nyy=)jYs0=Xt3vOTXxi)S9&(=w);isR-}^7CD-oKA^-O0PeY zlaggkV4R!r2Ps}|o@igVAlb{Dz81Q^=x=C-pKhU{rA)OK{U;M8)t%lcp9rB$mC_2b z>jDF5jn2_dg5>QUg4(Y7W{Uq-pTA@FqyQo`v~Z%>H3;7oFO7#-&8Ci6%u|=|3r9|> zeyZR$I5@*=k$z)wG`peL#ipVT{>hQt?!L7&du*r_>Md`JW-ucGr)$)F+ZT7e~L4@xIIDCEVr8RbgCR!2G3F z4R!o1y2!dK9%3yR6^Rd{9Icnko(pWh)|h@x<-a4Ezd}Y|KNFSwgZwV#P4|Z*lnxE# z+jb%}v4&FJDbk0(Z)1}Xs;Kw?&T+7{%{YngRUbCSLIAR9vnJ0|EQz?JvPMEt*ZlVK zbz!rHDg)aw6(gitr&hJ5unKa$3C6aQ6(?eaDylrR{+H`FV>!1$CIG{{C zoShs?XZ*d~prNn3Kh8T8&iD+4Z=G+TLiysI8XwwrZKkV3Tt-Cba%plD@VgmD(49+V z*G%)p5`tI@Jpn9Sywq+=?weAF6HlK$ZF4D(v4}Uidj9x8vNq+u&ZLMst}{BF(n)Qt zt1tjt<6Ur{;)R}`>sN!+mVR!LuiOSYIr7!!xP112%r|&A*4}-zLmlr9O#q>aSktX{ z94oz#8o`^c5$eFmDp zckQ0KvL9tEGrq!TAIE~n#T0}+T-;T3(vioJa4gZesR*_;(?z3h^;`(e7R82S&W|wN zh0i{4&9cQljTY^a5r6a2Fr}it!V0HdeczTa@!lpbt?ak7xL}GY}y4xy&M9v#GVK-eT=!TT8#Y!CCUZ!Q5SS_@LT^=3XT*;GE?<&Hh zy3i-BJ4a5YL(FaLwtGsX4hA>v#VQ4z>frCpZf6f>r#CqE$Cg1~hl2W^UNh~PYBXOT zC#)G%;IhrclF~#!_LRi%_z3Um`jY9=$X}|oqNX$hMLX15kzvd&TjD++8W_D3Wb#SRe zode!{*=L^d)cV{_O(k{$>5_E^NNDjKlA%+m7 zGyEF(&$Z{$R7uIhw@|i$fl9n6ko#rZqx_tE23`;^e?OlGlAod?xVyymSHa>q`pVeT zWo&u5t^@dG8>FuKu93C+NN`clgoFAnN*|1ao?Y*=vv5Q?HI;5^9C#xGIFI|*46W1B z(%(ZmV}(d@z`ccl=DN?72n4?Mec8s3FhX!D3}73-u5CUXDf<*64fUyz5L3)B{!lz? zqpGf&790Cf&9CC|Rm}1u?q1k2Xi!VRjXF<1qPl{2k$<_Ha;8>c%bBPa>fFQ2rdX`d?cYK zFwjbe`k>bi?-hQMglOCZPZvf+NSSh_MhNjGz{%$|I#5hy#DA)>Ixp3LhQa+Y#ixSF zClWb&i+qh@!`7W$w0P=imWwy=_(`=VMZ;lsR?awEIL>qr(c3#!(pMWI>W-rTBE-|F zm7nN+7oj+fA{-0#W(K$xl5b3^M8gl_hvai9JubrTs-^k8CdRI<+zPzd#fQ4CnNK&{ z&>6a2IS*DnP)5##MP^O)H3S|PHr@qkIIV3n>)7vF%$&&@%j^^P*{LSbkJwvJS(8$C z+JSCB+lAZS_gLHNj( zT!%}KhQ%_{+**6*wlFA!3+RZi-^F@c&t?So2 z7NNgl-RqyD>G#O&+rrKE2)sbnLT&YSX|L;}E#?fuONFMUrizPfkdmqa6y2^Eus-D6 z;e08A4a}2WnI_HlN6GGzzhvZgNcq$TVD|ECw{e#lcdg7YmnXYG@9dpQUq{IasJ5S} zUV8QYMQ*5bm$(bTf~`2 z{*X}~5{CFvsG|{(BQhIYl`uS&eE`vpQ-_At3+;9@mOF=|7l_eqvykyg~ zees#Rs~A6byrB-Uz~LDK8U~4njz+* zyDz^Rp9iTZrM2i?v2m8Hpkp2!G;OI+zLUf7eVqbYg(Mai^8cecsri%CGwpXLqY$b) z>aW9W3qYT{;bW>Ym9auNnoS2jH9Y&=%d)1FSpMtPWxf19xb#DACCAyb?egNw3F%T=_k{z zYXpADZf`nWPXvb(1zv4O^N43`E3Bi0jEfXJL0lMkA6FmW6Uf&iW@CMRR6@ci2Gi3c z+%_QgYnptG&!zfJ&sLJ1jUC_6ID7&!IgCM1qouCv?EBHd^6=#CnnFQ^lBJ=+Twjn! zrseS&`MW6MRVH450Db)^6U6Dt5S&4FA6n0?Gftu>~ z7Q(k>Vou@8>YR97$Zqf{)78hM?698YM?&Jw_*m;wJf>O$6T=hi8Z`Ovj5?^=-od6g za74s)qN{<+k_g5#=#LmvSh-%0@oum8^SAeO`1rLV>l@t)$&Q-pQ{D9+@_^=PQ{&>S9qEB-XfyHh(O2=p^2c3=}8`MK5 z7mDlYk3dELFqIHgNMLPjJN(tm{J#&PgsVXAJ}~w^R3wUDLg{94?DG7Q;|_5;Lq5yo z`RzxsfFeiqj_zqPv zDboPe-zRC|H}7!iNNL{|BX7Eio?kQ@Q6m8qR4ax;nxU6vF6Zl#@$c`UU}C&yvlB&ZFS2|SWLzfIK0LfKZKcc~*q z)WwY<>dxn6>iR%3l`R5Jqfpp3iJ-0eha0AK4`)nmn;w==)I}6cG(}X(Ryx}~Szv4u z+D@AhbU-jyY{aN++UQ4@>c|19UtYOXnpimsms*UD-Imd zv|hl(#CWmy-9}xy`iM&#KS|XmaJg4JTC|yp*}F5M@!7=F8FAIl#W{y4a9CMXbH+b& z-a+{K$|Zb)a?zQOrH9PgQz_Aq`_Vto zYl^-8arvxHA8z_%l!eeD1X4@HKdU3#>zM{rP(n@Ltwi+@w&o%-NB1x}zQhx_b?MIZS4FL|`S}ndOBzJX|v- z5UX^dsH8yZ7oB}(-A2opHRdQ$97K?Le6s!RUU#UIt@NVGWCXT18IUN^j zD(i;FU7dA0A~@d4xq!g)jV)t56?~z-LHP*?(cy7Z9($|Fer1T)4l`y@*zU-wF{zB( zba!jz&}TyDk3Oy+nQmCs|8={tbim1K;v%fA95>zP6glNGfks9@Z>_6icyGLx>s6SX zX12t6UuHwc8XL%-+t(#ct#x4%Y*a`^v4)pDw)MBHEY%I$He#5~1GcfB3)zI58pThTzlE%-K?o zT5(y){943MLKH&J{CQ%%QFZ;ePIU=s>kL_$j4D}ufmy{*ZG!%iq54h2&7Z8Jo^nRRohv>?CXZc8&uwIoP`? z%h~n>n6=zRrCDN6fmuuG^Mm;o^R0~3fSFxJMOD?=0|>I9#1*MV=%@4g-;D_G`CNYr z^Rt|wE!OeLElKy0`EB%*p3<@CVcmBDqZIZt*DozscRs)~zW6y)+Jai$gTu$K&LH}7 z4AtrIM(_bPYon!&I?EPR>R3rv$L4GiWrTUba2sF>j21_6?ZI z4trFtJ?rS>7kYm&)c=C9t)mBwv6)BF!ZESKdovXOC?Ctb_$KCa zVvXppQUPa=t2;NT5xMJb)@y8=snU{E8k$y7jd4m#PwSLK5>@}1m2HeoO8KOTR?g?o zSCo}$DBn?wa!!6{IY=0yXTf)z5gV&Yr+rp^q}f-6JQcmmh`mlTh~%5L^!|&anh^qK z$8bkas@`%A4+PU*l5nr3F16V%aR+1X|WygCxeaK=jSY5*O0 zk+ypR7=!s0^1cus2U)=YKH4rd(965QxUalFfMF`kwIENa~$l>^ip1=9r+Z zwbS|ke!1YUpd0v1a{*`{mvt75GE$5(Z%uwY<`hc3*xzeXzi~}t?2Y-|Xwntx7JG}_ zwo|Xz+E$7HM|=)s0Xwl-F>Iv|D;vokw2Xdi2nE;0d?Q$F_ODt1We=X?hl3>%wIi01w zN4=m`IX~;6&0vBhfmN%M?NvxE<#Sm5iZkpw2XE3_PO^r&{4Bk2LDg<`^; zIS|o&4}&bEeM60SP#$noc|4s0YWeg9P8sd$4cvdGjSQ<8tU>t;n>~RlNv}` z=$vr)(2HKF-6QC`T0j{JU%N^U8Jy0C^Gy4#!S;+6=2Xi)hUGQ6-_;b!%T;!$loJ%Y zT1_$vUUbq9uksCBikjWtsGY_(J<=1I4fi_)aSsfvY=qTZqkm#H(5PSLi>O~d0u0(%na=LWUD0odmf#N z{$QtLd_0NE4*5>sOsvN#0QW)T(8z{4pLiTsIOn0w3}Pt=?59qoNQ~kr9`Tjr*o+9V z=IM_ry1OWWL*9S^5Fg)DQC@ycOG`V<*%xQ^AvHDiE9`Ekr3-5QOi9Mu$YtZ>M4Q_* z^g+ITw)yt8Xsj+UJx{89d9Vw0WIT8f9Q>MYR z_k#5kJ9rw!5D7#KMJB~}QSST^T(8L8uw1JBFa z_FvXNA<;l{tBI`YU1%!`(WifXO6j7(st1>H@}{rV1{~K6UnY91ZQtlygnQExaOFt< zm4~Gh0<0U$6jW1U+kp_LkmTyntQiEX38gDc+8d$x7s&iC(T*REKZcu)K4O?ZdnA47 zjMVyY?3;*-N&3%Lnj{);k0}@WWf*9U4bjgWon@XjPv0JDha)rle}y++!w@GXPGx4= z-wTBJ0q;OsU2a}p<)_rt((*rkbOjp!X!SX*J0r{!#@B#nsuL#YKaLkAaB@av?M6zn zW|+D)SQ>K!p7nsZx>cXZhpwirPMF#II^>SR$)glI@Ox6tG)e}K!*`93huJziqgeMt zL+ehdtEbNA=Z}Yj6(0<`l;I5a-Lvft+c$l9v8DN-9_IY3dcz{G7H4T`>3eR0m2(-> zs8~j9McH%1O;_w?v3=DZ(H<+Hx|9u?4Vm@GhNviWKe{W8+#%QtR{ow5j zUO(|0Ip$)muM^_>yDd@n!r$)3f0en-pTz-Zj=Jzu;=U|9-WEhb@N!W2B|ZvkwUN7? zJ&iqaCDQ%dX|=CksJS2TkS>H@5!Wff4FEpw_v?kzzFu&`joimvn(UX; ziGf;GSH9_3zC9_w>svo%ckEjc`tOeub>xsJM>{&%=hL?Z`hR)Z8je7=Fhs*iecRCe zuPf-z1weKrE9kuP?=R_FeO1&F5IUKqHMnmI(|?)#E_(%HxINQvq)&gV^ZmndE?Wa0 zW>6FQ!Z(HDzeeKBaR8EgRVJT*yWIVEQpBYmh^)8dl5^iQUjGG=3=;s7XLObxU;ZB% znxXH3cyjbD`R;cY^v%Ag`M_65wwP<*{ht|{QXrm|!XWYg0_it9@vjGTw*nw};A*|X zx3ebybx*%ebU{EoUBTN!{%3eB`YYM{4GO>f9~qjzDgMup_BX}<4s-rp4e$MN{h zM*f@P|16wDGybOdKbbl;{-*dpD9GQ!N&asY|0kF+{#(WW8D@Hn)1;NOAtw`ue5Bl0f~ zl>cwj=FhUszfGHeh1UNCp1)0-zfGGzRlk2aG=G~me==r&n>K%jnLjgabOQM(L@dfp zFJx^3(;nQ9UoCoZW@oN;70*0y`mos7bY3Pol1$fZfdrfJLI%J5UljJIit3L7eflg= ztEhjaG9UGCzvtgq6`7j1{EJJ2NJX;Err!J1cmJ#h^W-Zv!?l`=mnmD0KU);}Q)A)J zW}W{!ujH>wWH&oE|Ia-Azu!a+ir9^v2)c*TsGn@L8;8)}yWM{Kt8M%lvh>EQ)bCxh zfV0fNG&|wp_`IdQ&&4&KS9+QIHQ;{&ygw!Q|2m5Tdtk9^DF@u7^2bl!`2MhPp0sd2 zK^5pSotrj*2UVVI11kJ|W=+LjUV)zUdXAzSa(1x1L0vDkJm+vzq)V$@+PGzXwbA6Jyq?G&j}?ddQ<} z(v&Y`c6(Hc495O>7#gZL!|r6<+{8aEIiPCY=|Y|8O$-R z_kD-CtTm#KFsl;!Hle_GcB*SNC|jqP7I+vEfP&bc%}z}4xZ;)iZ};;r25htK*HR>$ z`_{8~vmN4*Osh79-=I2ubKi;trv{C_MP&^nMa2vGNC_873t#z6XCu*+5TR(@;@pg6 zqazWs3;I$%XL>1b5+S!)H8;v^HxE1FRiN3HVhJglpo_0YQsL|VHGxx}!nnbfD_~kG zd-FB|&ctu~{@np-IW<~tZmSe!KzH)8^cl;%aw6O3PpVxFVL}=+~({1E1WXs8I z3-xy@-e=S#2$? z$M=9r**Q9UP{hrAr}OGPSJ&5HOBQ8V_uz2&3XjM9=JY$?XZAne2)FYhk{0~~1cC`} zY$xQ_Ir!~%{{ZXG2ip%&Q0O52qiq)2AuvF7*%>iTuX~&56gQ6kV+imoJr?C00oEn6x!EL}ysb-HsUgD9tMtDd+y55!-D$vekG-OZ>yHP! zO~QR%k%`Z0@Z^Knxj%m#)cpNu%0x9gwIIAsM}d8(8Vl!XWc)p#NY>^;8xL#dROqVb zJ7=K-m*l&s)Bsys+rs>a?AgJ=L9=6%eI6~xT=Xh_?1=DLWmPwS)G7F&tOQ>rUps@0 zO?*&qRQSbwdtBw5#z#qHe);7WS8g=y+c7gv@C|*|;667#bV-QoO{|%o8X#Mh{$te_ zU3$Jl8>x~%Z&`QsnLQZ9hEQ|^A5c=KFcJ26NARJ#rWykTCtR3IAY&EIRyLXPsFaZ2 zbVtGeU%>E(J5T*ume#2IvQGBs*;-dHiqIG6;lp;>v{E+Xv z{}3jRps&=xJGI~LxKxcE-u|wq&TS>ngi`nlM5$@yTAx9 z%}RY{=%M_)D3MoI-% zN2hKiI2{|O`D{rINJ`U*2dMS@9Q5>3qg*HJ&21`k8>9L9leW*q;k4cG0?0sZ;I)SK zy6T=OJ*_*xkEj0!Kn{rlwL-%8yjM>2V_%{oCVhVKB_>YsATQAakL@nThrxn7#ea`k zjbnW5zPli0izh@TGfb78i{$6LsanTs`%Vq_V`Vlc;^X57O}XP9^)j0oNTgg^3^#wy z1GH^n+|}86Nfm@5r%QOIyE&avJHAjQ_{-`c4t+KeoR-3(Q_{^6Wk*DtZYup@bEQIK=*z}V&y z>jdu}v{eDn%FWPd%~wyw%YhkSz{hqet4^!PVe-CQvPG!Qli^ki8(X_8==z~^vm3J= z^D}L6G^vy%ioN-|^CQo|o?hDG+GImpQt0@J#?_UjBiF<~WzmU32^jSiY#b!2UpJuv zhbFtRe0f$grm@>bXI~>i6NfMLo&e=v?ssl|fuJTM4_Hh=wo(6J+h%T!rv+MKiKX5+ zqyJP+3Ig?9Ay&F5{h%n0aD=Mjudx@ZGQ(Kf`__Ugr(TDwq*#w)2ezvO?_^O6`y+k> zg347F2MxX3u)xZ3j!trBW1E;-yu ziOf{T?EETgwr88*VS)rDdI(iBKIPnxdYQ+{#|l!RLi%0hhRm6;p~IhRw<~H0#QOoI z7k4~*d3Cqt#tKyXD_05rE!jhdU#GQm#4QWH!Ql6seqqIFvl55toRhFR zfs6D+)!|C6tQxFtb=85+OyrDvfql9AtR8XgciqIn8}3u`i(cX$ydeu>fL z%&FM@B8h#cL{TRoEDsxoR6*=ZAt%`M8ipf8A4PU1#JrHD|2X^~-qffeprV$t)jRo; z94f?xXmhJGR|AHee9F3dqUu^ok4~O78WYo5H94FdKUn#@_|tjPT-zrEa=BD?>5?f2 z!&+;QdY3fIX3@L4($&d*f!%@Pi%Cg+etC)TQ3vp+eC}AOK#QWWlCw&2ek^FR9$NHx zAKLi!Q%UuM=Le`01{d}mu(WTU^lZLDM!-2e+Qby_?u-a6L$sM+4LzOD&qkMF5j$~+ z6s^U&dpV2Z(4o{Uky6?i?`_rhnkK}`TF(eo>U8z%x65Gawq__f^D=1mx1Kg zr8%>gtUKO2c5YT z;O2PAN8&+8@UhUxI3J_#o;J4h&-U+r;<|5E!jso)r8Q>juu7ubZVM7iO3p^-WID!LK;Res~ST$-=@W!nXmi@wsgSwEr(Ph5r9ij-7!25 zLDB;nfq7r};icM3kL<;cr%>n9opBUvYio$HOAg+NK+^R_ZYT?LEf%i}aV(~<2ywfv z?GP1wpPw%-6*zovvL+&xof3iL>`jVD7cIG6(N2*4WVFc5&V#PL!!v+lc!s^HX;#4H z=QFb%iCuYSiOZU94`wcRb#shRh&9hab4s>sl*rsn3HsvtWQUxme*Xf(4O^(Nc;6{@e{!?RZw$gzbSAI}!rA zVFSZsT#8tMzY*;}EzgWv0yd>&V(KIW`BWp-wDD-^6}u-t`6yC|a|E+4VJKT#pbrTK zHx~MOd3hZ+c!4K9#s=RA^Lg9X(-M01lFv9qJ6~?=!pwklx(F;F@jYjvDnt5+xYlzw z;=Qvr<>0q@%YCj5g^9ltR2zJ=t#;IIiN(}D%debbd6HrDS{2wIT!d7h>J|xTp_`Lq za<_YKsTZ60c;wH(S5W4RiHfS8ZRA_gsMz z&wbQ7))&Kg`$lmW6*~#&gFY~~ajzP!jf>$h$==APvjeBDKK<-9iOoa4WFhdM#x4DB zUs^aFWjf}2V$8C}mnfSSqoDNWIK-f1@=;wv=luegg561FgxM!ex1F^H%Hw=FPI^;j zb8kva$O5YxRoTIUajI6w@=k-hXbD2e-8r9_zESodZEt*boQRwctHGt0q1%@~7uJuv zo=}HdIdnTT?TSd?17J(td?WCH*{lXJCeZNOe#hCgKS1?<3qm(xEfD{@EvtMMRfKhC z24)&&vk8}44K|?(bv}xx)bY_WVAe|I4D<(x=az4Oo_7^Jyl%USe!gz6(k?1h0%)w* z>~LP3iNAy)p4e3@M2+_NA3%+7U))Qgj3&A)N8%cT`=zGEc zP)J{`_?l)K=zBE!)_tytWzT11l7UWj*DQQ$rJMX~CeI=>!vdpSPNu6l{#aC0gjU>F zW~d%sSb%MmS9eSt7qTZ^2*Lo3cs2N)-x3NU5(W6zdlwsB>6+G2Wm!Y8`w z^TLRQPKce0ix9$>_fDZUQbb%PEy;|PmDZ;WGKDC$SpJ`R{Jfi;=;a*nS~;Fn&lZy9 zoD2pSM+QJrj4HY;^Q&Cs=b{H4#CCy=JnJOSB&m)FcORG%Sug4*EPK_rivp)N*qx4J zBY0lRrKF=LTX}pZ>(Q$enG$vHRm;H|wvk7Te3?W-3U>1D0bR4IB+cq37$E$qPn~yv z-36wb;tqbrdEbL4^0J?8^6ih2g(Kw9z{7yz>5K=7`53!;^KwX<8+5ga^@ zuPar0t`4geSXBB+T?rrWAatD?vfGI4I`@MT#RJePL^=WEgKz3cltUZ>Na~Uq4IyvVe}zN=PeP;8%?)sBFT*%J)!St`ebbE zi-qyDIP7b=$@&{-{xdNeRjCN*mQy_oCyrP&@TzIYlHOuD@w4o8EL_96VN6?lcY(yt z3=eE`@mc5+4hQkAfW?#t#xLf6`1n+k92nr>;83i+uO!-4aTnr#r z5brwp3%8;fVv&QFj4TwJ^CNnf1$mWSzEVU#Hf1>ddFr;U29iMClu*ksei!c4t%=0j zSzT`ZAa-boFIVg#-qgX)KJQV1!md}+I+lO@vKU7ac+R(3Lt^mzhzXA!4)Juw8kdaa zKy1W*88+1xFMSyb*-V#j=2uYS*U0CF8m-pwyveg8He{d70V6Q04lj(`ZUg;982UX} zHUTSa!EsNRgUko=8Pkkh2`2?JX!$$i@TG^ zwc)Ud+@@V79$V^~1c={J(?&b}Rz*5E6CkX6M!=_3c{K1OfMo)23vt9bHC$aCZ`7z) z&s;suSv{0O1++Sju2Bb{vwNOI4+;h2OT|GuD_|gsVAMHtVdNG0fYN-`V!aNBIJ$1?pg3ZG(eC?g=K0^?^V|CBU{2Wx8Ab)q!V*Bi7i&KZ6764*CdS2+^ zCt(a*G%g&O`N-Zpb=AFIf)m1K(xmne_)K?cYOl>dZcgE!ns0BtotBpNxbJAwH62>w z)KB2Gg(m+#LSGE!`*rQGSt@)v!PX$t>(O2+G@<-S=w_mQpt+}+kTrlf|Is2O&~AhD zyDRYv=gV_dT|IL}=$eIjAfTLxzot+cC%JYu(mNV_P1$;bnp)Mu;u<`l{o zX-R$gCN>8RNat1{m#Q*QL4G$xgzvywUEx4Jfm<(#;m(JsHt&}4y50Alr<`Kv_7mA_A?J40J61L%c3wWuN3ijT5?*sIRh@gi zEhix16Wn_SQBuMQy)%gYSml3(nW(ig=hu&yL$l#5{m%w++f$>EgX0vlXjy`;0y#gn zc_CxE6oA>LBY2lW0&_!!$1jJM98^v?!2J8>eS($r^>*B-XX4DtuS>LM@*Q%^|ju#duz1uG!f-I;sLz z%{wLS4O*LWzztW^?_`15(f7DdpM{l+&!?_`yDuA^Kv*b{NM;dv*2Dx}S|U zQz`ct@6!@R<73XG!wnNF&yg2lmMgYXV!;Ui?*BT*)OF|)b zX5w7UCGDm!8J(PH46TG$cWNRP#z{Z(^DUgg)%Mx4MXSPZe)uJh|9?dD9Y5b?9olv2pl^FlSn39 z-E1Us5(6P*qmEtGSptM-2$cp2eLSFJ!+UIi%IyJ@a|06|QXCk1C8mgH#r z99V*I(y|eYD%`RwoJ`XqZUY5VL`vQ50dphdDHfYK<*DJsQeD=Q0_1I;d&$T$Ur}BOnau&id^z{5{Y%Dt^rWBJH}8%1RC@MP;;J=egv;$J zdT&jaQn1?dxI5L`kiyJP5H62ID=wJFu_WfcK1u(atnK z|1W&U9~;VgH&@im&XCew?vNi}@V<~qv>it=7+2P_uCO{b^mdzscSWxB-vLh}ikL=x z)3=vu-^8+~P)%!Z>uFI{goK$vQt%!fX);cdZL%j9A<=tcBK?e9BuDeH%|hBbmPNrL z22cWrkN#9!K4~EpP}(dr|4d+qSQkiAZbpE(=g;(dL~?9(C^K2SHuii;TP6M&wWzua zUZiS>BkoQ{yEZfFU`|g|>3}`GxFfb0T3p`ym6L zlT||YmsLGeh(k*}$eVqyi@E7=jS0YyTE!QUPF zdBoX9JBC}X%)@nyck$WNbX2ZrJPmxm&G0T?kykqS$DhKdIiFgI7WBtkQJVqHh(qvq z7kA`%jAsxN9{ahKlM@IgSF+eUb4?&H;;7quZ$f(OfgYOLSFB(-D}tvFEVRCjsC$3dS)uQFmZb+<4fi zl5wY&MwSfb3eRD%K5m|lYvJ-N5-8D;+=1!==U@VYRpV^@271FSbEB*B=X}Wgx!b&c zIIpt(QVR?o(h-Yw@b+T2z*oq^rlSOve_6-MBtxPyy~Xbgs+f8I4C2W;zN1jy)+&Sc z&Qj$G`jyB0OJlp7)WscVrg;wR)RJy>gjZH|RE;X*t`p?@Q@&eESB%i{I5(NYq^awd z$GRLQ_zw|=akog}gyvtxf>ZGuaV7>R0M}AK@yfP)I2I(xfsI&$fqjPB8wPzP7e-ga zCLlMsX+h5Of(-+RrOE~d$F&M3LvZNuRo)?L*`Zk7@Dgal&)~5f_7>hdd^psQDdK|R z=fF!P zVi!##ln+xGz(fV($bODA@@$ab?|SHEzYZVwT{6YKrWu?x#cS)QdJ5$shKy*XUd+wy zKY!|TEMuyAkMeFJ{D#=J+29Wd2ybw%p+G#q?ziXRrGLD@iJkqYq9R7)6tj*##6wU` zT9U@$baSJmv%w!k1sYBx$W^0Pxa~%7 z_6qVQjddONAq!=WH!SHLGqwaOXod<@_8X~XIqwj~Ee$mLP;MOwJRSvzFxX>h@@6isQMu{SmeAAY{#J#iM7iNThNSgaCm5|t57 z+UnM5=nXl8lX9$z{hT;C|EqoD#xuJ|cI5nQa9`m)hpPUWHhd;z&05^%%EY=E)CTgd zgBh^;nKK}HN0i&wECSl#?j!Xfk-U+k-*L=j0wQL)nzd^GKoT!B+|qO%3ZWCY>EYyD zf?r9*BE9I_QSw)iH`BlJGDiJaXf)03)}3>oLyecxrV)y>FEq^Vu+-rjPeFcr*7bQ0 zDzm8(N!_Z74C;}j6`pyJWYs}2p%aRG_M_X9a_{rW%RjyRK&7ycoV@E%p_#^Sy8PNo z1<&j7BwrQOFVC>r$uVE&<6+q6DzvFgCz1zcOU+;VHoAZL^?cI_laLrL=(M745@~CI zpA1{8^Wgh6N_CgAH}kUh)jv)^wR;89wuM*BN1rMd%Zv^dp)mD(xZ-yb?xBtq&vRH5 zBu|R%XeG$dweO4D(aRPt2Ve&Kp}W9}Ri+S3d)gE*Sr>vfzI;5wlL0fJ$FCga&Et*Y z+aG)M^_rXXh5oLj;&6BIn_h&OQ3){}Q`|vmPK`6$F~3Xs;*^uZnWFH_;1v%fj`87r zwYtfK&Yp*i`E-wUIRT;srz2DVzS0PArGxYgqHx0rO28qkmo8@Ns&5D$jd?6~o;nd7 zn9Z-K8N#07=QVe;uO0}z!K!&lD*7_~aPw^y-ODNv_>`kbP_gxq-w%m|~8RHxn}H~N`1Tz(p-^>yOevnx27Mz^UszKd+#>;q%> z9Giy^)6;O(Q2HZ6X%u0H;A2#2PaN+-v&R0wq2J<%YU|u}>?(R$CFFIp-VQPnB>oY+ zGadc2=?^-eR}aYt8B_Rq5dizja~|^gOcORBX?>KMhc1Ti(0U~sH3&?oO$u^9LvV3Ku%dn2=swE4Q+X}MKrzs z8hR(R*kbYK$r%LSKJg5NT}urGc7ajSCJ+~j{-B_;6O2H~(!s>rf7B5_`bF+nwCQC% zJjD{-zOf=)#*^NgrF(_|=4>SDTLl_*zy)hT^DTmMSqk?g=*P!mZ?RI#RUZuq@KnV8 z$`i@Q`6-lKQiBl)t^<}XLz*=sXDxFK>TZm7JP3Q{_vX74=yO>1E!Ld00LM&!&`irK zRF#4nyrYc|66p&$m^X>HL#~5)A`i_VR=Qrs0g_Y%yu04Z*awFnGxSs^9RP8~WjgEj zct}4JBng2o^8N5*jMw+O!0O$Ei1WBbllGU8(i#c!G{Pc`cdc`?Nk~$y+aHNOK7%O! z&eDi6cYu?(J|QR6qSnH#K8l27;|aLR`|%T9uPX2NjCP@`y#U{qrxwI*JB>25pXbQq z?~)j^0%u94%lv@WkR|CM;`M|s|^S@Yo?{K#Jw|_W^C_$~L+KGzMqNqJ% zv_|bttr@#T(Ns%~ghp}MwDv5m_Np2!O07sWD1uVEcE#Q!R`R6x^SkfkxW3Qv{PVl7 z|4G6p@AG_}uW`O70<}I*Ci*WZ|BFEN|NW*?sPv{kxsQn}x8-P?vk=h#SkI}E0;C<2 zk(L-bK{c;WxM)(ugJs9k04peyx#~dS6x?~OkVK8h6O_3z?RdM#BN`VoKS;kq{dXJv zf#-P3c@ySb%37B7kGOgC><7FE=j8RcZB4Ge>AgJ_4#OO8dLLv{nc>C1GAm3SacrQe zB6FLn+U2r(cQ{O?`Lj}OjXy5PFVsP?P5>`z5^pnVfn=f8Kj?pb=Jr3ob_~`|!pA+0 zsT;`fVBL;!%?b=aXAjT7(e=A9N+e4pc%?EIgb(Q#5!b8%180=?V+M}qvwcaN`dDc^ z?O|eDQ}hYzk>O~-(!jf1!bD7)1in-1QN+|ps<_&((2}Eun^rDYN~5a6tMmeBnpSHPc189<<#6KGbVT zs=cpKG1TG0`(rmb7x$Bd{MGMhrqTRWkx=Er@Srta&FwY@@aTx;GH+V>^TR|R;}3!6 z&BRLy)VDwnuDI!&!VB2NLYm8kcUV$CWXAC=HdEq~2XH3H+NY<%UA#QGdct-t5m z=HcW+@4PF?%j!`*PG+jwW#Zh5;@se;k`%6+x}?m`<)CwV#ZU5jwu_h-zwSNrD#jc# zaO8|Ii|y7QvJKq|ZqBAxG}vI&z};*l&vYGhchvo;o+a?}k_<_)utBG1w+}b<8Y6gp zb(wE=fY1|9&Die$bZHDr1bN?kWeJQ+&x`u0VzAt$(Q?MGhRy~9+G z&LHz9k1m3{-E`1lP{N7Y8p*?D=K@WL{E+B9$h;;-ryfq~rjW<+ze6TIVPy8D|M1)M zgfHnF>#qltW@W#q!Qy=q9Eq>QyNLna&3A^>#P*rOYtL5qN>)#M>#S=ZDCBO28QEV) zj(G>S1$laVgfxBwCBNQ^<=dge{<*{bn^YbG*kXhQ*k22H2vOvB8b^-cM=Me%~SdxdVgiRJaGo~C7!NM|!)<3L}Sjc>2F)3pwEgF`|_m>3X z2yLIDq!^RAm!f1@_~F1t6{Cn6Xthj5_8CcuI_Z~l0FpES_ib?e-nPEK8vY5_Z-fgP zh2Nx)a+mZQD)8-C8Y;YMZesPnkoSq^;?d;^L-T8g%N7WKe2wX6DG;E%-@SNgeiRQ7HT3u`c!dmxQ8e_gNS z7&lDk8V#`7Kt8Hi0~_`2gJ)SCp;F3gTy1x9 z7cQGL6MKl-!GxR{Ht!+0S@U}sbLv(Ucf|;j1jR-uSOV!R$mWDv~#u{g1iY0|u%f0FB3^zV9}L%i}6Fl$3& zG~UwIJGQAZeh-v?T-tkaEXvY`;=MGNsBcA6OCRj!@5!z@6jlM=6$4lSUKS{d#@Hwq znCC@`XDC#+AKbG;g?ud6Vv;C9hHkVlO@g2w_ogf*Epjr33bho4Pyq?DK4yMDzqm|u zbyf2i?R0Wsmo%_f1ef;hdKA9?Al*##A(7KZ^9wAdbIMR7EznOtM_?q{ItYe%HG`J%XNjGXzM&L zk1>jfLT84mejNG%xmJ+1(YJ$Q82Vm>S`oLcHgspSC-HcWgUT%XCN!_Zr7oYxE`%;} ztdGZ9$?cLyleWhd*W;XQYnHC;Z>CWN;>m7p7eH7kwmwhx{$pAqR~$C-k^_rSu&Ot*4f{e}wU>wK~!c zhw4PAybq(43loYpQg%F#Y&C8ct4MNnW7TI6{ev8N~T*J>Y)T$NwoI|3z~7 z!C6Y5(Zr?ZS4KWfhSMNtKvg?*)M1gRwbjmcA9&3jXm>8hP^z7I%C;KQQPUShJ5~E| znh?482cqj)t^FJxs$6%W=>w@ul=Tsh{9M`K%yCoM@DxM5$nKA@LW>n6K)mJ_cJJU)2lY2L@9Zi%ITr1?&j8Hf^f}({!n1#{Q?)2VDiO~nMY2?(wz4CP|e7F$`AE5`h|Lo$sRa8I^px3h+PsH`vdGgYBbnE$hV zGr05j!TAm39|uC!PS5t$!Q%g71%NN>?W1aTmm9+psH`>L+~cN$BDM{iI|8tEQ~Yv+ zX!jNF9Q03{;BvQmA%mF-K1{TAT7$@m`~$A~<30gzea|`vDxTfHRlyST8fm&*omFH_ z7n0j9g>I>qA?bFS~i@7td>3@k-Q2#s#bJ)=y@B z67NWtwF@yeshf()x1vQLv4|P*%Skw+@G!)1@`i`PW=^rTi2k9mio~M14NwJ$u`}`% zjmeNGLko{Oe-xq%SkxoUNUxLvlM}>}$2DO{O6CojK#-{;EO08k4gi``ASgZ7jPo?d z+=C4^WmR1rGrRk`>=&Nz@mSjbmlu3?-3h|is;v_oKmH6z3tp`rng=Z)P)gW4SekVp zd+E2CFwt;Cog=^A1BfK_YD%!s1QT^<(AL+Se!Xc6e22meJK#WE;$}?WW84QztDG>MP{Qduhkkx zoP9U8E!vc=t5+qtX0^c)$`~v6K~wWiL{A~q$AT}fS5M~YQ;|rA0WZu!@M1Dnb?l;6 zvj0w-j8MN8=HapTGILuOTBh0n8S=9(yqm9087k3n;7IBLKe##fU_xKsD=f4}l<+9z~K2CLY zVd7d3kCV?FUtL9?T&3jWKl-2iXsc|M0p^$XU_BEBj^)eBq6x~H zCkX*9YBNvM{YZApx4JQ9vFmlu<=u%lW-Mz2CrLKqd`as^Vsc@s7`~_`S35p3X`%d3 zMyIl>sn`AZJf^qpaH9pxM82ci6NJRDMFq|`7FnPe&kly(T-^!FL-3gACGX<$3Ldhmrtp8Dc_{O!_3NUE#$ z=Ymk;U$2iw%;i0rQa6kbmlFY~NE+&1RFmdi(WH~+@MmN1cfbETI?jJr3e|E@B<)v& z3&uY9K}1u`kWC7MEaK;;e4&mkBme?dI^Z+azbnC#a>LtMFd*rMv1q1+4ZPzK;%XMo z@|yH+G95IDbmSnGGuzG16~y!-NNzt6CXo@4gN6XKxsJyPqQ~7?DdusNWxM_oiqNou zgdQqZV#ZpOtP_`6sb8})Y$%g8SNG8W{Hvxsr7GB|!o)3*BgfpVBR0F2u& z-Q(;+WZEBOVwdskU1M*xIOgtOHt%N`1T>hi=lyF$Z(Owz^6_}^R<-HejkqsrMtTNS zUj?MK`c0;Ibfu@w8ZNSNn6d$kubLH@zk_;cwn{Xa3OD%H$MEw$8tUn@PZIG^bok0= zly=}aa&SH44xDwu@L7)KDe&Gx6z#gC7lt?k8AXBtCH6HM3CdRJd_zX!)8)( z7vD(=T;~Yb&>Bng@K4~Hjd37Z&w3LDLhcNo4HFgeKcD9+q(Bu%SX0jK23eSB+A~!^ zc6mzY_aCE;6hLZiwX2h!8LOo2YfI?U+;oQX$<_aFunKkBfjtq;@?~Y>Pt@c z1D!dX712$UEHv)RR+!nOGP|asjRV^LZp2NNTxjcsxSO@-dGo6|2rnDc@t-rk65W+{ z%(C~nbu#2{kG}9YTyQ@wI6d*?mF8~0qi%5Py`IuakOsfejF$g5g7U)I-QEXl9I_2< zV7Sg5YZ<07NwH(4noLQ!%eG67UG}MNi*_$|H3cK~e@kqfkIR3v+?LSY;|Fy8|r}$s} zU5k#-*Aen8cJls{tyfaF6gw1BXUvBs58~!@QxivNCc4wI{l^wmwC4V&qU zPj`doa5R7IGT&DsX*HQj7QrSfiYOH*(j-SLJgL}6rUN@@$?c=hu$bLZj}Vj?k67<> zAIpt7aY>o{8$7CDl7usR#08+A{(wBT5C0+d2_=E(eC3K4II($e)Qiq(gSxIbWdeBh zbg?!fbcT(;IKr)YM&!!I@P(wYS~(M&m%qsME5Eo$#cwW>o4r|TEj_Fv4GxQ`F|i@=f9p|44x6{vsXOrhFZ|F#CG6kHD}`wFV8Bj*Lr zpYt}CLTVHzWWNa?l{bX=a%T{!*$U>p*?9exg=dhzT@%-WKus?I2BYkJMg`5fDl#0v z)EkcvyzbQ|~FYoivueeaK3sW=|fwU;M+;ekSogW}tN*BsYaiP#ErB z+fYa85#9;g>e#YCeAkwl9_Yx6(+?(+{Anbk2lv$}$C0Cp}QSCT62xTJ0=fg#a z{w>t4W*&^=xQ1+M4ZH?$`YptBOQPb!Eb#EDxnAsEc8wZuE1xofrR;lMIZa|CkvOv#x@iF5L6PYJZNDNl8|-tXIYW{gRY38+<+J4m5&HXy#!rp?CFIbu6- z?P<$l^1hl&gSoCCIwBzdAzetyt(ii%$|}_$syH!cJfrNHcDr<@o&~MX2&_I$Uv?lo zjVx~39khjh8=eV$#)gVVV*uR!}5 z!n^Aw?r`z6{jI2{HvJneGA`@G@@wiBBYfg-Ljd#D4RYU)w256kuN(>c8P_2+v=2(Q zcs+>YKHY(t3Ko+@+UEH|WtrJm`Bt|h8Y7V`Si9;lpD!NENB>d*u=*csNa-I@ z=4pGtb$IHyd4wv;&TFPTX6J5d%|UkYjg&qqL~yFBOjY8X0kp)#4DoJEKO$FGuPmLD z@$#ks!ar-=DB@g){|L$K#{4G)Z_2ol<|S0_zS(n$m_)Aw`&7}!nwV&t(ac@~?$m?S z;b!nKyv1q**6}-%X8-wK?_d8^L-FAk=-pqE6CBM#Psw_aLRhJiD+axp_)@wE+|{cf zwsQEYv$#Hi79fC!{#b{(9usd|J5Pe8(e-wD)q zjmi}hDEP&*mj!xxte!=}D@^wUBSZhLxGXc3gE?m=6eS?r{QH@8&EGh*X(v#(Ad@?b1Nh<*56DEIyLYTP^jHNV51#avL1F|2s)coQqMd>rQ? zu0I0ihu9ZU>4uoFr_+&V-_Wbc10%1|hHn0F9W6pgj%F3BeI_ft&vdd)lN=vwvs&0Og-tPJ>0OC?`>QDprz-=pR>_sOo;cmtmkXEJ)L8vAB^J?} z(Fr*}SHEBkyujGrqffshE(+Pj$m|rBxQ!TPHvWr3zQdEYIdY1ct`q&BOA!#2_GB{j z$;X15pl@%7BEtA5x8l0Jo(dOw5EP8y^V}3)y9{u!#$JxUf>A{mQ$dmbespl&JAdKA zr#8gv1?MYOduqACS?Cw!S^igOfoyU_D<9mE*V&&`Udmv_pS9rI%H_<=>sRc>^;nqm zVtGf7agx;LBq6iF3)qy6x7-mPYJ0u=d}u4mFr%m{lVJDLk#Xpag@do1Sdg};tEWW4 zO{5|f-8`kC_bVrSricRAHt7`z%R+uh@!D3Y3{zD#G@U}W*-g6V^tEl8=RfK;j=o1- zFsl)72~a1My0u=6b-Ffq`jN?MTQF14mv{MlK-7HL3Y|65v{FCj_<%l{(=kjE-Lz! zSK_hZh<1`TG#}`FuKlvqutvogcNci`VbAX-s|uE*k2oG+U?sDTZ3OLiUTmn_8Vy|Z z=?U0LOrAY^ssWq>DY^=0@wx3gigQ>XiYO?%I7;$yW>1d!QYN`6k}Zz95!eJ^OAE;O zW60*Wvx*yuoA@#idW3q@k8f`t;4Os>#+DW`@%rF)nE$$)rJDz>yFN3HaCV-#xyB5Wg9?+eVBl$wjI9j2!p90<=kSKM$%5hGf1dwMmQ)q&W8?YAK z;5lXk@Ni0&Zm6I0}$?eN+&WDP&l0Wn^J?p&k$;QB`|mEIG$X3+UC*LVMtq$Mj1mx z-s;B>VF`RjM-`Mr4K7-UaBSW({4o(#>Xk-*J`%OF9XD1bJ=-(EZbKIG0r+J)L4rImxD@wZ-fWN^i0^8~jcqmJM!pOt>MYnC+)?@-h~))c z32!NUhnCRmutQ>|o>ym7&%V6vkh$LRaC}p=Z29gBP8YZ@rPO0yeXywK;=%h97ZC`$ zSR_2p#yMyEykftgHKm1fk|{J>eeOp`bgTye82`$^yj!_(V0q)*meHf8xTB(H-tEx_)Gd&4)Y>9 ztUm1;@-AObUre3!I4?~8%|nH($}iRngX@x4@s) z%$Qvf#c3shFbq^b%6Q$_%Dv(V#v|C08l$ZQoz6py96wtYCW0Oha{xmN0S0t^2+rta zqvQEG^wmXCjVct5N8(0e&ud;#@NnWQTYYasHG2Xpa0KxE6@YB&S-5ej>~yV}F$cU_ zJR{e~!`G~^9A7&Sv3w*VP?%Y8rTXz?-p08d0nzw(@f*f_>3CP{D6==3#_q&I+bncM z;MfY1BDED)UssgBmRq5nuZoP~lsq}x8TjL|%#Z~q-z1c$ZCl8T!~~uO%v7E%do$I{^W?&iNmU?~ji42v&&J=%(&`*yhT`$qvE|?^YW(Y2@=CJ$mK2=2e zoSifswjp`w6b)7>OgkUvaw6e(D6&bC4kZpGj|xC8;EXljldu&%`8$N<4aIpy#{5$e z%7kT{!}JqS6&P^;-7J!}jV$?-Yu{E|f({QjQ1VY|c`2~bzx^WsMwEOP_T$7R@-uZO zz4``rtE+`h6bI6?IA0PgIhPzjR5>0aC&;;UjqjFrqvfa)hMOW*^7Ejy$3ThVUJ~xb z?_g|OUJChXwwDh2rj)b_U28LQ zbUE>0S1b8r;bO@!*8CY65WX2unmOAA8HCCV8T(wCn19+u&S4nF8EoYt&wYK`!e6!w z_R8}pvr#gqnXKLun?kbey9OaI$92|bda18=>>@-T&pyrFH=uYCl2;khbU0IGcRCe) z=dD2d8Dk4^^9xz~E@E9ODBSGf%L;>;4;52f&y6M#9m{}Iz51AK zrZ_{2zZbY-VPde6-bBNMGfWaC$h!JTJDG4E9*5*+3q;WLH*Q*?>Kk z+i0#lL40PRex+whV(k8B%DC~z;nI}PQVN>gcnTT{PtMT}etZ!diJNhP9-k^;PKpuN zQ)(bKMS;{Lru86n4hQ}233niV;;c=%@&!J=-VqS&R(?3V5Mepe!%%y=TClZ>r+Mj1-a2W1eS)}2eq3N zvg0vw&Bt3p9NKM!ttNmD{vkiD>$AT*`s1M)OWHsZN$T-#78{}E znMs!(`Pp%I#b9QGiSSZ#K*A=onc^wtk#j_AaPmz4aju`mbX=)o+;yF|mW~fEE?*ze ztM~B;OB~7}$Hi=(NlLTQE)(OlFy_~hMI=9t_wQ6U{#6DXw(IdUB84mPN{lBBPiKmg zgAcVVARjkGY(v>&S?$~s2}aF(Fi=jv2; ze+S8iS=tp<%(EVUODJf=(fXR`Q)CyTt{v!p{5z5(nI+ALXuICL(8lcOUb-vUlsz>g z>P)>pOQu(+_&W)f`BqpYL)j%ojA?hIqhLkqrV4dw0In0b~_Yw#i@%ZvhZ zVI4>~z`lNPWSJdKrNetuG}i;^sX^Fe%d0E;_k|r-0%@7lSBcEb#lKoTWCS#j(bfsF zX*RM>XY9exemD+Yc4pwDi;c|z>%T|MyUe9Zom`24X7B~mp!rOXdA0Y?3BkFYObUtn z(jq$=|MJ8?_4Yhn+Orm{6#Fs3C698pk!VR_%kTqpH<)`$WRA53%5u+|4SdTA?5L)c z#*Epdut$z>j{nIz7++N7`qfH&p$*-Sq(O3wJsH*^)G_H}IyX>uFGVWmS%g$Yy~#?C zPQeukHM;rT^)gD>X30@G&!|;+tm~gO%qSilg)#@+rwK8r7z-5C2!*%SS7_KS_#0p)v&)8pHOn=AdcaFH)gQ0GF z1BaQ9P(As&?O^#ni^5o&78K>;iJ26aOH`s2UuTHrWPwQ8A_e%pWk2$w^^*wkJ~&EW zzqf)C?IgKll97Za4&hjTxE}&Jqd{6``E%N#VUU7arQ3l%Ik)W$kcLb<#uY*?sQNzR z=oj1VUJu=qbqruynq1!(a_}$!%sMb0&Cgn(NR}q^Q3@uvv+T~EzW2E@HjMB6Mm^X4 z$mYeUh^%PTfIppUsN>`Sq9K5}Z9+E|MfdJ}x%Dj{r}$c~&yWJN&q-F zV38^tcZsz*{SG~KK5=>s$Ex9e>V}l;B)WdT8ShV0yg2=m(6A*GqoPC4DWa#3Wec;q zP?!zSq&|mUcFy{N0W6|+N3tLoB*ss(v*dPe_W(af1@%PvUp7YggK6i|3U2CfQ;b4O z8SDf*gZcA4RYP09JeY~v5httpnD~`7;oKa?N*Zcrv3o>GR7UAoLs}T#Mo_Mo4Oz+j z@bw8jZ`B;+GiZD`bs(GO#ll{E@enb7nr3}={)L~}3hFD(ZlCcsi?M55_)-x2RY#~$ zoWm33r9Cnfi$C|s)G}>II{_Xu!imC5rT;WNl)YY26CtF zEurl}er-0I>5B|a2@+km3ovpG8-$&iSUFPD3dgQ|iJb=)_~Sunc82dbVe1R^C6W(k zsDHf?^U%Iv4UFu+J;Yl&|0o@V0$c>54jb(;cPj=&=a8QWO9aki{(5)KU1 zuRlr}CcA?fLse1P!F46;PyXHUf&OY4=xJYdZ(;UEea%3(m_Wbb#%7ZxTgF(i!uVpO zx5T#;o2!Hz$U8)o8x?yVydutl2S*g zoRdyr^nC-EVoX-+?W@xm)efAG>OXkGrXiNnQ5H$h)4I&PmsoVq?w!-O2G2r1T!pJ2 zsR1M-l823$yG=6qj|#^-Yo9nfz9>J^YSA)%_6e-oIb=A@dvs|ZscX4VULmuWx`qVi zF&}-h1f5hu8DldA}vX4G~Yq`O@D{0rY$NK8~0p4qNL|^~w z)-jH}Ogg!hE6J=lyMlc@&mTvY^CXb9>hUbbuNB3m;r@v={LdENBOAhn$<bA z%}8Uek{p+ii?ax{X&`^T3Ex@2>Npn&$Y9@2mGAbfbW`$4=O-lb5~?Ilc^Sx?4;A|B zN=e*Jh}m2gWI>ULT1@p*o`sbcouOWMaXSC9fcGv=^|t&7_43LcNm{wXZvc zCV3na6Kl)Ry7XY@Cvcd?WPD^*OLZc-F0UZAa#?;FW4f;l5x?x~e9Fm4#-k~LwJEll z+2{4iQfr1JV3<#=_!?ZuCBhxj+GUd*);mcl!qm)8%cnF> zlvMwR(Efid9sVEX;D77`1=@};bsuo&()j?nN{QX%6+_TniPtGN+(B^2JmU6UW4t0H zbxtYef-GV)+05vYTueAZe@i6I)d1>h!q$ZdDKii>)1Yf@#~=v_l?FkCMgoe4`sP!%-4E~C(LTb%xCCt_ed`vN84=JXlBS2X$<@i_V6N0 z+)>rBARn`zs7x>zU;;?4SZTo(Zwj}B+K8&_nse_--Y|kg2Sa~)LIr-4$8D|KYKKgc z2ET+RRXmXig+684Nelx86Sj1Af3P1;Q(0Nn+{a-M6bdFO#T3s?zsLP{`xz_#y$r(N z!(+gH-~z}-ve36Gye$k*t4%0)gX?BB09|O$>P)6hRi+FSKq9zoG|`!+0-sH zUHMQ~^9?4&8FzVXQ&46da^A%oD@%{G7JXuT@Vp!#SKNey6b2_SR9qCzrbK_)R9E7a zY7O9iI;Rc@P-F*1R*dO~r$vxdJXG^0=hUMMBttY&_dVxwTyuUdPDjD=$b)M8xnzh; zQMuSLN8s7W;TH3&_gK+&{?*M)5hFk_4Jqu zsyM==a^ZY0t7+q)+;NYBs}3tGHN;7k$whk3AYD{DiW^Y`qdXhxQq35tN*m_nS*cE zYQEe9PW)Xb8HzWGw42wM6A4HDhp#O7Z!_Eac6VkY$1hFjTYPAPRg04RCK>tGs)ro1 zW4}lq_iX!Pstns*@}kHM#>N*yO|YV&Q#$zVX3};+dho6)2*ULxA8o%`?)>bx|@2-eW%HIZHr!P3b{`XT!MdRgbl5j^=jc5@^Osh;T&|n?!$!*m_M<_CxNf z{(p{GQ>Sh?jlUnc48ORNIh2BszLQjcFkUf97yLkKJ_aL%w#hm8#59zqjS!qR1IIC*4ED^6@j+ z^$N-{W7XaAaM&f$9J^kj9|bTkjN&Mh?qbEV>XweXfV_Y?Vp>tlKxi?gz+p74A_y)I z^7PCCw z5ZbmL(vvpw$%7oxEPrvJ^9ex4nx3{``Ywi-5BJ_d9arLV#E!TgbML%#>D&g}0o9>h zl8-uS3ey6jcTSU9IOYB`&3Tz-WO?rmu_$l#B$oGSm{Y_MCg)tGF z&!}@X2QTndd+a-3dXg&99xgFEbtfC}-tKJga}^NH<>EYl>gF}4Jh<{8-+T^Ug;Yu@ z^-o)VtL@*v8|mMTR?lPA*=|!Qjjij}D6nXiILX|+De27EB=&+0_I=kZBWsy^wix!R zjvSk(AnX@E3zCn@7BroJ^KD{wK_%L#-w2YAGkDbcovWKsuOuX-JC1 zKo$~MRBU|>e!|j!CaVEVV*s@0@Y>xoBiX!MPVyjvf`ymGu^-VE#;{6ywo_?4{J=n% zcOH&Tj7XH&xfB|q{`-Ujty=*vS73^?bA|%Cgr*87NWXEnG*lKEAk-+jz?O%h9`FO& zg>D;+<}7FTB10Hy(+OF2`+stP|GSwb+xpKE3ZgXZZqhMf79>jIY&lGRwrc zOOz)&9sJp6dJ%C1`6c!)u_tAisRCenDLYA1$Mn^wZwS*A`MOgk&#IVALm@4nWC=90 zc^<0AHCX7z7X%d&g5N4IAF%w0+T$=coA{PBV3Kb_6%$R==O+Jf`7Ez9jX^lPvXqEW zJT+i)R1rxNCeSmN|4~1Zcx(GpczBirf(zv$%E7(A!%{*^tTYScYq;o| zr$0wl(#5@O5FG<)MxrXy2QeF>PKtQ6l-3huk4?e5*;eZ0=2waKm}TbBuO7X7tuat^ z;jcBHO8xpYV)B;_G}gEZiUd|&L0K?cxymqpL;=r@Fok&B!&nH0v*qf9K7C3LQj-Y| z<3Wta5WegtBN!i3fmU2c{EkydB;rz;ZX}qkFup*axbY`bkw;I6^E=}v6)obPS@paQ zQ}E3-!jELsgHktMVj@`Jog0BhHzN0PV+4$^PnW7RP*`!NYvg5aT4kKC#e(2eg3g!u;0g_rP;T&|zL4{gx2zKYdA z+Ff%V+5ZoB$carR^vOZ$CdL=q4A1&c)qtAsJo z*VvG?G%A}Qz2EMtJ+c2%?$-dl5I4zc;u;rkYxC%%58d53bg*0-I7tU*osG}X*xD>V zjYmBSJgsd4h+3$(Jz-FuYP{ch<#5g>5DnY*#{it9JJh> zI#6@68DwI?fA@j4c5_indS3Cnn8k6@x8Hz|6Pp>C6a1f5$7kTH`M=_fen<|949d@N z4iy(zf})3hNiNB?S~y40JX(ZG8fye;t|vDH%jBxk_Q-cHHAzsO=F7HXZu0#_kLx;@_;j}FLi74?`i=UEo@DL)amBMK zJn15I88~@PG6ZN^Zw#&p6$|t~1E0g%zfCXyrvQ3$3czDC`d&PTy_W+Zz#5D|{az8L z$V~B-!i8jp+6+oY z>H8(g+W9_AQ;gn_VHPV9mY(xfVknpQ&z~I*WA0*KK!SkI;&Z1>k>Gk%Bof7_M0X?M znK@$#0l2|WZ9I0DHN!t6s^rH^XyH88fKfm@*BheFFtt#6>TKDS=`Z*Sz?joWiG=6* zWzNgb*a2~+tkRUIz3Z4mflz?d<2Z#W)la>LRnspo<`K z=lAXJbiej5wg*+zHff;lsahfCfx&yneZXmqZXDpjX1n&6?f7N-<_AD@qD%{ouMHe7 ze%#|Fd@9;Qi^+<<+`zLXNI#;8Zh%k}0LpPYfj2r}rpADVh8WxY?}c&Fz@k4arb{A7D{}twqJ9yoo99E? zcg-g6yoR1^GMV;v1OPfGBYtkEFB!mO&<@OJd!Wor@VKBiDvRY+M$tklL(rLf5!P+P zt&{iDD)iQY*i?TQ-1T&98pN$ZWTV;pbLcsVSLN8NGt$dCk;?hhpuw&$7UORU)D?KA z#Ke2-(@A&jpV)0PQVZjgjPw%qhdsscS6R`$htcMAFh%h!ORtJLxG!YtBty-zY80ml zwF#JKKy}x^zx%+#x8lNoNv2%#a!0RLxHPeE(Mmwcu+tfu^TY@z0W>ubT>r=%JTr3+3k>$Ep=2CJYHR_sIa8yyKAT4rUg4xRVxW_+DZXAMm>Atchmt>1YmK09U{@Ks|G}zQG|I z#i&9AE*!2gkc-tQz<>bzEHoEKHfBgPbmMH9Skhnn!$)9OVMbXru74mu4G|6hFIGJXpLLnr4<25l^aovy_b`qVa7DZ5mY+d zbaDMi*oAobu!;GaDJ5UGTAVWZC-rR2O>JwqYGaqcI4K<<}^qz+*33y~5=VBLkVwYVf zGaOr2n6Fqi&=SJyoav8Po;sE;?@;S!(tSc-HBrat8GG)y=aXVtrj8ezTgW03t2o-O zZbQ%P)h%-my<(pi<@FPJYQ0eeMwDqnzou(BUY49j+bo?e?hJ;Eac6i6KFq!1)ly>9 zV^P`qUVi zLGF@;VDqN^S`hsw;@(ZzGLyf7mUnev(p|}$h+E{wbv_t7gIdGnzQR#ZW9B}6#jx$0 z#k7;sf}y|vqr3g@D8<@)^lb6^8{xyRs}yEH@nh~EW`1(F4iIs{bFLJte5762uW0N)n6Ot9n-T%#ALxGg{*YpCZW6zV*E+2 zX570tuMDLnw=c#<#m&UcLZxdn~AbiD5=8&RDn$P=3$lL#%RUUXV5*r0D zpPrZ`V2z4B)5RcD<|mk%7pvhx5O z#9m;e0Aq2jgp;Zy%|kH&M`zO+;q$ta^c#|DuWxk$Y@m46qn=$b@3gdO zVkK-=M9`7f`}ymz@JBfc%-wCEZar09Z32yZO3E|F3q)VB+@>6f>&xe2kEHHgX}^}o zs1~IX-{w$c88Hx;x9jIH!=Vm_ZfOK<+Q>6a z-hy=Xcp6l;t|rgh%0B-Y9g4d&f2ZI2IW6c-6$`a3@OR>^soQwN$iPC;+3;Ik`!_i- zRr-9Hy4ExI2sDb&Hb?U}Ow2vfS)#*KnwNQmet6=vCB7%)2}xK6%{t7x(Hz%rcfEIn zIq&twdFf4V$8I%H9Eam}!)c5O=Jl@y!V$GMs_DxEhu?Q#cf~mqYCE5P|8lv0ON+}1 z78c8)sFGvy^kNLLORlQr&Ba#VLBNdUmB0&WI={#5?{=n<&h~vfj2S957%=4rI_GtK ze4{W?=4SIqaCT5d;K5(E@9~8T+49wPPhuD*GyxwwZS=;r!(6iu58~j;YpQgjPmeW# z)o)H4l~Lr@ML%48zL=a-v3U_ml_0Ik9TQzQ{na$mESfF)cU{dTBR&@vYG;6K>uJ5u zf4V&1vH-x4W!r`Vdkmqr9pcC7JFm;SiT?A6^c9h{{Y>e5!Am8ZVuaPn*EZq!S)i>RfuW);9i2>GOlueMVXbHM;J>rTR=q>aBZVQ2?I(D zNQrbR(p^I{NQ-obQX(bY2r@{cq?CezbV*1_cMB-e-OUUz%=wSsUEf`Ae0SY@|NmX< zu$Z-gVdl))XYc*&XFrcjOS-FI-@H$fgyWKEiapP1Or&K>ex>=#!WT;c?c|7ut2QZ| zeUTB&o0JRcVEkK*dx={r%pxS%O<*onO|ExrUS8>KJpVmC@^2DQhs1HCOfIY5=RDNs zw<1pc_AQ|U^IE#hSG+ANxd?#-v2Yc;1DGjDL~Dj#QMIXt8NlRMyv}W)pEc@f_P_`| zg>EubaJ31vx#C;e;PgP7@TypeVr3l^U-$^K?&1+FKw5FMm70xD$=il4slwt8?_8!; zRB@m|2CUX0yeC}{?=@cJYgU2i9Rb4zK$G^Sd)~EQ{`W8m=14TiRownG5+?|`Z!J!J zzKr|*uN{GQ97~i7q~2zij;V&~=_>*%9(EW&H$pvyb8^bkjac}BiwoP;nWfAiJA8P^ z49SssLq5zVv3I&PAq{D}bN8>6!%I-njplKk>tbj(N%#B-a}c+Uaa{9C5fgvB>>1N- zG*g=paT}++?$#{i1b+ki3pS3_rhWkvQQu`7ULL{fszg+dr_0h*X|KPmLQKM&{1N~eP|D%NJ5PAsMG7DH>^pu;(--rIM=)<15%fEKfJes9@y z35<#Aq?o?Td$fSo_%3lW5sH%srL+q&Q9{m9)sQQ4vJak*YPW^Qai~~x`@BP^yO+m> zmhBlsY|%6Xw=7mN#oGZ#LhHWMF;JxQLHJVk^d@^@QtkW|bxkMgRNao?C_)r3r|c}8 zbCWId4VrH=t=g%^CccAs=SdUockt|wRBd$-t=RDqMQd=|o^#nrmTxq}9M(&U&|GN^ zia(5;($Am;UO`~w$@IHef`s3Z{OUbu!*@n-B2onf?1nu@mL#Q6i5d=@mtW)&yk32N zTr$&F&zEsMelItvqVTN>g16fHl1sWCZ54A{pp<0gWuD`lod#j?E;v0Wgxxt@J|CIERaCLd`|26RbpF{E=f1+ju zqHPISTE^Zu=WUc7A_v8n5VJ1|oRb?$jIqfueh4l<3d&2gvw z`5wp0BCiCWMc@>@jCQ9$aqj+_DXt8yf|yW5;J)p?rVZpi*RXtfi7#gpjtdROZDEfi zCPAt>(&pbjS27<6p81qa%ksJgN1kC(YRMaX-LCB`j_}3XB-dX2s@Krx@2_iu?|^kak<-Vx0pHGEMas7sY>TRpkAeN}qLoUhG0O?lSwlJ21qKg8RS)$pe@lG3B$MOGC%>;8FD6Ln3B>t8 z{JG^n9&56Wh-Un#K+b|dp9^rWk}^@Vap0CzdAAc%B;LXI1z;~WHqPW{xYO#@Fg5l7G6#1tvh9f8<0m@Z)r_D`v!3RNKRg3{+gkd4TyAO= zwpvV>3{;j4d%aXZcndfQ>B0&QUy_;9eZL-~oX*0xu3BFy&{(N+9!x*&O!sQ;J}?(Q z^q3}EzKvptyS>AEw$V%(!sD3SvKko2MQ;{KXq!2`T{*cQce_DVFfkk}sWxs^bY}=D z3N{O3Xf`ST`H|Wvpuy5hE0=`hN!gesFUk@(X^ewuCqHT$7dV-J9w{xX-L19G6~h1G zjogDDKv?p!&IOxP@o%jK3=TUlDr)%tlpagc;QeJT0h|0F(>&n~q4|WxE_@mirZvVm zZ)t!`C5d-O*t)-|+~oNE$C*kVzWude>(KOa^GrUA7Spo!^D?=x?Kz*F2NR`r^Fp(8 z0_QEUJ6G#jF7y|r2cx9FRj&A~e#OuM2{{ulb{^F1eZ5ZOFE6qa51xt5z5W;3&HvM> zU3@91J1u^W4k|ZufXVb#L_nex(Ddb8MD3B^@C7pt^ZI&;dK?<&i82ne%sV>8THeL9 zL!qk|8KW5xdCH8+Aj7cthj18-haERXK%IqR+Q%Nihwz>C;a^_66Q6RtpSQQPd0lk> zjN_R*e0%TTzIFy>xUR6zY^VsJ$*?ah36QBizbD_3vVH#>QfBHWw?-Az_DhtA4Z+V zR5rh37BgRH4&FbyuEfPj#xmpN3xvtEHObUfXrr*ZKfh0YCjxXjoBLJI=|67W}(7;|~R35H>r!6U5v&(~0D? zR6nRL?PW_q|Niy~K`#U3KJiIgeFQcxieTa6p?Dk5ybhvZ!EW%-HkKZuO22VxDxDN)(OdZ_oB`ojMiGcXajQDV3bjUNkmxL8h4mp;SAqL%62o?|J zLmu+SmJ4FfSN&}-Y0AwO9O=>{M{$9`zsrm?RzV*7b|JZEvET5HKlu4X6Uf}?9I)kO zWfUZxJ!gS`-d7AbdN4)S9hJcgq0$I0V;O9HHW1PQq)pL{q*XeeY!{bnI}B%M2J-}y z;NJ`<52%w^^Z2AXq$YQs)sX<2>X=N9q6~P!2xK(aW65qhz zjvG@*{%+Lm=VhNL*hHPu57g5h#E=aebib7nD%<6FzB?@oN?MfjS>(2~ym-QgxrHUe z(?3XhlSS7 z`P_bpII{8HIi<_0vU+s>ALa+E$^XsyZOn7LS`2Tl5MrWZk7sgYLO74gUXm3)*MNR# zX@xGgZ3tzGyt&nLsUE^9nGz`l!_zut7tB@GPubF(7uQP+;j?%XAdUkq`zfw!LE6~j z;_B9F_0PTa*ETFqQho-9U-FdzO4$^pw)+G!UIA3HH$`sj#@6ho!f3fQT_X!NLby?O zVY+`sFX?jexVvSk#T3+PNERnR1l)l{S1ki}CDNux-7+AGQw-tn$A0>~65<3+THh$L zlBYidNw0DwCODjUqy36NSTBU25Z(^(nb%j~ox;osKt%h|sG`74cUqitV`lyK$ci@K zNb$Yd42P9@i~MnUAw;9Y z9&Ke2ymu!VL+0vNq%Y42nSv_tQt<_#_`@gk*3JoBX|0s^>Q326P930Fu1Rfl9zDrE zetwh>b`e)D8Gg)6&ezlJl~Hg52>iSF>qr?qbb%=GYJu$cU$E*LxLe^~hXoP4*~LLL zB)RDL6^hzR(R?cI%g+tjuOIGZ>Nh+U7*<>dN!?+!+Zx78q22p3c6gC0m`?Z@_fo+o zD{o@U_cC&whJA^Hv|)-K@5fB~d4K8Fgxsos*hq!kkTDk>Oq18+8vI&+{l^@Ltjp+5 zS#Vqxdn-B#?U+X8d!Pe;{5cQ!y5bGIau4vv?aaA5mv<>I1s4wAgU!W*9IN$aq9c5O zqN4wICW=1nBn(T(28lL>sWwuvjrXMQryjz$3AG36ZA=y>V?@*apW2jUcF0ddwmE1` z&`d6(0o7v0qgiKj?O9ImL)kF=D;-~tIC8lT-6Fdrzk%nuMM?s;>uZ5qK3Q|s5kK>4X%nn1q6tM*_M~7E$q8u zC8vxai<~2y|IrP9bjaTZ8|7u57*r11m&fHuq)6IWh#vc85Oe-*Kr9Fx+j^xG=#r%D zBVK<_5_pgi(b?9aJrBQ`W2*MA;Jun!c%(Im>~LW0pwzvL2=`lISrofH3oXMNez~JB z;CY94%Ak;&0#qC*?KZE-ImmsSlp9~5Bqh)p3a}mJ{Q!B66G;$92(IV|pF2WQ!roV? ztu!hc_z1sd(~LZ56t@rM)>Yl43MPf}TkS1CMFcj&cf2Aw;WJR$=)`D{u-pT$eB_GSDeoRAN+WTzG@>|s3Xu~8RqtsmPJ z8n&fcOO~^Bk|OCjmNJ~PPqvvCLmij=KDRgq?*D0Ms9+@b9nFqlT1q@MkzjDasVMc3 z5if;qxj@tK%Txz%XF1K`wu~luSl`9NpuR6$Kj*-ZwdTXO!K%@VQ~JCK?tx;VQ&Q&0v=rWF~NbPo!IDO49epk18p_m{egCu!Zmed(EI z@+;;WCL6R~#N?|bchC!eHE_$#2G{1=0Hick*ynbVBcmWca0S=}r0ddxqAsm!Xy(71 zTE;(#0g2JA#46oFY3R=`rV*F;t@I8POfmfPS3*pYDqc*z8!%d^G7R3HSO1AjA$E)F z-SR?63)$m?H~cNXd8{mOuY7O6BYb?2%d{Xkce27Xc>Xt8wsc&Y;Q{aXS(DvJels#% zm-F*$0qE(ETTE7#Sy*hj#sm$33Ant1m$&$AkUh`YZ(8~GYyP@p_H*7J@7dEg-=F7& z1aYC5j11_0MOK$-Lm`uCxhP4i}*u9_0LPc)n)0?P&rOZfV zfrb&oe?mWMebGFp+DyooD}g-7D-Re7rgWEih?@dg)AhS+%uKdJ=+0I`x=4PQ(!&%Q z#0S}pe2jySYsg+fWX)jBNbC*OX%EmUBO!9;PxP>g;&aBC>5;P;>kMWb<+AZL4m+v| z$1QQZiy<2wS0a-Y>Bxhqy~hdA#pn8fcbE>sLnRJ39mUUdM-yzXA<3Q#jeGMiCRyJn zCDc*trk+*msN#{3-)ReNTL8DPXu4B8C_mvtwkC`8g*9W^B{+kgImw8hJ&}PUka>_= zd}WSj&$7jHUBUh3lbp(V5)4^B1xQDaBWR_0ZGV&8o(D66u6&Vj8XOLcsjZWM98wt^ z7z&h(9GdbQFlQHoG*39>08VsF0An6Wx`qJr2mzt(m>f=aF>_oeSC|>KwSmJ(ZqCZ> zJ?%}-q9xY46Mq(dYAljEDjO)%8B~%nRy9e1Po+301)@ zbb8dqjqDUp&~C#Y_!01T9fuBrndG@qv!0YsgQ5&8=borBrrOS%WXemM7 z(8Pu3Pq9R~#Yr>g0;P4os4%tQ0j#N4Gzuj|c%1CPwv*2YI`T*pdqq|{3|4kZtWak) zgRt>QFhN?Y1zc6)Fysdy^gIp)^ADA03L7#tUWlw;Cc#apL__ltyeOGd1@u>sl*TX} z(CjgU?+*~L5W6M~T1CzBY`WqINpycd&6^vJ(#j)5))9LW+8^YQ0fNmZWAM+u@XMal zk#at(v)Zlx(~eoU9LDQ4i-`i%wc|J6`5&g;wa+v6zV^Vv4y5yP;8YL36C9qTZWWBQ zHko42pU6X7v$5w^Ryy$3tAs5Fcw_K4)HRxLqC7ED^uX5v!v!H=)_VA71yxI6E<+%iquTKD#Co%SMyv;4Rd=hG}* zUQ8lB_))Vz-CwUP3jN+-7~(@Y=w3d(_wUhbN?d~XmNy*uQ^5WLKj}>vx5Le)0X1xe7QUK{6erzns4#Evy3el@-BW(2{?nJ!=$KH#JG{ywC9n)Vv>2K{E+}^zMj|im>3YpO9ml#0N=4u|Cih zvyIu=Q>RmaBBAz0Oa2I_NX21ma|ZxP#PPvRc|vax3tS9^=69rv$6xD30ZSW$ z>AOts0zY#9ig@2#u$|XEe)e$l^TJR=-tS}cHgnwLpj_*F)|P{6%r?gBwbPyq`2HNa z+xg&=tmBsuU=9W|g}r2MixA57$Exy}87XB4-{YO0b|1Y}ShEo0OO(U-4|3bRT2r~D)rH**J4#ZGHd~RouBg56- zu|G{QcVOw;PjAJnWoLj3V~NdEtyTricW$AIhb=$Sf^L2cdw@|&+GzAj1e?5q^2v$vkz<)8C*aNxUm9)&X#XrXn}b!OW^fy+I*$(i{nJ4C>bZ5_1SG9+q_Cl8a1` z?>pp~RhSKvUyhx|!}g#nyDLB0nDEm&EQXfcNpMhn-4jT09r~{F&naCnkGGN02HttH z8e^sbAvjqJKa|`3=5HVp1_lA#DJIe2NS|J1%>aBm9%Xo zwid2l(gh0B7y@OrKi0Bso?6pHFZ$Hx-*8GKp5qZx6WxqVIXVvszoNIjZK_X}r1GNs z*jG`mx}^gwIS98kzEtk@2=NU*u@gWcK_;n=rhxVQ$bX5(H!e_C4K<6QQroL4u}lPB z=_teajgWbM4zydsPeI7#rUP(xcG}7pB6eLa4u7||0iXj2v8XrFZh6@975F>s>`|T$ zu!kP`l!qlkFBlvyJh0rmS9c&y#vEiMe;lkdb8-^oONgbpwYrsVWtBS&Y#mHoz=H*I zLV*Ked|e3hVXNBycb5S4{P$04`^_!^yGFiRT#WUBQb%oxpL#+r3L3Hw<^%auk`KQ5 z;N$b1Iq1u5|3^E+FSu$-f03SUiQoe>C!xvbl>OhX=RqllNXyIJZ%J}64XsADqqnP4 zu)PZQ-Ar07&)r8>z7xaOp2U>gsGWf3e?yqXTX1(RW~zx|?3nNbRA?F8saO=-^D3IU z*_M1}6_;VWwsZ1sGyFJ_I6e^sk2%QK{<`>}9#iFO)#xOVxu=7^0wbgRyyOzroZve6 zV;n#lOn1+47Cl_^jm@J&h{7n^-{xn|?Q5m0%HT+|A$#NwrQ~Ty7a2;AmtAMP{E_+IGS^9*anxE|>;*}-CW(FQMrT#z zhbO%3xp-|=0t+$Io%iK!pg|nnB!YZS(l^89WEl?Y3}I;7mJ~Z z1J5~zrO-|fYj@DACKX4d8uAO=z#Q{k4Bc)=krIPa^={w=&$*b`xeO{MorK^5IG7&e z6LU0^SdVaz{p?k`E5;gHzj4-JRS9Vb{5yMT&#E&6H;frZo7w@a(87Jj+vq&G!aVl_ zlFlT>pr`Q#5T3Q#Q?B>r#^Cyf^fF)C4$Fvc9XB~tBG}@UH(gzv_5&9| zQrG(%6M68f>6>hreY;#U-G9u`d2TRi|06y|y~F|0lmi`k=%xrK3BqoNnn5y&B0oHkf58aWSHEy|aB~T1qrTxL z!yvt9e;4)LK+YE(@h4cs=k(oAd9!|omLqbyzU{`4U#=uTLF;L%BJej|g1?48!4DEt z9BQdTqHdq3Cf&RIM)RP%8AD{U8Z2*cNynU}|5yMGGJ`MSs`R!9FypS zd%<=i5&W}wkd}9_+!YII%7RqDtDD`z(~bDW97qrT9YD+_2ld#vJFfC+lko`%3TEU{ zg=Lj_882p0tsA$`HZ2Tz%LHpJbPJKunsp037>OZ+(&9@X0(l0HBA-092ueH%lr4)V zvx$M@Zd91Rq19fJ<79#s+__TbrGy0+bO4@ZpU>k0yVu-n!t5m-bg5zavH;qA)7-to5QnEYaj1V7=ekWrSS#mu-@ZZr|tX4 zXTZFyO#P&FVmciJEJK2yTpsTXfM`j*$aSb&s2FH1U6!WPg5JJo?Vj~X`g!w{fMqO` zR!`_z!4~=&p2gU-F}!62zg{{m&bzj5JovnM;kL&2w{{JVX&y}d-x(-^8zbs>j5$NhO9Kz`0rrbPep5Q%6#le2%6vOw|I z`;89|RKz%961trrDk*6~|A9)GE67r-oqhSaTvi|1Suh2o$+qX|=9on~Ab36k2V}~u zS{aEdl8DsBl;J|rUy0n0)j3!Gn+qUr;$FhrVOPdMU?=z0w%{ws|D5=|gDZbbo_y;T zRF9hbIBP9b8l=bKEF%m|sJ!uFD0lRCcF*@_!`&a@s;}!m;$nqsn7)E&*fxBfjIm>u zdlY!MsF3_^OoGW4e%(Qw0(b=FYOt#pGTr&f5g&_UZq^S(AKI93m865&8H0%Hzf=}b z|L(q|$Xue_(m6w`pJwpnX{G~POlqeY>?sc`pY)4O-99hYcFvrE9Q@79j3g|hXN~_0wg=EI_K^*P4BJC>=>J5tHY~#DgE!XKU#m+>~jAY?x}Q@ zvlhBQgR>Zt1Q>Y5Ng*tq_lugp>MNJcdRkVr7FDkQicMgUd}Ld8P-1XyTNW78*v*Ra z=RZ^Neii)q(Zl&^N6^w1u+r2xZvFKUy}EB7bga^BuolW>K_8pQm(cCTAK#_x=1?-_ zT-hJ)f&{jOWTB89?>LtcWYou>j``*TWt08`2`35&7(qipT5?ql=5y401t!79v6-BBx|` z=QS%*;cdR}w^1y86{m8_oTss`c1bSM=~EOhqeTczphA>|>YWpxb$Y-hRSl)?C^Wu5 zP4bFND7HzYJ4Rkp@gDxexb#X^r7=6Mh1slNuE_(A8_NuzwR23}dV{K%wZ?^6$2pY6 z5jIGoNOt%tZJe1WE&7W(maFpS&CF1GvKU55e)GwsuE$KXHjh#*pYS|N<4Z9^bU4OE z39@>9V99-?om6*fu*|k@r6^)6tgvQyl}nJCa#?(kNs?0W=j*t5@QosR+pi;XS0y5= zmV*ShSlH~QW++UA_&S|B|IiGVde{6EtTlC=r@CPB3IVX!-WN=R>>L~^2Pt$%=AQh@ zW);6m#I|G&F7lK!%O7HUey5q=dY@$%?aL#lrn zf4zR3!{Vu3n9;A-M-($ygrB;r)y!~48uoqujqCPAUGWm|K}qo z@=l=T@HB70;53_A4(jXbo4i`>Dtj8Ick5ZT<6kjkzrJ*aDh~kXbr_xLs!0XUw=N>i zx09Q-Z4JvPj=fo@3C#gLvzSIP)+)mjZ?=Zvvj+_$XOWRjy<9~<5duXtovJR+ig#XA zqw4MfSAJR;qjcawSHCk=A5&H9f+hVe{IFgiKx#I&&<(rh%*$7L!?0NlTdvHPwk^9{ z?Zt0o_uPYH-Lg#CA<*m;^e(N@^uKDH zEhzt95Erw+!liuki-$2uy@}6-_diF+Kct+0Oh5mxk7x4md4buylk@6rZv*^Fs~c9S zvb(FC)7L^zQ4{!@ZU1mA4zZ4EVxu3k$3}5-az?!JBcp&SGKu1|AnydkW_a9JiugyCa+WZ8bc` z7J}GCBCFRMGch_nHl;@3^?YZ2Nh<|#y2X7^-9*8{^GLMsXVvO0)f9lD&#mw07rKCF z=f1*)Aw-z3f&WVQigL@B4x&sz5DTp6TP=>20c7sX7THc?tBo|hqd0*Zk z7~JL>4U|(-+F)D4qUmWIJ{2Et9849M0`xzXSCJ{d9sx6XUIYFL*9Dg2*72u_-KI(Q zq{Al8pDSzb0Rsn0`9#v}O)?H}m4}GJh5oT=7GMCk_A5q5QY%+qxW+*@cC(JYq^jQS zduXex*oZ$^Qtg%x)|OfD0>uXuMEhT-(|`Kw?X3o+y4;~}=>6kWDqmQJ*dH{vY7VF^ zA!CK_WEC{Z)pI@ywkG@Ve6*f)B`c;@r7TI~w=Z&1%%ZO7xk%%A@f78Jfs5#+X8fa4 zTTSysVePUg3Y7Y}rbQ}uCh|{KEuvuliH(?K+0OSSxn#BN00PFJjicMOZd_SpF1`Ph#%Q^((^e+a99f?CpmUF_+M~E8 z)7acH{(bH+?d~3OtYw(>`n!Z2d3rbD@{F-g+^rW~GuB2oQQln>Tj@Kl&LNUdsU7bN zey=!YyI@*weP7!$nuq7o!_10T5{o=+sD==Pgk7hPb=f2&BtjXJG^AC_gsegge&j>@F9(C=)3 z((BI4p=IqUsc~+6%~e@j&f3y`Aj8O+tik8{@{F~AH5=wd!I7oh*3*z2!*WxlOj-Nk ze!2_t_(AKZY5#b7E)&W#<41;Bk_#y zekN`^WI?Tq4+;_xiiiYEqyM2H2jfKAlcycY7w=yUt7j zbI0)QVp9WbdQ|GEzNEUP=MeK2-|ijtb<+mwnSRU9!Ql;WUZh;}P18=v)<$=?lzOWc z&NI8}vpG0T67>b#fvVD}Rl6umiF&;j;Zw`)NVMBwFeiLi6&t6$(ci8$+_iyjO6aNV zvtgZfHxU{XELwsSURZhS^kcr9HO{lN-`8+IspYGEyW>$i*dUeO?{R!L8MK!;UGPpTLuFhHv@WbYT8OA*I%jS}BDAq=sF z#>muEQ{M6^GYg0@eB*fAS##(?&nm_VfN^ppzh$mfSCOYpJFe&#a6BZ=S!PIF9uF?aYCphwML-H3h=2wc3tbuVq8`oBugJ|Et9m0b>*$;Z9unb8ogz<4tcg67o=y zo%K=LeWNmbTb4*Kg9|GI&YENfo>))%z6x3*RwiX`5q586z2kJ5MQlt=j5qYo!2mAP zs&7ixVW6@pvdIzoI7xmiS2oa)U5cJ5(Sz#Ehuxl3UJ zB9a|qzwcC`j7^C$l2PMTiqw-M9jUw5;vDs)0JG#9XC%b1{%9G_YJ1;an}|4hnfdyS zJmR?Z>nJDc<82&jMPX5URbgS_k4G{ZB*}d0oH4!73kGwJ`6`T??^ukZYPwLRRj61- zcWT3oS%bsbt_Tz*lr8(4FFa6Vpc|L3mUi@%FAkdiZ|3tmjg88F>6_B<14(4>s~>s z@t612MW(%X-@sIiu4>+#{o1B?G{B;^=K9d9v3<;}14@`W)8gB7jK{4DB~VD7c59qV z;wO#oS98yr%*(n4)_#r~8d3m1KN!Xuo0fK+WPbFMGjj``+w&+yUN)}GIO;Tc1qsai zyHs=*{aUNXp1m*!oTZ-2=JK@57Y6rU89Gzs>`_?kxvJnA(*UxFH4h-qM6WP%|CyNa6NVaNQBQ|s1E2g4*;1Nw6j2fFj(cQAJ)n+^ zG$&1dWB*hcpN;5SQgM!i#|NTs>=MO36$X;#1|ppvrzVj`i1+M((kk1GyDbU+3 zMk)I(B_Fo=gPG*aZ}=ir2l>JJ{VVhh8yed?k^1!F4gC)6;RSgn;3U}4OJv06zGOcG zi&z0mf}&zy*866^BU@P@lV90^h=}9E+pvUbKX+dGmFwN=k=Gfod(q_3aq`k1*Wdt~ zqiQ&VjQ&y8)1CU%${HcHpQvZ-xJsW0$Rq@45FCB?JvF@^wvyjo^AD$Nm!tQ}{%DbH z<&t2>e7hjIco5FY-)Wa!o(#?eI)BfVVG{p5W}9YbSj#r88>H|GpS+nl2l8MCXp>)4 zaMaFnPt!MdOVkecp?DisuTwTDH~_X36ifrQl}GQ*1wU@I&2gHeIC;6^m;?JoGHFb> z3Kz}bj^R!=PZCM$ZTS(zW_*{4$tt~g-0kv1K&#m?{J8X<+lp^oVz!Mog)>F6ElW5@ zm8xiH$r5i_LBcb3^0{XgPOd2WldE54?$!^}Vvr9PzcRKe&?5KJDBa_CDMG$%^DSu0D@(zAor4D$`5n zXtD>EOyy5{evgX^CO2Dc40ukc?v(wS??1>4jFCH$In2)bx?^~3d?G)14430qzyGY) zYpqdv#1!x95q5f(uC+nL5I^iMAawBAvU`EC8QCfuL{XPF*o_%T&9E|kP+43pdm})~ zw&HVvOkRe?y;~PdiMhk)n_JIfm*$84fK}ZlU(YYfo#9q;If-iU!JYz#lOsP*!CXzg zwE8O3SnPfR?dEg-ak;`!B8U?q;r9=9#cTnMv$0r)Mrw^|G3pqqiVt7e~9`24G`)7 zu8`ILhnv!jRQOXN+DHkLvK@#BYg3~iNYp94AJ!)M$S zRt8OJJyVGFu6hap@k%9>Vt#ZJ&YI8Lq@-GKLq7&bazuMAlmV}g>6m`?S?@D;1Gb0x z+^^!8is){=@((hA+Iok)Yfc~{Dk8lVP71%@ZhR4`tER}zS_QY)MtskJuW?ECE43@D zud`>p{8;!f#mb<|I;$f))&$wf@Z?9Pz0@gGGHF#yd4?tV1It?9MTfr+E13H^FY=}` zF8NI6r;6Lx^Ja=Li7kR;CHK|)&?dG*^#(2z;HyH&bmg9QUVd7nNaTzPhLOltbFK2SeW$vUiI;c-cYyk-`0ri2$ zK{)G*a=q$X&A8rNWyUe%`>x!XAzVN2`q)Ct*!* z<4Ds^t+c=ov^o74E?B-VEIIOPAr-!wqMSPEd7K>Zupn=eMD!RTb1bS}?fu8G?BcHG z%tlNJ;gIYZo0Y!cv?-H}QT$SHNl{)bCQ)X?ROt*y-NHM!xMTtUf(h5uLip)LUYYdQ zE3KAqj^FBz>B{)oMeK((LI!^uHM6l=zKu-l-Em$`T+QC)SgkLMUDA*>EUp^9{)4$^ z6L|2&Z|B2L*!H0R;? zrMNTT{pEL{{Ltk-aAEVGiEqvd9k5EM)8QJP6Z>~aD}0DZu2op$&&=hw}trVVU)_|FbM03LXy(THFBCf6M^L# zgR*k1K6Z}N_oV7ZQbA*gDZmex#-1H5uiBc>30#Elh!xJ2`(FdZ5^%)g= znci%X)|3bXl{dD9qA6E5)3f2(Cp+b>e2VJzYZx1?3OPqaAG?pTa*qZd4YYLoxh80Y8AQ@){Twe zPQK1XkUGwN^L#J?-VH3u%m!=i!^}w#Ej%V4);AY_yo9F!m1&R3$mt&XXhv3Vlj$An zCHz2VSsbigXQEay?-NT1X4ZPW&N&l?{E-#oXE@Wm@xwh0XmjBu&K<}WlQ-fR9uo*o z@Gc5!zU(WRPMk8eG^Q>6W*#W%Gbw#A>;YB@k6t1%C88E{@S=B(hBkb^yc~DogYU~E zjg1-%lk^)ti4_yPv(S0?TdmMCc_2k>Co5TwJCx&k^>^BiAN`ejn%A~$iCP(DAIsX1 zs;D*Bj#t6RJ_1{38vDLw7`?e{9Q4Aeq?7AS>`{8O{?P`ZzQ@W=W=57(LQzBG>cJ*L z$70yG1fGz1mbRcX^dJ@8F=-nlT}iv3#S7jXJcImv?3?3Heai-JP@G)vjlICM@_$}& zOSGiASV&&Vm5};#cZE&XL+`ltD7=c1QHQsCrLW6Rrjq<`g~6Z4*V*y;WNdnlBE9=+ z<+w6jBD4^mX6T|qRNvG{uVd8|ceOCm_42?EFgH)Z9L!i}OODfUk)3zN;5B@`tJETa-_L;A;pLNnpdzO^Wr$~n+ z9Tv6ZNKzKMDh~QbLNOu86h5aH2FZ`l9Gcz0o;yVZ>INcgU>Vx$4nk#qIo*}j5<0jt zt4>I7_CHF?5mamQ&4C{URq^ko6lH^&hZc;{nm1z-vbxg=JmM5q`d507C050W_8pPt zNfYjttift%WhK^*3nzC~AVSp`I|9eab>4;B7I5vk+Kty``*akh># zI9nE93>EFznszm*;pj^1_Q^j$gVcf3Nn>={e+z3K_3|qRuh4{)gjfWF?9C1^0s(w69X&VBVsQ!(BYtvUkK2ovjw!ievX*4WIPgZY ztiVRdnX@aK!Y9j@bQ?xWhhId0oTcFhD6eN-B%jhurNnv0=kMID2h7((P4d&HQ`ff) z`hd4OL#v^a?8oZ05yquOjq@{_SS}NhWbZVNq=rwG8@hN!F`c~XoCir>_uPzQP0A2u z#Rla)#aEM-K`y_*>HHgh&Xq3Aan_$RrnOMyIW{Qt9`@AwURkK9MPcY0uK_{9I;zj4 z=UvgCqnA4-I#lYTX%D*?TzOgoq?oK^a^v}P2dOC*`A6bA`G>);k)f{Y$^Yg8#CMO4 z?X&Le?34|p!keY(^8V!TQd}&=RL(cX9mu;`V0~*^I~&sPd+Z!({t1w|@IMpPi?=Djk@D^EbEkhAT>W2niA4iSM@~S= zJ|Ysq%DT#eNOnXRR5{`++&bo_AEB83!h%ZP)l{t7rhTVg=x0UUv>A-lVINZX?FG&| zS@+nE>6cmPZF!YFh)UVxl<3uz5+ba%<2}@uBV@FfC0OlJlnE|UlvR~u%qK^9)tuNr zkIf`gy)OMw7)bP)eL-=!vtcAh+UP^%>(f}mV8en11`S-E?o$6`bkfS`@ynJ&6ZKv8 z0QM9@m@WlHpLyepB9Vj~6{Nw^`{Cp$v8)N+J;kS;4O@64qIJsbB7?VYsnf1lW06E} zIuk~C;K@=fx{nhc<|9QCFO=k?;-b45#0Gl43dCJJZlw@ZCs$S8ENn^ACn`^RP9gc3 z-Pc{eRU}%9Q}POwRV8>NNpGhJ>tkTveh<*&UGNH0fj5amXU^$APB~8O8><=FiT(=Y z4=s}?h`vs;11rHP0E=1#88|^*kM;ifOw&E3h9sF{{9&oZ@aNispf$9}G2^;PZsxQj z8)rXG40;n@t9ugZuEom1y3993`9XAu;w-6hi^ar1&LQ{ydQP`sA70EHtwUO{o)t$= zh>D{NhcbviTun`FUjqC5-mQDr) zf&S0Vt|`oMCrQQqT=-$XH9*AvlGwpn@Jz>3Z996ZfjuDpxJd6>|E_&d_8ZAhdP|bEzuuQf+ywwb4>k{#89ZLrjMkt{({>j6^Ko-e2DlMiRKWmdtQjgj=rk$qpk7ET9 zX`nydD#b>?&*N6-)*`ag)U2UgwkpjT;=RjibEAeQzhf-VZyv2(f6er7b^fNacPjN^Y{S}rH0J6jlyCaKS%+SpJ;FV@*op&-X9sTo z{?@#R7?uVP^AIzk>p1C?#2C_j#4#|^^e*PcyYrCplbefD`?w6Qdz{1S0<{`Y4Q@{H z+m}BSrQHc2gg~YB+agY}qRw)kJ$6ZJbV5J+bY-kXQi0eUcGToqN?A)$k9KTdK!WZG zDi^mHO&-O8osbCCdj|O?0vf=xGqfJ-Gorspnj}G`!5ysV_)b;KZzxx!RK5_A%Pz;x z#lH{44I_?1r<_h>(Wn=lS}6PHr`V{R@oOACAFO7n0gM+A!!7F#%WL z?L$HrrC5Da>`0j|)`)?`uW^CT-O`Fb{@$4HT?cw4`5tGr%ko9CX(cgGvfX&GGk{={ zVBq{fi8sFdJo}`qs9h)W;&hd%h*{g?~H75(r%f`(_jsje^Eh zY;G8h62yrtF{B&;c*m6SiPy9;p&RiFHlv)Rr=wt+T+^#U7eSP;s$t_7Yc|t*7M`W{ z@srrrD|?Z^)~@H1aw*FQ66jg31)KDsXb&`I%uHF|gmEpEyS&@X48a9h=NpA3F4l~( zP(IKKT$QXR3myp@JaB9O8Dz4@t>jpqian@f}cXc*hIihcW)QcG?j-#@a=l8#U@$+o>e?^l2|mua|v ze@mb`74F^~Zs9-}&``?6-+Tes@?FJ_SGuGeukfNBTVIbml~gMO1|V;Hs|xRd64}ke z?y=Ec3ZAmFafp9jlxkydD`(c`m?szyJTbjq9G#nVpDU0xb5VW&YZFOb5&gKyx@6adT*Th?lvyKpAEX_LB`5(MfB+v2E)9jqO@wtx_8s<-d`OY~sYnB51Ks z6u^t;JIuGi@1lgpM;|gUyD%UX$a$ky(#4!`2Wq8{@I64k0=WnMPDSdn-sLJ4yYZ_I zCj|l>j(Hv#65o~4{Xq#UDPF3t_?(sXX-=W35N8Z$2eeRw@QCXz*ROJlci(gpQz>j9 zkv}HmxWOjd`q}t(@t3$KoR>itrRw9m@-jbI0iNZYwWm@sAxQ=HaIjXJ*l^1hIdrAcdp98m>~m&oHc@ z@xb{ziU41JI)0SWVPATK-7fH7NO5K@SXqwiTcAp2_h#0(WH@;V%#zeUkhk%*T zW`lgGbuh@aPf1TN;EkuH?-Fba-*iji-quGYOF@Ff>bX&lfE&sIIXg@CxcA0Kc72nX zSRL*xV}=w;*5lcrfG-k~9+xfkm7SrM8E#KmZ`t!`>T~4-_q;TYpHq%uy&Dx{pSzlm1Z+ZepXFFar zKDs<5Hik)!` zw{oiF_4Wpi2ML^=&jH(V`{B)FNcv-UCC1!1Wp~hy$6x_A`u&`KtFT1wr77!UVfeZI zWZdr9i_W#)V#<0rR`^FH`=MZs?*+k?IT~O2n2$UQ-#X?9qTln<*H69uMsG+?*>w9i zTl#+>8~m(4pjGQlpKAU)_%&Ex%%lY>3kDlu-$r5H8k#{#aMoyOqTXZhz0){RQ~`z_ z7kNx34SEHG8ime_y9S;cl$g@9!WmfNMNvXmYYT});>&dh5urqixH>L?gLr0EBr<5z z?e^_gm?;dQo?p~VgrrEn7C$Ez`AU4Ich31f;aR8d5jb02?J+i2cS38@k0BAQfa|oX zD5&9vIxVgQH7rG|4xIfO;&4PaKEd2}vKOVR{HQOYoZs*qAUV>P7oB>UO+XB%hZca6ZhU|+dk9K%%raqzYf zf4icuL10*(UyOZD9o+-@6;G2NI$g1pNIP`_q34c?T7<<9y2wkJ=Gv#1dTWaWPRke( z;yos+Dq%i$b8}Oh+0LN40IU$vBOQKSn|tf)Rq>y|>`&5X#or|b=JG+UgRTqy2-{m7 z=E_=E_K5GolLW*|xv_x;El{+?ilxUB3QFyT&FNoYA4sQRU!eB2`URD;H-VM+a&6qQ zq?Vm{FNj;_8-C%^Mu{0BIxJF+?@$X0ACjIPyYm8lB4dw8ExF8HA-->3Fo5DR0toRo z$eB?Mh_MBc_SzL$i*mV14zu=nT;*LhhHyW>6{Ffg^@(>*fEMQi_u1pF-(kDyFh{cF zeeOe-{ghwbBNgCA6J<>3!MUs3*!lkVB38^hIrVUiejcxABl``;r~T@jLSZ#i36K>%I^ALalR#>1Dax zzrfxiltZw71-_7dgWPvMCMSM-#(Sj-q+H1a( zl=9#czSpMHY;e9FgHm9uxu=p*mK|6%*VpVjytvAMAh?Zm@oE<7oj7kVdfjBKC6=NC$H-erRZNrk|?8_IPR~7Fp2oiKe{0{KI_KY|H^{KIK zQ$_%IUYAk2AyD1t{Pfu0?3m}fBdw)L^u!(S)3fspC+5QK`Q;a?g@c*z85Kf%!@D1v4{?>w zWU3zinH=+ZcLtW19@TeXqX6`-)7xt00pX=Y&cV_7X9VWPT(9WD+?*;%Ja6T`SL{H- zHn*wpa_qt6`5;>1U`d~l%)S{{n!ekOs?V?`vPK%i1FCeWYUbL7mA8D!M) zW?6w*I>G5K@=DUS*r$APU2I6Kwz%5mDg8M-H(C4;!VEG50^?=p^tB*Q`OP55R0zEo z0lvKX48jwrzy96GMT6nmvPV~a&J)i_v^t-{O^Z>mql0y=4 zqh1N7?U|YaO~v?m3i9Kk0@hc?(S|3pm4u|r_cDQuj6*_op!klmM@$Juk$6%3tJ^v{ zjrx0h2C0_JL?n{M;BUUK_R?=dcSgLO6?L!eC36?OY82SIm`RH8D)C-UrqU zYvBt`FG6CwY)i*aj%kh~dDYC=CGbuOwKlk>!HZ^NuwC(YDH+LGDHIlYyearnq&Z-` zYNsYw6br&UQ?z+f>&oUTJlL+G?aFiz-V@DfyK!5UN_Ruuh-b)2KV#QPmm;*yjqX;& z%gWAa0ks;;dvc%boPT}=#(r;$3uxa>yOl#f&}P{yhsoFDAzqe7m{AC=U&G}-@E;z( z(L6r&B6)zN*nUJkA%Pg;x_-{jU{5GiZ(HW$%g#JRmlPU`6~cavBMjoEZiv=PaZ7kf zTs+aQN$pvA19ne*wT6!fyCB53mWGHp=zM#Sfx?ze_eLF*I{d zFk(tbKF4l{fZGQ}{O?~j>iA-8m2BocehcuC7iFaM;2~D=A^aNK>wfqpG~^gA=Up>qQ!B z(}3?E8svYGiwTOQd)4!n*JBQBmDSGxb7YkYjI%~9A@{`?cdjya2J8sT0}#0gc?iqg z2Q<{2n8vLD$tt`a9d^cgoD^;p{eBMcTQ>dbyZ87uoyVl#gbklD zxfT`udtTv9@J6*7G-RH{3H{%n;i)FGCQM?itX`yC%RSZ?wU2Oc{RKqcy}y9&i(W|a zjcqb-kDH&{Tf zBxKx)qGjtltbM0yb{Ycq%p6n{WQjT?t;9bURqtcRZHW2?ePosrknDHDalzUH<98v3 z4+oE7mK^OnI@=CJC_NpOw^I66RZpFo*9T_5=u?JQZX=U@9>@pL=v z=4#rC-A$|)FBQPz=UU5rmz6sd-5a4+xCf=4IAyea>7?Pzx$MHYzKFu`GV!tz#6oTt z{!LfuAoq7{od%2Qi$GlDi~2J7&qiDiDASGfyn=IC#d)N8E`#+VQDWTihX(;dY~a^V z+pW>I3C73TeuS4usL%$&^qsGuH%}I~@L9m;vyu(!M%5Ff7~JTKGo&XO7uA%+!SrMs z>kadMg?bdXYJO*k7C8g`#bn>YTo^lbE#Ih%=vEtoSNDRuO-4ji5V1eh-1fEE+3ts}4| z~ZHV^)o%6qsko+m0Ts72t z;p$xZ<3n4H;N;E=XJsW`a&R)O$$DJ!nPil)u}?b2VJ^-pVgA93Arl;%I=b0 z=fg%9n#LL7r1HC-et7Qff;zq51~8(aC9b*;EUrO{o~SYmuo3p8%!dOh#PfQWU4jVA zS{uo{Y-eM7^k|f`alTO>@Vj5qrW9s_3AK!rj)4q|E|@wg<}YBIBCXR-fR6|7O#8>S z#4}9(L?ENv{=Q^}@HTOB@DjqH){!-lmk}Gffyi(y`_Rt;4-K6)%hSd-C1Jk5WhElA zePXr2y7@DR1UaP)QhZ>}QWGg0(|e4t*&` z->HJY`lOTbG4_Ol%LDF1I2H|^6^E^-gxy;Es~0uE&Z)ZCCNC&6xk9d&ne-Z~xz^4M zBktQ5^ZVETxWn7;pwC+D7k%ZtiB@WRAIpAy^3>k*i4- zFES=BoT?)vgcK}*u&ML#0W;0?yDiJTN^*OCY@3FhR8 z3sf1R95z(fBiEZmCkAtnEjdz1H^>l9D9GT$XDx`th|s5~ENR2ZY;|kij*sQ%b+d?1F>{ksPhrg|Y=7ikbXb%xCs>fHK zwec-8L3F)dIq7p%HI=|`5uPz4d)+1XbIQ&FO$v~=3Yy(Mpm3T29(e^0HxwRS*{H(; zYWewLcqOM(8q<6K4Ii*CEJv7IGK%rjz|!R?QLgVwP-=QdqOrTejeJ+5-*2269}DG5 zn3O`BqZ#QFs(wYlmpCkTiV{Q&?40tk&mx8Q9rpc58&f@$H@Ic4=aOT-a%DoCN-J&F zA-AeIHWLpFav{= zLtl00vOBQ-MD1zJ6~sEPX9I>d!(xi=>I$SS`3^Z~t8p<~B$>egfu4c3iCKC+pONVp zBRF%V8(5)Y>1*4`nY#3yk3>Ma^^v{?;Afxofy^tojk!go>^c&dp4tlZ0nyb&iV@8O zv0S_@35wGVAAH& z-J*fV$hf?hS;(+UX4IcO{!;WmfK&hNOZHL@GF^{m!`!ETTp3Sw^m?p6S>Al3^6Ykh z7e#WbCe`~s`BHpWTl(COMOVh^CX>9Knde@`u;{M0rZ$cFz2|AOyENet|E>F+1di}^>Jlut;uh<0DPor{KLS%TJ7qg%8!lVm=EDg%X#M3z+;J4E z+opgIIHrcHO3V{G%C;on!=u{iw9H4z&XeU{G zQXavNq)#9jES&wqM?_)*G8~Eh7^e?SOD4H?Y!5Kp;B}FO%yo}zzhYF3<7X%uzC`^w z14P8BQ-{UkYmnCs0X1eYi4z%FdmWQ^4;ciFE}Ypka+2RL*HZ&iB=4Q>9(0m`K3i7y z5#Gu~h7;~3n6xh`%o>7+OJt9DSlJQIvgKelb3%T#;7w=sop=u$+ekkZoYVcSuL)e0 z)J#Ag>C~FAW%*v}Zz+RKIIg^rWheq3*E>YW*v&y8{`SK^wcI@wr$oNwaW3-=AYx%9 z&7>kzD~`G+yPuBxnYf$S6*L(`H=WbZDyHdjSo{E;3+C0mgUW!0#>Hxvz5Z}iepFqR z$YU8?i@CCbA*@m0To~~gzy>Q0dX-h#%yEHsrOy`_7y^luLnAFG>MUN=m^c-etbdu1 zb+{u!yehDgl~vr8v3aYtOq(my*=Gru2%pjuvtDUm*O3V8(^Kmzm@(lhX-1NKgWjZl zMr=i7b@&zrp+$;StLro1-eF_DV>&ULCHoZ~?ra{KYi<>6s|uT$bP2B*R-U>(#cK3d zw?HVj;`nhaO={hV_ghEi+>JbFj5$XEvm#a2zp#4QpZ8sDBI;zXTfp54MB^KUkqrM0 z9=L>DET6C3Iz3Qu9Fdw$-zvO3I>oE2^gyYAe*QAi>c8@h!&z6`7 zl6Jk;Y+aX4qNG5w8$s^X5-*_j=5M*Trm9w4K!KW^oE?lV?I4fT)gklkyFhU;lFq{g>45eyM|FpN^3ZRm4-0@ty6-bla;(QlZq+@OMEJC&>1DxM+5c z7Yx`ia^CmzU-a~)mkuyyy6bO<{9d5L%C_j&$Y1+?_cz`8Au9jltUb-)Df!cnv4Dyv z(@(~nxyX-@##xH{{StqQOq)c-b+6UBaLBwsc+&8s%V+^96n8aBjnK3K=gJg&udfwx z;8Ku0M3 zwm~K#m=a}x{G)hkouj?CyLix@1Z2_4o7SO5$0VNe9uZx*I*eLSV;||%bX?ic+z{&( zl5lWoRg(WI-M*u;V0C1%y0az-2EaSl|4HS-@|SKp+;uUg<=dVzj(mI z34%mm3@Q_gp-yByapN@uYk`?wGi^709P?q|Ip)s@PGkK?$BwT5)Qayk4h(yH&8PLH zq071Rv**V}u(HfZXVd^!JBgPbHk%L-O5sNX-Wz9-8Lh(qN62RQL6| z<(O%%I(7vWGR6mod};l}h;BE7;hSN)^x?w`*z@2+g{Z#n9|(@gD!RhN$$4I^ zy>t7V79ujDOsCJUfb#K)E%vit#humAJ|l&><*IQT6op(1w53#C-Q4K0!MX)Rt6V&f?fzV27O zpiau%K0@nY0PRP%dDZK+UqNd2f+b20V~%4Ted>;G59VpN-mg6{V8E2#TNH+f&Sc7Z z0}G3LC1xjyxxX@Bv)jZFyxcV<2&-`*{HX_1y(TcTwr?_U*eG+QBL}X&061&J4OabOC`z{?)Y?4Ko6m z-fot;*_x;I>ks44t*-)>!4G>5;omO zA^#he?|#OBTmjy?uInM8`+eKq={*vwMI>XNf%OU;PLgGX)C z+7s9IXs^Sxt6L32Rk~q*3y&Kd1?N9{l0UeTeJX!UH*F35^BujkJTh5OcZ}nlh#piG zWuKHM#?oUZw*8Rpeyxfzw>+CCbQUKoX%by46f?8((M|pIELfEU$4uNM+}iL!lgxy5 z$VqRbm0Vu&fyLNfB~||YQjba#xfk*R_XM?(dPk3{$-b6;2Cx_hF&Y$CnZ(FRxYqD>Sy0zx+W#;+D zX(_P!Wx!OS+V053i^Z-p(dQy)6=BMGtfgH13-q2@4HS>Wr2Ue$=%Nz=ixqePmPovWk;a)HvIDlfu|O$U2Jf+#)be^z3j#7?A~8CZgM2UAQbIM2tc z=uhVyWR_9i%$|k5UkL3&Ce-735L8@vt$%i43bt_>Um}Am01^4-YnL&F&q&G3qJ9X< zHJ6Iz^2q!wGJZm95QnJbtm&y|Rr{OtA&ro0C?p_YTMZd&y;9+Kzq1&}dofD3w@>e# zd%NWY%w}xZ%2%p@uBuVT5Pl{^lKuR-MxI%5hnQyoeOc+9!#o>p~_ zy5*Q1c2y(WlG&KCIU_fJ(dV^)Fh-DUhFUT%D*!ijFl7 zT<*Ja$>mf(L*8nCPmr91Ns$Kq>d%JtZs}FQ;prI)Yyz1IHzDRMO;KO)^D7b7)P_bt z!VGV`1PX5fof~a!i{oqm@Lyg{O!7< z-P`&H@5dq5MXmYTv&Qbz8csty`(Q0?xA`fheNt?$@cR@zlo6dA%T=56oT zw-VJSVhqV4V}Wy&%ztX}gQ>cfe?Jdm^0~f#c$>i=}im<7o9o;LGx{~*d+M%4zr=bjpPZ+|`y+;{!jZ3BSPJENuUL!Ar93W z=pMu=;&hr6a0}Sr&9i~RbPS(@gFT<^N85|aM5emhitdIE+aapZ5d8INpVdsQ(eWYhotl z1T=wAj^D6MyIqr;$=CJi>s#$wY2^uz%2lm4`{IDy`8?!1n_d8Xt{Eux<}OFQOZBK5 z7wbm6AlfIJ6S{$ZpU|^zm7McuPD#9UcGSJT&DdK@CwMd^kZlbvrT&iF*Uh)fU(R#C zcj7D>Tn7K5D(PWh zW@FQl5~N3Jv}do0B8J9XD-FgmZ&=@sq66(_7lu47)3U4g`87kWoK123nUhJ~15}Gx ze$%wzi#wQm-^xp(>=tc?Xt zE$&$W975t6Kogi`$MN}^d@PzW#xni7+8wm7AA7=lOw|q94-waH)kGhAGSQxk_Ma6e z>N9qRiiQqSSfJ&zRL6T4yyt)^i!_c12?C}phO6Wcol8P%c_J@%uuS3Ku@K0#_oGHB zTJ!h0nQEN_NXmB7U2b-2h%wA{$U%M5u``^ni^VChh!mRtb;a=I9`Fwu&*XP~LqWrl$ajsvYy(=}^UYArY>Y$6jUcW5%-Q9wpa!zUkAkj@~~n;YgMI zB75$oDOJ*L`c@<7>yjdQwJf91H$zwB?19LX=s#mz0n^9IW`&^2go0XD zl#a$QN@JuM==Zl}TRw--@$-7bd~w3%sLqu)&oV`*yR@5a<5Dn*%Lp!`17_iw2r*?b81m84Ph>0SX1JYhm_hrf zKgkIF71@q;00nw&4V5tI2~_7lm|6L4v?nsJPK*tm0u9Gyi|<~pl^EzO>nj;jZ!q&J zTQO=XYZ5a`LZAxS!l7}aY|#A7&;d<#2z#>1smt6rStttlLYDo%c5PYuRGadnuCTH3 z#q)k}kjmv!1`KlS(0(@v03gpf>q~(-tk6CoC#?(A<)ly!B6NN*L=RF-(k* zC>mhQWK?(Osr|FE&~WtWV)$)`vXy4r3u--y^qZzmSMo{0IBe;7jq74Cqw4G3$`Z~B zwt!^QO~;iid^fD9Fi@wP8rBWE@m4h5TX?{w*(t-*U(3GRF ziW1gUNOzGlVH6Lp72TJeIA0lx@+1l`Sb4BpRUEHm54hKVVQ?d(d>=@RP>cR3TG^D; zy!$qHw=b}*>PG0Wkguoe_#vDYn`vq|V&hnqI~sgGuy8}Omz`o0a49ibD}6CjdR-YL zKYA`FT0ricBQKxz%7qmK9NCT@A!uQPFScn(E~jnB$lfBssn8pW;19BKcfyApdP00c zV%Vt_k^YLwakxuB$Dv{Q-W^`hun`IbS0ne->#RMPWJ%Gq&l|oK$(6hx-^E`R&gZe| zuNanIeOUbuIWOEnfLqrc67ntYF1(>p`&J9?;etoBE3+MQFo^*+XvUuVjo8wr`CUpx zDIgkcskQ1Rj5^g@#lHG6u)9Sa+9gvUMVA2fpj|rzbU;pf>SW4o=+ws=?af)c8O>Ug zqvnQpb^By1IyXCfVvtmZkJoudh}XqGp_+nTdHgRL5;?o(fr%-k0tOU4eSY=o&*G|k zqRx+8jLuY;1DLxV7`J9eU$t=$ri_caaYDS}!RrN~uzLQTwwzcXuHH56MRxi%Z@jtl z7!hvjwz>1RyT?6cU9qYt)wTs_Z2LSv20j|HSNSo3ysH~?&^G#EX#V$L2M#cBWE506r&C;|^8TqkY{O^HUYU3Z-?(%W*0^#aVE>Pwx5W&F zgRp4;8@eNNJd`P;Ezn=y)x-zmQ9D@6R>*3OaQ~f=uYMzyo)3%HdU!k^S>_}Obv11y z>fyC!EVZd8+@|>}v+K)oN2l-SdMSpDsXFP*SM9m~6X>eW*8NcoiWjrGkqkA;7h=1P z;htaGv$Kqn0CCLq9x0J^{Iv&~xCdzeIGjzqG#r;}QWaq#W3r>>l+oSocPi8f9l@fA&X6hCJ>B<8ur07^@RsBh#0}O6_mB+0h-f=@8 z7vaJ>bn5C{l1SlXgecwe<`n#Q^&@eT$JE?Uu3sveN?EseU*qLgw>561=y23Yo) zhHpD0Ss4fngdZZ9Y}xs!&CBl(z|pB2cTnw!YdxQ3+w<2p)4SG|U_Rnb%IhwwqHq}8 z;Lp(F1>YAyRKaw1h*n;yxab+NbMDw zUXG=@jc3RK@S6TDHplToL<(yul#kTNE&Nd=lUwh+KzCA04Y1?-P2a0PHzRJ2G?vXl z);91;ZTJFoO0hsU9Sh^mFh2QOm8et;lsyohsiO)p56cE(6FxQb>d<6z5#v`JCM=g+ zs)W&EY3*xDB5eGnexoG&IiI}D>N1RMb-dOI+If%%ukMDXjrII3T4vi_$6J-)Q#=2dH zDM_}71aqm|SNc}snsL<)kK|tYs?h9CRV(FjFS+Xs55D;_9owCmv^4}BDW=)ljA6E;5sH5YyIRb9Vlji;EN@4^y{ z6lMX2{6MzCi;7~{fmrGzpbP7S=G+He>&-{+s!doypejST1kpvg)y}7F^#sa~#k*dy znbi4VCZBKbKZ@vffg(Cqj&y5}qRNW=KjoD>^>wXs|7c*YgB(n7zke~*eG3(UqvnMp z9c@(!i>scAKdKVK=~?Ff|3%y(@gT~cDCO1uxb<6n9@W*&vt+EFF!e>MDMh>YYJ4*` zk=(qLn=Rwmt-qf?(_ES+csy{?u(|BZtl#DcS>DPRa&%4&bwuxG9uzwKE@x5Ll74+n1F*K z?LsG&n@3^2H$$USM^VqD*&5W-5w~%Dq_yVws(OmQ16w5q^AIXiSj{a8j>lD8^?74O zRy5h6W?xQoykp23HR|PL8I>WiPxU&r33|N@qr{|e%kb`ds)c7YxK7oPV(KGpSkH0C zdn0wn;uPX;VU=Q_7Ilt#i0TYO$+n;=b|>#B};R(SrS{) z-I+y$eI7qd`QI-X&YBSw1KWL`QqvdX2{>(2PM=fo=jlS_5pV7EJU&+FzP%*!=kK(f~ucO->S6s%VscuR;}%2w!Z z2N+PG7vq3>dMf*O^Ht);>aK}F-q{sEBipK@?lOS0oc8-OfL7(?^jKyRy6WK(`|(GD`o&S6gLKW`hyE?9 z<2*njcdioXa$jDF+kW@%u2&*AGc3NvawDw$-&9rp-w%7XFZbL2#RB}le)2y);X}4D zka>goxrTM@_rM$%vgaQu%fS9FxjIfGa=j|%PXJ&p1}*f#*YocP2XX34w{wH1d%@b6 z^OB(!81~8MlXQF=IUC;eO?hUjP4so+P=IecSu0A zgRb*T;4vO|;~?zMYPm{>>W$O*tnOH#4U>UtEu12j;0#eR)iz=zybLt(Jj!y@&Mih{ zMi^Q=;@=_b8+mvlBFh}b1seV0)(>CvF!(ZlH;e*YJm{jw&Fikc)DRgASn0Km*Y^ka z$5pEe9jI#p@r5`0zq*an`^DHp9^I6~+7Hap_v5B;q#GTYq1E@GZBqlPj|yzZ?uyvV z*or<(`x()6nI!hc1e z6kR!-CDsn@gw{&l>i*g&@)baf+Lf2qhRbdvfTpygUckh)mW`l2`)e_4vg)|WHlA5M zP~QIB>yG1a7DS;|>tglcflAt^fLgAG%n+5qR|BuwN0x$~DJ^T5G?lb@Tm=z}Lxc?+ zM;EiV+1O+(FYH!){p1=F(xO7Nf=w#;u3F!eG!E&9|4d~bhHRmo2@@5gomx@-RsEd` z6R?EDP17T~q;P6hv^VNy?5UlpCf^k~QB&t$?}qUytAym^Pk<7mv+5vQ$a?w_dwAg|0Bw~)MplhJao=KG(3)f`Jn2%;FI6spC4cSQCIAQ zH|JNscYeU6Y5pP|nmYOTzj&ZK@BjrhlyvGnRh!pyvL_n7Enp+wkQ`5S5*)@-W_jlZ zaC3>W9Bgdq^_;BEPdIFE$#u?fxGm@5_#b?LX=E!j4yV27Iy*ICMXU5B3x0&>As~o>;yDw7*m{@M zz)+koMCP;M+JkM|3r7;@-==ewr=u)mS);xo`1}PXycyvnWARMv&*x+`RO;jVukXNT z-JxH5eUp!GQiG)j&n34-FYYCFeO5glnyxF87^A%+5M4v^C|~B!x=H@xMVyyth-$Y@ zJN+?+QmQ&TvZ#et{ItR?hI$5FDu{f97N{KDXfiwnrS{HzTXN&QW?9{vb+g!#3}^PW zkk<0iNaEH5Uigx)hN)#x6`YjCp9Wm;Op&@Rv376s?tHM>!qp!!u$uFkePlxZV}9c# zRNus46KD4P{^MoytqdBAaA$@$QkEsSHFq9~V*)a?IGMG5V&(-@a75#02D;=k9dn!_ z!5hDfxG-~AU>-dn+9g%v!rO6!0^J|6q2Noqmfoj90}&49@!~9xLcrZU^$=!%>4#Xf3g_2JD1ozihUJz1c@QVzShqZRW#Vm%!~7UV6tMdiIc9Yx0rg5`CWO+TSj7Qi z%s-y(jM>U7uQ&CbF|PbJpsH7%_Yu3pIon>(*$&7+#Up+}A=te4-6mA=^aNq^el zw-G+LNnp^`p{s7GC?m`6ywR87XaF|-a^3$PBs2%8BoZ*WP;cW<`Wa|Wr2n1017fyj zI3^4|t>Rx%cv|xJbB7Ngij8*v=n8)@^nF^SAoQlv@C;hu;)joIG%#aO0ozhe8~eag zm&2Vq1=K|4D&^yN#pk0iiG>*T#3!}n3CjEXINQ|;`bc~j;aEj>|CwnQ9dEf>m(F59?f^Jm`he-w&c&lZ{8j`1fFI?3w5PTBTmyWoFScu9NYCvq4pYANz3E=-w~Vd5N>aHt1-KBkmLZ}Y!ml+r zXF&4@`d4aC1EwDXaxb!L;=Y5L9mI}muRU%^neGYkY;Z+38yB9aOBk(Hmz=rV-|X}N zq83WqyoZ(54xviGQX1p@ysV)X8 zXW%F8d1DHrpa`tA!ESxe;DcBv?vKW)EeHqxSJs6Nmb;y6PvVMB7C~sQs%jO&tkKYx z6^5rxcXrQg>uIqg|9OH~H6ZpJj~Mzf628yl*XRunV7eh|dSD$_wt{PKKmjHOYv75b|10 zd8(c@W^cat`VJ3E!5~}7N%WPcJT6Yo3h&RJnMXS9Tar-f*_Zho^sne6D^@i5*TAAb zgQVd+wYl$GgEp3uA(#9mJ))J$ zVaMjgs&_=wa)^%HQJxl_@V#U z6e&tw^Dd+*F|KW`4KQu_bDaF}T6ygKfSydpH;4;+aP7jiVJGRM_e;H_EEUT>$mwJ6 z!v!(qKAU8^VazJ*^hShWXiq6X+J;U z7o1y86yta>k~mp@{wsa%M{5gm`{nl5r_;^M8NV&_nxL&l7vHTJzro*IcRSlGi5cfx z!|flPHXDyN`4{u1gOP?MTWzOvcVRXUn^P@yL)#@z7mwI$ELoS!9u#f1QjGi@?36Wq zbu{Bj$~JYQiJ>1)X?FW}aPx9syo&f&od44wr&-DRNSoJ=%)h%RHuQFTA7fwv`U;0Q zft=gc<8W}uDDQI&$@A^4=wK(|=oz`&a21h*ZU*hzaI39IW{V6((%D|wl9i=m#^mMp zcr%f4U)r(dZ83_AAmST&zjDr==D$|Hy3q^2FaomM>iY4{LQPjR?ZD+Jh!WI zrmHD#*GeQbgzOdMeaKn!0>^?D`+9{Sf@A~})GuVKa$W^&{(?nMPmdRpUahC@0(`$r z(Ms4Q3KMHm8AT$9gt28jJQo@dSS|XH6~B||nC%p##w0U(pzH1YbeKK%!5rI4OF2+bEM1`F*I$iH98c@!Pb>KcXiBm+GoK0 zncm{VM)#57^O92BGnz+)bH_CQ3dwr?P1gSJRnn4+ep_wRmOiu@)k|L2L-q$NHY0qJ zu*5Ivb0MeGEX{(RmizJ>k56F>%WJGbb6n<)`>mEsM{8BG4~>~;<_IUm8S`G-1+G&L zM9Z3YZ_%lx<(%I5baf6ehf^_*pAfz?r@R~9D%zVutwrs6j0Ccu3?Lp|4|(?2a{DUV z?;gLhoFl!4@?N6U*sw*Z3^QP?<{I(fRo77`)M5DJj78<_@uXZ=?+ z_}RQ9xxQH2v5heH16>B>U7p9x7ni(07s2c}F zrN2k=_Ljbm<#xS+Eo{m+$o`e#ITD6#3%bFHAM`Iq&@?jdYEXTZwh50H^U~;!$9CMr z>|B@NjHURh%p1XY(j0iOdd-RVGR|u1Lj{L#KBqv6!+`R4+^nU#8s#A?1?gF6 zjC4HZ&D|GmEZW9mAp^}VA2`mktF`?T{`KuyeV{iw^ve$>F8S3d-8W-edSd)_VmQlI z`Luk|J0vq;Y|b%YPN2tR-y8d7OIgqR>4=e`wfMd~E;m#tVl%i&HUC}C{oc^@tdpQ# zBa6R~N*4R_Mn^}xMH0r5r@1*lWJ>0W&P%;!Bwd_GwjS%rl}TTLd7G`B6rRFPSe(5T zmz(+;){cvB##S7i??hG2eQP@XX<}0gB0TApS!6Yl2~E>cqV9B>Y_-$B)9#h;pdy-N8 z@!@36c&F>Fpx>jR-{2n~xdu4AT%H>RL{qO@fjeVmOO0l`0KHAP1QS%SEJ0p#EId!> zdsnpP)#syCXBZN+r+(}%F&FH<*<4R~c5)JT!GqXp8r7;aGAlXmd&>5@myY~1QtxY* z%m497pgR&FZ_nI&UwFAZ$`2|`f;nMSJp6A}%R19kgq&`CEaSeS#+4r4k`Up|{CmK{V-Z}g=JmvNWDDl?RR%U{O*aA-3~_$;{_tG^6u(WR@#d_V~m84F`tK{eYi zRx;4`-*-xnLhxk36+Q?-B~jx7>Wr4azt|CWNf<}E<9w@3Cc5fy0%;7NLy|%m_F9i- zgA$mfzrxi*OTUwjlSmm4q;Xfrz+VxEF1(@BuQ~(!PZp-uTSqC$#4OvUr{Gh#Y!%nS z$r4a5m%MN3q8Zc0TUouuYa(vXqPvD{J@-%lbh*6TVF~==hc(*ya32l~v0y34Nud4X zPTfiK*VN%f!ST{QTi5Km86}M7lKzhs+nUv3$c%6EUa9NL#oDKvA>J%(57Hn$0SO6C z>!*4PckiUDPdCk(GPdh4OpL?Z>L1e3RAu)$<(01uS-U<8@!Se^D;-R5ka($Q+uk*< zSh2~e=M{>$SGV5UzvMe7`(a-$)v#n%zJ4pAZ291~-_u8NN))S(vw@*BB2&um%U2Fq z{N7k@_}$8Z>Gih1T#Fjp&1E^5kxDgOV_kdpI!ejgU~oq%F>cKCV~+mGhHc}zV>k3L zPU(Rm;Wf*TQ|86_)ZzL~rICc4BxT}6c5A_?qpw2$LXyKaNwY8@+ZT_;82G>J1;aI0 zOe;#d-EXp0oYwRb{l?ede-}$^K@pn9>({2X*gqz{jXisIxaZjXG}IVQHZe|Y zuef#O!++wwVmT)gRb&_HwqDY)$Mv7~S^r->2y;`vN0_Up8b4%nJ$lO#-4E@{)7Uk% z_e2!RPHX(qUvxVHBIN8b`;mrH0;95@1Dyy^i!*uH9@!Z0oF^n!C92sH72 z;0R{Fdu+J;;*53&9N7q|>a*SgM)|fF(oIBpl!abT^+W#s&(?kCdYdKCM&$XLWes4w zI{fI(-S1SL729xV6dFWei@X%u@)dr>z4XP}n?^QtZ&2oz-|wwAezTeB0@?mHTtO#@ zY1T`Gnt^vSg2AiD0qw5SQ8c^S8_ioyf)chNRSt#9K#mS*ORg!E%Xh1U&A-ghffHpI zYufx12kqD2x^G%W-6E6kMq)_nlF?5S0{~~+&X}{Lmbcu|cA>UsH$Ip4+q-VgoUQb~ ztJ-qaHR4fN9!&mv}$R$Xaq-ptG)9Y>NTOQ9H z{;f`E>|6fK^5ft*REB?Tr^Tu?L4RK1z_IXPe){~3BkNo7TCozzq(6`==vjT?hJvNZ zouv2c@8YIjca|o}kdj_3&kCN6@Mah29CyjmDr|SeD?e8oB-vuYqgW}AZ13F`sfzt7 zO(oHOSH*I8!+2_d45@h|a)4Ha>%c9!gXH&XaP34bOvZcH0Q}J;r?;4IYQyVOJ-_!% z+17B05RC{=tKjt(A4@-1>Xao?QjlWCRMh{qLqd0q@dD+*rN{n9lJoDkKi%8U@8q4G ztW{{SMS6(jit;{2?ce0E`0>$w?%EEcG{qEMk4^&#NZhZ$&*QGqfWCu~XCvg6f=&^> zbh_U?XMk()=kh)wmY2|%!u9d#M`rLG#-2fl0=fSHwS;+jo`Xn5J26n$oM1`g`+hUA z%v~W~IJxw0eW2SB+GgX!;U~oqiM2JJ4R1w>g)fjr;JRPU$GOlT3p7%3Z$PSyn;|Y0 z6F8i$__1oGPj%IQH3O6Qr>;n;+2(|vIHkm>-_$@-&Kz#d&CTbu6|Gbp>exQAOtZA) z8inP~bWO8@=W*q=@%7#vO|KjYM~^uOcgOSp@~&gAiTBhQXrKr%?dt_275*&CnoXe2 zk|J^$$F7kLGLBiiu^g<~ls%;p8uv-{?R1RYe_N3h|E~W069sr+=#Bh1P__&m5I_dF zfTQk4Qp%>Mk*;$>xw3-hRF3<^BtK`F9&|_tX-<__l~EivpK5?ta`6{NXp7h1%zYrLpHeKic!qhdIqkbA{U|}yC z5;B_OzFV^(ocWKyqYIWatNI{jdKo)CVD$G- zU_MgX#qxZ_%|l5GKiO12Q2uO^wkdYZ3GuZ+>#Lgq3NM=RkvVPU^HDp4RK!(6pyJ#9 zcXD{okVRl1cK_P0M3@8ak1o@f02acD2RPExh|piKL4T-7-bNr|7r1@C-0)UoFkQf*Kj>jYha0Cp#Xt`3p_4>u1=(AD@}IC>2vo$ zK1YK?8$Qd~w6%4u4RCS^V*8W;NMe%NGI#`#5Zq8c+wL;>Emj3T?hS+U`i^3GGpQp( z0l3RI=GK-l`>w>1KEZrfe8J|mxXq(ck7CJ=EYsSYmW7ac_@FGky{W3Yl3~i!xwC~(Y^J|6H8d3tcKeVM{Kmy!BD<7gsXO>4M{?OtJa7c!CtxY= zCLq=v-OS_uP=T`kTGW4Q0a(FnFRLUhR;R%i!2uax6MWQsMr&iN#KK!j{hX(YZ90UW z*vI1xoo&)O+t(~Uv-O#Tv#Pc+j=AunM?p@z*-Bq&b_#dD1Bg!TB5kBu5k;xfD-K(v z)rLcoZFzTOL?1kn!%2?W0vA)cJh%OT#=h*u111R#-He9OuHjltNWzicXV&J!OEWPK z{slBW1N}~!As@M4ut(R^~NmL^^QndUqJztrOQRt=BU({w$PsqLAg<`J=gw zip2IH?eilOR*^2J-&5~$PH!YMvmYItOnQz4W=W187>#+?7q}#^9?P1W*I4znoE)y} zFUau0dwUcjKMMg^_@5`-{L&@r@bsBkx^d3hxv|WCP4}BK1rrxyd2iNc94^&&{3JlG zAW1L?vQRj@v=)?IGonp}w2E-_!4P+@$&hGYxa_VcWG%l+ji8{dcii6BBRw5SRdl)K z;>@#|kY9n@d)7+am~9qZDN$_?H0Pw9YNtG3+L(5())#LtI(NDLe(;3&9#xrfDUMq_ zVR%dU&0I?~Db~}2thA7-c_G`QB>lf1;eY;&U10#cP;dREz6%{qBgA7D`-O_YxZBS7 z!T^*3@sj`R_hb164f91LWj1;UaQyWvHaDgyVf)FzNj0Bd-29lm^Ws0JDp%6qVi*q|9&HZI^N!8hDhWqzEH2|6+9e zg9A$8k&J4F2iu(Vay{9d{=_3B>0^ShWG89}8kgtZ^Stu<$_<}1Hy>IDjckoKjo*|C zhQdyZpR82?jcI&1U1D2|n`|f7rXYvr!(US#^P$Hd=w5ZlxU4p~gi49_P%%4u@rQuC zX_Vf70o&$#9Pex)NkMB#SzGFFH7sk>!f%T%s9|qxjB4f|Zp_HHe12YMWmymmUoUOCjfy7(63uG`f}b%YA?y7;KHjIk%fC#NpetZ?nQgyZW`47|EN z(=^T1>nUL&N%>i%v?*TB>w_=AM+9 zWsZxr_?_swctzzku~pmF-nN%J&%Wnak4t`lv;mcNYvS7Cr_?I^Y*A3x=o(O0O_}4y z7E*)KTOAZ_tFCQr${KR^3WbO=bm{j>--u3NJIZaX5+?m9KGg*`Lo8y3S%%fk#@H6B z+GN7_QkBN1wnmi<2$R;O8-u9(oPM0z8?TnV2wnU$M}&wQ)BWD13MBJIf!t;lv-FA+r-0FGXTwwHLxVy`8g-ULPXO<|e-mWnPWS3;3H5zMLglp>mdLXhEINyb4wG=` zcE)@kQPOQdEuqwzaGu(B0hr!MzFY~fkj zRxj1i%2t=e8D-eMaK*!VwB)t$HSQs`&Ge9M@O2==RrUbyc^ioT>kaz(b z)iFA<2R1$9&O=Ww3V8Ra7&zy<)KbvS(?fWC(hhgdPl}_hZ~`X?-{4J*5y5p?)o%KESo30@>d;2y>Ct%voniuxFH6HxcyF`=P#ThhrcL-==YjJ&afuCZx zx}uv7DRK~46tj(jogJ#(UwyN8n$-L^!GI$S6_~#HFf8z?#U||^= z^-$I9F~iE|2Qp@G8|I1SJ52X02$~INc->1?SWIiNdaa*vg8-IpVbER$7VZaTk)(42 zsT<@Wa%xmvW)Dcy52g#{jI!i*>+Zfp5DGE3#||>9MxM>nU$uDQK0466$;9#3;N9n@ z_`v;7{-B?bUklmsDW0G>N*-fFjEOD}32?eDKBJv2EfPBB5v4p~0q@wk;(4XVrAQup zZFRixOyq7x*0jbDBHqXr6QbKyMFlTu!BjZTB$wcy(2%F-twbDiZ#;~-aC@%8t&(5T zq36|Ee$?f?M`yTGtcbCp`HDSnSiGDV%Lv6np4xs^u)`zF$&U1tHm)fWe8iV-J)9Nu zn?BoT&Q6({{ZM8RNksgc*)jr?*@nLpY<0E2I`ZYLemCQTxaDHj238gGO$c+CElBRG z`Alo3%Womu5Q*gMizyqh|A7CUdeC4oj&*HCY_|E{z>!w>LbJ`68YQ>Zz&NbbvRR7q zYNwPWk<{}*taUVdM0YvKZo4#X*~O=QH(-3TU)fRQKTqrbrc*0hDQ}W~SnN#Q$=i_U z#f8-%deFBacB&_EIloOa28EsZyWO^wBiTWljRxNS*8|v91@SJFbdqhG>A>F48M& z*&CAI^VA?70r$6%W5fZsn0bOxc0XhAiaKX)mStmc%Dzs6pYJ7!JvLEJ-rz zuHn4#=YWBWJu4Md-@bx9bK5DG3S$(fZw@3>y+>AKx?%-z7Dot%u8o%lAk`H9Il%0gb!YVz3D0*txc)BFCyiz8*_-u+2;2%h9jtO zHK^S}r*yLd?(~w7FE{p*jOYv|!5P$Ea^1*yzO^h*5AnnAhL&#jfDOlKASB`Mkyf$T*npO(U%{lFyrlK;B>6ID88VMf(Pj)Dx*f9eLzO-3vBUzR-$z6D=9iT&3$c`TEgn~+KdFPA=I>#L&{yFK58 z17FUgH`D5Nz5*-2S)ph=GFTDm%8$gNmXQkVOO5*31-g5#qR77@o0KXRCpm^Of(kzd zYD}H8zkhGlB+MNS^1wy^@2^1q&nw{coZUC3iMb%#as$T*y_pXHx{y`XQgxNH7p^oA z7HMdaP{+JD`(+&ynht#e`+Ttw`GTTt>_ZV%3A`gmC5+05FVrmVg|MXP><14ljT7hP zDg#BkXv#G}V^n4aT;Ub{F<;4F%!vPg`f_+$0}gw4d73piGp&3X5)!teOIp0*qV0%Q$-b0HtE ze!(8$L%d#R`J|cUmY3oT$sGmvleQD+36@LKX(aWR8gE)uU-wKTSKviN=!3={nm;IA z#*4@q_6}xY!}Rl?)se7204U*VktzO}8S{PU#`mt${WJpI8iLPQ8AEE_?GvV6M3#n4 zIgp3P=iKQ+XC3o6xN1C*b*`|H@Qed#ibh9Y*PhS+vN`^(V8Jj;B(B z*JiVhKs#017*h~!@|H9$P%T1zl)0mgVR(@AT!gZ;+rAz#&Y z3xt8fGM+~I6zei6JV{}dp}=pMVM!j1f7)F>y6$JJT-v^FTB_x5M_NfCefMcw0cZRE zYs~l;C-pz!S9$B|=(2g{g<%##;JNK>g|z|j(ofc>=g5l+(WS`zJfbovJNmowJ-r;J zz0&fnf(9Sc_sJ3eZkPTKWAemh0N&aN=bnoZq`{HsA|M{Q8doCE#4h9X^38*ly~ONH zC=2Oekhgi|UQ)bzsGLGVC=+VY!}NtOa2k~;a`}@1&CH0*(FZjJ=9nA0Vrqt?h)T4t zjz!=~0e_mN%J$)ysr%q8;FQ6?r7C;F37jj{jZEc%C*FpbQIXZfQ)e2y5$+VDx$yoL z{+Zx_bhx}T4M{@O`B-5!W{Dg!dMdCg?F)`+hN{-6MNrbu_WlrwLX%w*;D1@+w-VObbTRCdWi*) zHDK~aIe+H~i zRNdjiC;42b>(~46sLl%zK>Q&85HIKXf&WHje1>@GWJ&S0R0YH)+7WcG6A%|19|C(9 zy>?~EueMeE5wCU8tM;_>CK&~l%cJM@eKeDEDN>YF)}Z^)BlXey+4>m1op^<0Im|G~ zOOk&c_A9jKW#?>H>-yZV!rsF;`HInj!H5yRc>xOGoao z%CCTF;B>~1J?m`i@5(QAb9BC1gp6Uqoj>Jli0Fqza=f8qc4Gx-mI zpoz&p5x#yrJ2I(%(TXMqQkcvALbburJ{UWLzORE;_}=9Lm``5Nr9(ncx!eh=$IPBj z@&gEW0oSD0PZ?HBsLs__d;I}G*Ne%RLsD!LFtOy0%Oc7?jYh;~h17}t2Mo-iRIv2Y@Eo7cf9cTGY>E zb>kuCaqq%z-q5i(B=-J#MB{5fuzc~?ej4){Z?c@%)lcSwBemKI4@GUHSFzVJX>F69 zf_@p(WJ>z6B!lscK4XS<3Hm1*iM-m_oPn?dwQ1-Kepcw~!$dlPWB4|198-a*z=|7d z>-WIhpvr2Y(Up}{;xS@-;mo!+Z!|`6ZS*UAAx~Iuo<~Ycc6;QO=YHwt4x(`5`JDGf zsj~}9I`%ufyW2p;MhM(ZZ1i?>X7PN?d=X04yzaiuGdv0VGwQIY<=)q70qCrqFGWYT zXPcPOq@WCj=^B9!K>m*aw4m72?tlqg4|gQRpk$q}DqyIm?0x$Jm$F)j7vVe3|s@p11hUo{iC ztN(X?oKq9v$7!yw8)O9UHET{z`sOo&7_$C<4N<|#zC|BPjPBoFS`o5<;MInyX@ zM}Lz0Oim<26^v>A)Z7F8D&^d5=hKGjJ-%DYatibWOOgKEG;xKuyu_T_xU6|3dpRDh zb0O?R>X1F3Ujxp#R(6*AhPL2FZXfbQ(4jgvLMum~pHNr$_pid8cUg#-j~HPbYk5q5 zGTRl5v7~6Xb9_Wo>}{OquDDD10lPK(xmszD0?K;+K2rZn6gqJ&We#XC=4m)&znt`C zMM*cN*7eDz+NpfH8v6 zrn3naoZe})dbf`Yt_8*jOC~iLqSrDR+~@=jC0B(=JlVfG%_eKAmA{DD^7jf(M_<5X z&>31xQfE-hMY$#up}qx6frkoIE)j}}q1EGjA6dnMp4r8D#32~&0LXWr_BEiW{7o8l zwIl=DzjIqaG$gOUzBsuE&_%LHQPZ5oG3AGPBxNHuAg!ExU8xf0H7#S zbIyRpN*mJe3B8HSpo5r^$D)3tUymqV9@969IuqtVzxsYO5LTsYmB11oR0OJ$FrXUO z4}(Ihu@`W9FT4aP2dwVLoQdT=pz#Hoj)7X{9Wa?x586LD2ty84imSxbvs&Wqx9wf= zD+g=irq>^MMP^An0YT@ITCU%arp-jspubYMgoVFN41zNADqCrD>GH%CN)Rk}EBZ)3 z*#aP*j&2{&B zx_|Q;{!uIg0LVnoA69tbr`v~jhVM*!624rYH0OOx`5FBflB#MGnJ>a;BSvnB*n~#r zY2-%UOAP;g4RS+Q-r3HX^0TaXH1FE}%D25vS-NO-e*<;L+B79r7_aMSd>G6hwM_@t ze|KXXT6`BtJiA5!e0(6ou^cp4jd{aHEs^IwuNWeO&?XWT&OI4&UKzBLCU!447}XwX zsN9Pt7mf^o*Iv;(L3PA?8XB7ef2v-Z+di~lxmLIH z6K~S}Vh`ZLaQfJ25IZjDa2!46xgKAMA*j_Nv|iIqRwbeU!dPt^*Y0%5K@jt~>dH!9 z8%nUcfl{^vA==2iV`h)#$WUHOU>Q4qZsstAXZQKTJ9rhQ%klaZu6*E{9XubMjyOwd z4LfsaP1YXWh7Mz8)Iu|5b_71~$QG3E*}7650x;8v^NH0%?9~`YCO?N(hcX_r{B0ju zyK`Pz=!Mc<A;dCecG!`964<5exAi4>$CbqcsZ?qWLR2=zlzp;{(G$h7WQ}58N>eJEYR%YkC_~;KzCY{in$wNBqzqcX z{70o;BVd07zU+1Tg+0exm?YwzrJW%MHg8*+=GO?b8~E;DPkoQKe*uE+Rsh)3nuuI4 z?jxZC)pw&<;;PgdBJ+zid!c|NY27KhD@cEUqNeXMD@$Z33wYsCnBd~+R=c}5h2miP ze`^6Wo=4HeWd(Q?qAx{y#4^SoHY!DWKCxyc4K-kzTMx!e|0tE3zf&HtDC?F(2$uBX z(e$Tyq*=6xi)Ft;$Q+QK_ zbO}ZJO8ITVrUho5oab;| z{E*Q<<&<$u<4|G=&B5*0kW|$br+YcP^dpvpb@$hO`l`Y1P0f+ojr)Tv-rykWJsxDy zFD3$*tfOg;-te${J!#wix=!^CC<*qQ#gjkav9X6+egP$0Nq(}CLpzLU1US?h{z)K{ zRJk}Ii*?OQYqQ!4JQ4*JC_bm04E6ZFP>UmZ8wNp$C=H2g9^qgNVqVOK;9`6Xhx`Jj<|cx#Vz)qnPL$>H)|DJMqtk?P;Q zl7E!Ht?%S zZ=G3}HR6xPz|lH^`#Q#C3a)S?lkIB_AS-&UL)WivMqxV-!WpMs8M~CWt$vnXU3w-~ zb?;%+7m?j|)N~<8n4Y@ZPCOPcacG3sA_jfBRsF=oUn7#||V& z2z)Gx(cw+qXMcCo<<1``_N|)X$0=ZZFdn%LNaz7u1UbhS(H=h)Bh8BGbuv<P?JqBJR*F9SnLET?hyM%X{%0xpe7zgUEm~vh=6s>sD9U(XTt`(x^-83@qd~xF;}~k` z>Uuy${W1I@O%=Ph#@#!-q>``7ODq>n-?%TT#VtZSCB&%M3BiMBG zbC4jHG=+9at$5Ws)d7q9m~_y^J-c|u?tsz}Mz1@`ieAi0lv{-{z@Q*m&3hyC|Is1U&^js;*!zBnzxV^keV0$J5>b5DKL()=jfv zlW?~*lEgEwXgP^`poeOZ6s7WxHA~ClK+JwpUdzdn8(}^bBA|mI%DG-0FwB&7-9j6hP^!icD{${R(Al4F6YgDz<-3!K>Y%W@s^i#zA@!z zx^Nwy7l5qlgNaIFlp2)|lQhNJG27*~1L6T!yMd@R>m`<|W#R=YGP{6oJwT(57bWa?Ec0 zqS!BJq7K~!4Pta~qQobPDnqb!Am4hABoD-MpYzl_ z04%33`QpVFv{ZZnD%&TmO`<0&eOg#mYjrQ00wbDC zz49t6Sp7G`A_YE>vsRQT@AS3^*}H$>J=ZeH%5jJ8V*=N8?4I4e~ipWs7<^m=)OV9+a;O$oM+ z7c3=|dhWJZu!S0%Q~R1u}H7d!<+-` z3Ev#l+HSKuAxmMtiISa_ddER@w6v%e7g%chE2uHi#W+Qfk`F|ZpnVYx1Ka`h#<}Hi zZJi+)lKlqSw3I-3VI@M9Xg~m;S6T4HPXo};j|yD?G&oJ}9|bXVzC4`_(wp#>x^uX? zE-{Kn_NbTZf4aG**s0uL2;z3(-PluF!?4w_t8_=1y_+e$6*8+mpe#Nl=Gg$?%2&D!Xj@qmtvUg(dY!5pME;`qV_ z@kx^QM9XsYOkRvvUwCN|Lt%>_(}@ze zIEoc+=}I~0Ka0=Vc}$l8nN)qLe!YZI%?(4+;c?Ehq}qd(XLL^7>`pLb(UB@|q00w+ zU|uT(RNw#DnN8rHi@ksjZK}ZI9uqwj7vAPjeu9$n0Lu7jDlj#XKS%t4E&%A;gm@3- z1on**0X(nF`y9K%LVZ~U3W#f$5=IjzFn*xVeB%tvQ>f#ltW)T9HH!dX=RBZj9*0-4 z^Fm$3wPL5~I_+D)D1s{9sQ#lX%x9GDjxSc0nkD_=15;* zWJ#W5Y_O-Hoct};so6ta!bt80de(V2S!)%cxWeG{wA*cTzf1O)`gBPx?Jw}W0@YRt z2k=OqIs(mjJxGH;N}M|fyC`luW3O#(X^p&hl1`{0Lcj6m82gF8jWYI%Ive7ccmNeI zNKD1-NacvzW+Qf?#FAa*-OC1)htLb_eWc$4L59HFya14LrJN_LDI^Hd)>W)!yOWec zvUdgcxHS5*k9*kNYdg6veq>aV`l$jLpD^U{hz#-Xs?xN!#=W-galF5z$~j^;69Els z(aU*tDE$BHDgShum3vo3l25-+iY1>d%AE{-SiHvfk7DufKh*mwAXjLtYPFS$L&aU( zR?_8jR&%24g%V*?QHTV#7`+!r?0Y(dIS7g8&Wn^fSKlOc%`F0@^tV2U3o~e;p zI=lo$mMIKv_@frj@^z8d&%O~b2EgMAdED*`fzEBr_isoaL3HvQ2K0%{7mdkXyRZH- zo{#*U(AiQ#S*m#nvs0VxztehNK_}KLVp4q zSO^R>;D<(Wm9YrPYdks+gJUGNgkfWk$$jv=s<0mQ>y}IXX2wbS)%U@MdEZ@ny%KDmmfA9{M^ieyYRQtE?8#Eg!&e6)L@Bwe`004)juiLnuh1 z^X<@^Dh(yXn+DyQvEnl^)nOr0ze#bY8r~nsq+XKq%Fbit0Wb&{Ln?TfMgryakzK*u z28iAXW>!mp@5?@3dgXoxP{fB4C%_wS3dd{#&Ge7nL&GEF9;AnC`5(>(s~K-vE_xcqG}{!cjjUcI=8Z07y) zN^X$DrX+e2K-D@0)l5Kg5O39aMhsVV31S+^IBH40CCAqYj7L1-urHCa*Wa;RHohS# zX=iXIpR3|*Vb<)oH9O|6rn4d@DSnot&oqbh-pmL&#nm9yvzXd}qG_~&Ac%vA!UD3G)# zHjCRAgMxo(z|spk;Y9xQ1B42V(S9NM;Aa-4E@F`Q{J>>c=1e+j+^y$uSyLg!Kp@vQW zEYl><=`Vn7H2tMygr;SjSiE}({EtS0!D6}hma;hBI<(b3upWjm>!DaH+J*}U&q`~> z{ayyCkb25W zIve#(^}%tFla9aomB@Qk7nwSx@-0!Nz|jiwJdF#HXev*p+;$;$wT$#Tm|vdeRGon8 zknW*AE7x9JMzQc~7%H+dE<9LdSKN6@LqS7jmPv(m9CF$edLAe|P^LMK&QsUbNbsk= zdP7>W4mHdomRo}O^qZl`P&3~f+fU~(LXmsRc|u%3mRmS(G%?E`l9NXlEp**Z0h2Gm zujVCa7FU=Ueod5K!+AbsYecL9)z^c^fWCnISl-h~<=a!X6=GiUMWcr!*{ez=mCA=- zqxu1n7}J}PE4I&t1EQ@&BJW_YH%OKvD2iB-ydK@BU*5sWxJeNYWIOVE!ZP&3jJ?vc zCjMGP;skb=0Y!w?8GOb~Ia$tm9?Z&lZ7mDcAu595eqT8zS@)v{EalOwFN5O)IyV^VZVbO=e}96PcA2sTnFNDp{G9bCc#o zsf{YmA|hbsR4QbdID%7PBA}umBJjKE?0)aF&vV-6?6c2)Kkpyszrvfvy4Skab$zet zyG9d-?hn{+QGTap*2W#)XS*$(KW_>)lWK1LvTZ71JT-GJX5I+AbHoP7j~4P;Nj|DR zTGMTP&m5L#3U2Qzr|q=9e0JTm7Tx@#W#16&oy&yup?9j(tdL+Ji-$fY4$~) z`+O%?+^n-1<^?b>0=?1so0c42vsR81A1^vXh}0HWh)TS=vpGnYHl#MKiLURF7dgAh zDrg^fT z&KeEImzRaKind9AZZ$0k?wTSbEGC{z&V(sQ99KC(U2%e{_E;^*u`czQ0%+z@Pv@3v z<G2>ZoC0&inW7K20r$;MW`r=!Y-KI@&PDk1S0AfPV zwF6bEb#5*rWV}-F1)#G$nED{3%260spE7MMw?CzbF}ZnGRdw5RYfbA?xAP47`?aBu zS4^M7YdxPz2urgJrx&9x0FBFc52S(&fp7OO9+pH0DI2;n>7bkgSw6f zI#-nT^KOl9+m59RcR!?>fHtSP02W_G;Ve*(v{PQ=-HB_JgfiS#>#O@${gRlbb=xYu zb@AZWo+*g+Zp8+v5#2cXBu61hOd)ZA#f_LO}(<%AGS{nO$VkP8lU4WMlmgQ;M z=`{wVz0*ofI3o{`34q>g0&c}g?pc+lG8(r+u&LFqmD>j{soD_Zx*uGr`ipY8EfWw& z^BJ4VV-NsQ>dCH{(7TtfxsV*08U_TmwjAh4m_Du|ySD5BaC>k`(DhwKtvPxUMUuv2 zLDDVTuD+BsXa15~Z*X?sxhs`1jcIns{flusQ{1#yT13uIO#-ARkZDaw@r+G@-$1(E zqV~W|#g7)Z!q@M0h;&p_HBM|j-E5otcKtNqWJ$l*Hr}_vRVr;N@yXf%1@+yoeRhiu z8qVxM=8Fp&?^aF#Vfwu~cMkocq2NkAwmJHp^1`#h1Z5+FEZ4rEC90KoTxCVOLX{Ul z+Hz;@ux&@=(3+)WW6^r5#xAWQdV<;cEA2J_Y`ms>)?)11yEm;O@9-}VZF!nex}wYE z^OUpX_WXl~c+djK?3>vRFtuGlV~Mri(3Jg8TGSqxOK2DO;=mtu$K2egtN!U(k|OSl<4`j zQ{i70+4|(oK!B;`HizI;^!}l|!?fL#M>qD$tUM{By>-7kkO|6nIep^RQO6v4Q*n(F zOq#w;dMXwBY`?riQsU}f4S})GeC9LNtc~&Qm(C7ZG(LU zici(q2QG#HJfd69?h8|TqhX?riSG|{(v&WHIV}H5k)5zVX+tyGaCccNO8&ww)4eC+ zj3G4erTgaBY%f#Wxu2W>mwxFk<-BTXdWJITMC$fayOe`|S@U7}HKU&6I?EGwD*WR4 zQ1{d{sC@5QxzJoktIMjcf2qS-npjCSEJEV(wwUDRo(1ny&So#eY>Q&u8-ql zYMZkw4^$A}n1Dl@&N!SsCG~L5Y9JMF|I=x3b7E^!hV$F4V5v0$wb#BJRP*TGv{=}hY_?m} zDy`#F;Gm?OrasdlO&f}HQ$XuX$A&aM=>kfPkE!fbmhrtFSn$NNE$n!!#ks4F!>MoW zS7k&!ySw!rw(&-_&iMjCD>K8QP6wj9M&Z%%`zetruvcj1)9NPo9Gidk@eBddlW{+< zzq18=*YdWF*=l2^mh0(#Ax@j+(&)}LKq#OFf!zv|Pt-n=T-Oo9cp(gZoh$4(CJ$cX`DRj*{0;Xt*gV=`xopewO^)m z+I=jj^3IsdO67ZRDpN9&!Z(r4VVo=LPP(jA<8HKxj^qGjV|&8A3nIG`b#~VRvHjU< z+^6NfqfzJLx>`S`lAd*;*;u{IeKq#H@%)s`QiI*b1?L`{ny{bwi!>%mtC(^j8QQ`D?Qad zj#{vDR0ar_muGyh1=zs0eW0{*qwk$%$-uSfd1jZ-B`TD^**;Pmzxpv;_h#CGhKeD>nV(`e9@RpqcR`eHH5ejf3l2fw%h8gozD*Yt`@7wJAQ=hOML0IosI_R6&KKQp}-xpE80`{I*bEJ z_Nnu%U+$!)WeQ$UZ53ZPJV&m)Ci9DCo|W7F>vvY&6KoA{wtiEiGkn+*tr0q=(Qxqc zjP@>F(r%l}=dLSQHEf~BWbUazR2ZF%(Kpi^-W*xDY&YHcD@AF3$s4V|Qj7MS7uQ>t zU#GnKdMeGMAR>PF^#|Fs69w+lPvs9e%ZJ<7rBCa0-d!)J>ay&7d|v8Nt=7CvM*B|2 z#wWpm3Zsv)XXpsgqM!b34(_GGqc4g<>gD?HKRMZ0r9bIBaL&K0G8~{P-9oW@uEwMSL zYTxFKs+)G3P!pb{OzX_p{e1gmO6IO-a$|vx6UXOG*+0LJJ#3I2-y{_9vu-h3({zrg zocfad*l$Abv#jyi{e0>AZQ``s7_?nVRYyu=Ou9@*d0dv~Hs&ASp(3}|`rPG*J`Y8m z7anfESE!JtU@?-hF%L*-f6c0;TCIKC%6jRukd9w`rr5mdP{HLcM{14rv%7WrU@dj8=+_clSj7WjT9+||c#>_OM;eY1MZU1iuAwKTP@&&l2WDK%3NXY!zGfAVJ1uyc!-6s+C&hQnom+bOB1+wySkevW%n zR8h{@D)9OsO?ui5d1LUsx|eG|O$HdL4j!;w9y?@oO;6Xkm9Pz{ zkNhP_{hrZ6;xVAXTU%{^=EE|81xvKAPZ7PzgyCT5XakgZKG|Vs4us@C+>Vrete9uE z?6gbj?PX;$UGk@=O)fXPe~E1EIWGUorA)1%W<6EiyQ@9T?1YNn*!eDnNA}N8HH8fS zk_6nMYo2jH_dwE4!?iz6IF8MmpRLf_aZY&k$t|B{*T)V#eC~3|RV*e_I)RCc`ervj z6g3;n*oHe9+jw?w7*H>EG&h9%x=)FuOL}LclDeWjM{>nA9c@Qs(#r=yWj6MX@9Ne; z$u#TB*D9Dg=s3Plb5!fn?d|%KNB4?vqd%hZix*dux%svN{j(r0TRY`G(Pg(PuGh5{ zaLFHoMhHUt!>S(ppH4c>9y(&Fsu}C~ygs=>Ls?Y|yoH{YiTh|;)|z|7EB1M%<`xD0 z-L5jDUlYJ*f&Q!bAM{`S%NX&WSZvHAASUFcw$OnHduXKPWoPXY>)M)y1ESVj7VTcU z*aNvPt*jB3XI?<9Xsxq+k=FZzwz{#w`jSZL=kjr#nj;o6Gy0AGv%lm-s~B^iM_ukq zu21B;1UO`+JNFmA2M;uImKksb-XxvG2ymHfPJEYgEAE_*#3M?wYbwGRNy9 z@%kd) zfe3?pC9XcI0+%$ERXSdy=FJ($(I1?2Bc{$+JGO0$f4|#?S*&f=ykEHgfumYl#xBa( znh>Dw?eOm9%}UOjMw2!ssWg~gL2td`Kvg|`W?7ucR_!P4wJ&e%nMn@SF&V=(CiQe> zJY#IAXk2aUHFkOD`$+U7lS9notzOrZPIaZ3AHVP9f>gP)(d_4}w9g9QKH`Q=2Y^D$ z+a3#N3{AQRGhB|&zO`(%3G)klg4C|jT5@I#ZqnU*pggp2)4kB$FX9j0vA^}~G%8Ua zw$3E{j$`T#AosJ{$AM7Lg;hA3=6>8YclOTEEBhrB1@L=K_FS!lX!8?^+1iqgkt9jAjDkqx~`}nwCvJs6V|0h+05gXFZwn;^_~_gZ&UtzegFN7vF);J zwzu!fvus|w{p>Qo#I37dXQY^QPwz(_NV-yduw9ShIP=lz?9b{!UNsJj_BK~v4m<`? z>#=ElyV4uw%9MRmo+M}T-fEX)^nNLNr5f&zJ9Yl%SEGj7(mb26H`{r(>UX?G))OL) z?2M^ytgkY|uXiT#3k^z|lgzs7ipmf6#g8YMoeWZs{A9zuwKb&AFSoInx@@#4O4n#D zCKG>0`LOB%P`h?^(TKHfKJ^J4ZLlY>aIGuIW@5uIhYv-6xwK-Xn2{OPuglpw>}Pe z)%<8r`IeVoF?aV>m^#0`t#Uya!q~03Kj9s1gH4}$Um(wW@W7pYiUHkNz1^*Pkw?f~ zf*gQDU16oJ?>;5BSj3zaLtN8TJW0XBIqk;8pWbYfN;}iRfBy_QZ82YVk@Sa6)gRG9 zrH90w$D3ow2R{|9EoRotV5L9GU$>_!nJIBk+IlEHupcpT89&J{)V5m~dB{2tt@uiV zl5nil0)afI2PwLNVa~snFOy-uUa!x>Kv;L#x2a^(fdwGYc3JZ=xEK?ZPwca%l;qXv z*~+3C{Vx~YzP7mzBXBA6SeSgjxNx@OVlEmNh)lydA9$j&#E0<$7MLEG6L4u5pr~}%7e63ve|J{IEdt&?vE1ap%WVK*L|TY96!$e za-!LleWtfgtsk{kzxhE_LWxU2yV+)t+|9n|Bv|)RiuuO=0}I`kOz{{nifNO!HA2mvV6FsH=Ix3>yRqKc?wpz*YQji&X0 z%tt)H;F+gb@USYy9yWbyp(xnkuiY)Sc(=lL4JsK=th*F){u}3oX1?TBbYxzItK+ z6PbSgF}<*82PnbPAiMkhYm4d)80*gd2o8y1@zHcbZ&c@0)GqJ!`A;_%2E8udhh$MN zBBrh$^uFbjbh?9%D+?hVfA~wN{Vh+A^Vix+-(^lBKXA`=|F;!t#RM`l=HA# z-6>u)bWPNB5&h*sLHD6d*Ui`4pT1wClahFtE#{aMuo0pwU)V$>*+L0=PFFwE!@! z<_c!ojEs@?mEq$bo`#15eUMqRDKo$mlyn1guzx#z_z%PA|Lf$O0MQ22VIJ|rbIA!sK{&$5VX zv|%3>iIby87IWT2wwyI-EFk13gRRbXn?Bc5pwoRn&j-^;Tip|f2lNYirEFwsx?dV1gkgi z)Q!vkgxvI6zlJW;c>C7*O2v1d@`MWu?PD$HLJAKS++G%1MKO$PhjnJ(ZZ`f&^eU?B z+&Bk+IIrlve$?T#^blWE`@E8ug|2(iz2*)1T1S0%ZP@VkL+v=#ekR-i3MQxQdhN?> zduC3uR`5j$?0xBX^)PkK`%|LO=6mq3D6BU8Q#48NBJeQSOZG&|0CaFS{UWyelN+Md z7!EM6hubbf#hG5~h4Fl}=es6gOii4}65s19WP z)q|J>-78U}pz4&TDo4^pM^r`SD^0e@^~YN)e|gg-tR3|Zrnp1=4K8?5_u0L*FMFH` z-4_)k%E-8?`iA2Ec*p&?edX+t~ z1TJT*0QjQALV>kF)jf>{8aMo$ET{MD!}Jawk62fY|-Mlm8#c zSN*S}`#(hY1mL=59ehs*PVsuxF1MDp>ay2Um&b9hW>8PVi_CK?Nl?*;j7R9|Wawc} zRJU1TQw?9x!3=yUI`2zSGzy5#9&gLv&UP>H%oDxh)!yA1^xpGPhYeEGV_lR#^8J;E z(?_s=6{L?n(NUw#Nxeq+#oa~-plf^brkwS~F9}Ati|DmHicX5<$7RlK|Cr;1_xcK37cL zdt?y`ly3Fkik7pQ{UE-EAy;cj!Q>zY$`J=LK68Pvsi&UVBVTY%0{1^cpY`Jvc@DIZ znm&9$8dQ)xvCLhyCqg<`X%3~qFj^Xv-?FV8JHy>E}B;fHtAt|}(;=`IE^G7Z$%n9^i`~x-9fXW#l%+wLRINfq88hGpe z=fI^CxxIk2e~@8`HSsBJVgH(J*QAv1S10{7Caj(#uBnDK`;!tQ4(BP#(>I6rt@7#Z z8uD*;+k;7&yZcG|HuShON;N95Ia6iA_whWVaiH%Y7&_tBeQnTz#y*z3U!t`KXi(+$ z0x87q_rN(1>JOF|eCt`ABB%ew;#w$X{rjvlHh;mlyyE<=b0@BH=1}CQ!>TJtSAaJt zOzAk+n!Tq2zk%2>4$CJsMto_EsH(V#y|SG3ie8nqVvx@~c@()@SSz|RR-?R9LUm~a z-hIN8iKgXl5#;}0od2)ei;4ePQIZU>X8&^RgzGAR$%boiUPmexBaJ=rD{=Z?6HWz4 zp#eF#xun1Duidc!>3#ALX1}M7e|#?gouklmMtt>)N@>S;olzPp2?sh%{F4SkNM7K; zE~S5G)PLNi^AA7Gas=|wTNM!}h?+b9oLQGAVllR5#SJRD2txuIW#-kV>UjE|yWd38e>wn1B z88xw9NV-Y;@E2o-|6fSYz{GuiRwHBE{;&Pwf8X&h&{hA*eH5l(;XWT3dgF5prLNU7 zD#vat8`vlrzcG)`hp*iA^IC$+QCq3~wm*MBF<+1=uivm)AqVZs&C#`&yT+?d*|4uB z?edHB`hs04zv<`e5=$hPN&mxt46Tuk7Yzd%w}`+!5>ZF zUl`=a^RIa;IRb}i&2Ee^)kscUCbL2AhP?IA!9qb^rb#Ki;+aL3w!*MvXqE_fG`) zcPIJ7@&8{E`?ov4#(b{@_~C?q_?rJc0>7iU{(A&|thoMPIs*R{`~Rql{;$~oSM2{S zqx@gM^JAF$e+lr+KEk~Rh_6IZhbV|FonO_$fMSfzLcUF{e)@#@e+&-|m;vb40(CAB zE2{)t!&3apLi=w&$3$M27#j4laQ#o=(y4QRlVR0MXIIylPvcQdyD`7~ZJmeKNK1EdGjQ{OC4IB|W z2j~D~=8u)l4^i7cp93>h>>Rikp*}tnvP43b$JfvS*2jH^@0^jkko%ln` z^Lr1IR{<|%x=-LANyvkvl2E(ucx?Su{w#M%`c1ObSz#?l;N-Bgh96`9_j>%BpVZ`w zUAW+3=EQjsQM5?xFeAthg**9$QIX_A`j+npz$8Xs>IRv{H(OA6ghLV^l>azn_|ck| z905e8ipQJvm04s$@t9t>Wu+2lD6B)C)LqVkz2Db7WbHWq?H(a;&+GhJA$a|gS^Gb+ zyTA3?>cQ*eMKJ$pYe9yjtVqB?;j#NAl!YFPPV=l?w*PYqd*8yNK(i$hXZ9_!LhM3S zeypzk(E7xnssL&0kKj?}v+$tB66q&K-$#xybpn>BqXK#X_@vHTQjSmQzQHd^uN<{R z*59u*MHGO;JzGc%Y&i@!nPoNY{xB-{PaTMDrM!xyVd{2AvGTJd~hT@FjI zW7ZL^`2Cgs(9(S0{vDe(3AsvcjhJ9rRFi1I##)E~sKsEY=~Ig~rMN@jG55bI<``6o zcqyTbK{LPw2c7XL;Y;BNqGARdN)gQA^r2UoEi0O69LgqmE5FuSbTjuMs?T_4FPFTb z?lV&)hc4Ne_2<^F1~^bD`cxX|{?BQ6$?F|Su_6xq)}*Lr-eh`V=lAz9S(!bY1I4j( zOp~B>qw49y3F2SPy%NK35(V%ny+RcCKK4=IvqH>{Qm6php8_9`;^}&|WtBC{$x7gy z=~EX5Fpxi|OmlSsjkEQV33)lGyob^N=O(PHTfJ>S`}-X4tlfLlB0<7%ksFshZRsp; zcpI1_)NQD`rgx4J_JT4jg?pPWEXDF1m~C-hhuQ{zaXuQj{}DmQ z5<@0YAFXQ`!V*>#H*!N=w8fjWKevv{wrmpm8U5Bc#Go?j?VSATCP+Z0x+5@}os~re z2_{@k#e|e0f!R-sqNwH{Lp&^U= zer0y$@Wf%@&9|L~bWaX=E)Y)O*ITZ-6W+(W@P7;NaTqkX-QPxXr)9 z6#1I9VfotIhE~eYzm@iESs;X+NW&u*$F`>TfOb{70)30*ahg)So@B`KuG)`$dkH>@ zy^+QxA->kAY4TbR1Xv5FQz(&>zM2>w9WSKA(vW98ESf|1POXQ1zzclNf%wjvG&tXy ziK)pTNnYykn=JIrv8{;3W+p`C&|yeg{=oE2=GP$BurmSLudz2DBI~Ne0WVIt;%j*T zxPb*$a^bu-khk`ARYip5%^=Oip%sA}fO{DE@IWHoS)r0IwOW%zotx*ipda<;wg{O6 zam7uXi~K6+n;TCUO;9|4Fx@#43GE~+7L(wMEpSf7U=Aa1kzkDdAnNE zMii-^X1`21dJ^LG*6y+2Mz$9#f^;N*bdHCa4Z)juOa@Ljnf-)deVN3t8tx<+e+Wz8 z7=Y>KV+(~JfCM|2K8o{Y)ne%1LFf-p#qaHAfHI)Qjmg0=Eh{(GeO0DQe&61}pcqr_ z8#782jxty^PZ<@m$SvW@&`E7?8-6b_Kq#g$-o3!K z1F09LlJdM+;Y3)#p51r`-G>J6;)nMHc{AQ#%N3amQBxqua7|$3s9O5;9B`o~RV?`d zLbi>Fz4o5|VJ=X&JD{d1oQezFBb+D^PKegkMe0aiPgyqrs|rn7Kp((9;xl3w`)~Fq zJVu$qg=_>!`#OyD6CT(d?DlTbym%xSD^FN_Y?Lzx2p66l36x9Wcmya8^4NkS8N!$b z_DyP|@-%(0o9DrIXz`3M1zXb-)cAHBZ{kWYP7GF{&l!| z56>Yvb(`=6Hrh(k=~n&C5ja3J_`aiAE_&)_7Au;IF$I*qC{i^+qgCLcfgGjcSAz^&@G) z;SY#5%-d*?L>WYJT5r6@c@{b33L(d~EB(Z81N2>1k{uXjPY$Svuws3!DzZmJ9lL&Q z8V?vLXnJcu3CF2w3;X&kJ>YJOB0_%|_nSGH8f!jaJ(C90s(%4+v2b%^RxsYSoNkGW zHMZ!ErrN$~i(}yXbJGrB33M83aX>H`n2@FD+K#vC$rhx*7f0J#@z4&3vL^9P;u7T| zhnAYcXSTwx@laH2kDhYpy4ftsA{9Gb2v}A!{}pzzml(~NIeLk?SJqppjJ}v8nJRnU zaDR`VtiXhOrBWzXu7(sF0twtpk~1gfZ{$X^gvtF;0gJOQY_KX<&JAo^)@L!2c*al~ zBR!PwX8FF#nklTK;9w8v;RA^9Y&yIrY9Ms5qy-|V;f~S%Oq?QStJvvy=dsRp|8Mb? z_rrQsa{m$~27g!%9#%0eRy=LDDO!On9mqsXqhPN5F z)~MbZn%%FR#Kqh`5CZG5@?@dt2>6Ha83Aw)FBdk-r-+0dKow}Dstxa*6H|(Im!Rmq zINxFu9j?4yV$$ma#kB8j(?Z{9F-WULd?l|bWxmmjOZT*@H-?jh6Ok#mk|BI3hND<4eF-(3}aiuO|1y5+!e)?QA<(^ zq$K*$zX)*tp(+n62b4hWS{B9I3a{N)dZtA$niTD(Qi1jx4HR(#jbc9H z;MpYy2K(h;E!nilY+o`fycZja7p&Q~w)WUXg49}=h!gE*S%!ww1)Z$eXt{wv5t(CY zeRPT$G?I~0fgsW&K2*$HCVwI+$=#i|nly{5qQSGVU;X}^b~q%~4(@WU`~FTlRMp?q z_K`x}MVQDeQIc2`drfWIYFx)&b#ndYi{ks5#dZNE>bB=p(A;!Oc?<3Y1Pr+Dmequl zL4L!Yo0oS-;(NT{aE%IJJEPr|B0*@d2DL{7bAP0(y?PMw+@3#sj_eWEGp1vvm z^bPH5L)Dr-Zjh4jNH`m+7Tc{P(hm*ws9^+09dl$>PYs7Vk<@s$L1;Q6%599kDu)q~ zrTp}Y;YQRH1*l8?0_?}P6TX=Py)9CbFu1{qXcB3JYWkgcjEmQNpJzFg9spiwsAKsN`_>j ztTMp6OA5&rdjaXZS1)pFl@%H0#Cgz?I=ir>-1}maneKMRcN!0vh>CP@cvneFcuO{& zw~aot?aey~-W%_9SF}T79%z-Id;<;p<+GGU`Vu`jQG+t>YM_A-MUmxJgmpnjdAxom zzZ%-TCT1&0 zECe7OSI6IeLF1Pp+v#th+f=P-MCsOL!*(tJaoB6`S}eV+UUI$f8)*IJZ{n3*`S)#j z_Yakp1UVGQ{4y8P+#Z@tO^Ke(H%<7UnC%{Rm4;_rP!@R%6eGT}%*px9nHqC6jm?I0 zPkaYiY}!JwreEU3Dm$x#hv}Bx00b$f8xt)dcSI&ei=@REz$lCM2`(oF$fo%FYg7iY zpvX8LmqoL7bGz1(#W0GVPB^q!GDR7I+mC$%iWiKP%;mn<0!-s?z%}_jErwe4cwQDB zok-Fba!dI=8Ufb%vHh}`5-VUW4FmZ617=)7H`ND6qY?Zfxu52gsq`pFvq*fi9WXrf zC@aAs4SyplRwP4I9I)_xqD~V_-T7G!y|cBQ8<>To*?a&gEfQJStnwY9t`em~UekEV zmjG}Qct~8G#^VM3c&w#<3V-f$E+Q^x)HyjEKMz7!QRbRNs``+_7Zsj1oQ@@CCLnTl)!R4AG|g4X_lgz3p}N8v&wL^msur7n*FljFW5Fme zw%GTr16&wZ|GzW?duB!PI*G_XXx_B9Q2ZGg#vbMEzx+}yl%@q!q0uGHz%Tz ztf*ns0W2aJil3*5PZu!swmyfTDk+J;1d&%@9i6_HOTyW_4T%!k(y!qKXf|2tZ7uFL z0cF+0WfUi|_oi$R=`WJD9pptc0%tx2Wi>z(WE0zR-?5ELsPidoJ2S+zS6eOxvV$-* z5YjqhisyA;=RvH@2AC0Ue&ooKdSfBUqF^3WhHii`I`Z<=ybiToUa!dzRCVI#KAVI@ zVWGkiBjL#IyGsl&y6cvXgWh1Ml3{=}NN^JCa;NbvLD5!lPV^E&(>G$B1Q?g>?%5)W zh~p-^&9i`FW=x}q;Ky^$?I5vpy;FP{UO~+wr0>)IeeihNgfoJUxyX+wO7Z=-{IkDp ze+;Tn95fC2GHCjneXrXr#Z=z8P$F6=$@*rx+UCWAdT4(k`sc12j!(1SE}sLJkH?L zkA3!kgVhwxx1_&x52|votw0DS{nFEjaW;#?ZS!K!Ju4+@(wE!ZkU;Ar!X0Ql0&H(F zvHldoBbf1%4Y8A$&EX3p2066;$v$`z;G7Vf(GE=`xP{LplAAvW7zn4H62rprpS&}r;_GyHEU=gMvOburpa2ws=)q0T2*#5EvSS$Jy%g>!!qti zVUm&0;8~2X7PL=8o+TjB0tv6IvSSaosw9d*r33#zqE(v*PtuMOqn`k4E}A$PDMpBs z9eEI|PaTkc4e$|*PzW4_?V1a&`-Y8piCZ{qAo~JkCXp{Si8fg20DyMJNHnt>Dm;5D z2?y^gq0{CfQV=cZXd_QnRWZ%KCGtZqCi)a~pw2WR#m{+JSsHv8@4$-hej@G5Ruy3= z#Wgw zt9VVlJ{{xeAMBG#yuzzLy~2CU{j3uv=Iyn#D&^fI%Si=?}o!$-vn zy;<^fYh@>1&;(j++U(Fb&=+VFyn|o6BO1C~1@cw>gnShp50WUM_jv~}HxQ$);?$I> z{T56BYY>cOu15rpkMW~94U1(P`Q=*~9|!$>692X!MXYDdP& zE-m;e?y2Y z_6w%0en|UfQ=QVR-h|7CE55^T2(FERPNMN- zci*Jvnp47OP^xjHm}(@% z7MmAA!;An{V#tr)$CW*>2b##>8!wW$Fr@oU4$zLDSxGB=dcoQ8ZvwEA(}J@ znX9Jm%Npi1p)bguWcDe)_sA{<_gFLfl%GvPXqalEf5*Zk4>@kGtjus<;3ko^V6Igl z!aRr+&aC1<@cNk+ggJH{tR}fHAjJeh@2$-pCedjN(Re3wQ8+WlRbt9HSHkA+e5yYr zuFTtFmFl+D^_RaB&-?W^>^uAb=cV<75XsXA@!of&<7S^_0S(wfsyL<3ik(yv=r4YY zaz-wWYT``^VaJygG#rslR2=Ht_UL4A;fGID`!t!OfC9FuO%w-0(|braJfsljEQhie z>J=F$K?$1`*?P=VW5qR}Z@6~k{Baj61=3Pl^#5nDny<{eY) z1!dd0O7)r_QZ8+`m0;!Yb#kKPV+&8PTvas`je`JIq7^j8{c9NB3EPO8& zzeDDSFRA6gxe)c0{Z>^*O2~mo=3BP;Zi}068gFJowI5O~e+JyOutT2I$8-nIu7Qvp*ack5KXTW zq;58uJivc)Y)Cs#*2vJHcbC!tW(^_43P3Os;Hc$7nOT3$s4*Q)G!*W|R`mxvO)kJp z$140x^9?wO!R=>XtCs}fK55LhJ%QGfeLDKN9RiO%vbO@%-TOBx;oG8~5~$J4RoS^P zek6g-Qt@cx^zuG?vo>rfwqMS8NR8dn&yEV(yfUwooif*Q{wY))3oUN&@r=zMS_x z+x=T<{MSS}>@3i_kel)Bs@8Aa)1f5AnjMQI#q8mVJh%ub2MJSc5-COM|8z?C)zGQ@ zy`Q%N(YbE{#&mRjCr&0s(Oxh3^R^+Eb0y9%zpfJH#FUS?$CfL7)VHp3bBQU6oxV#_ zB)EqaId-WsCTO`)oZc*_sEHHlKf9)tBoTJSA=w_3h3*a%UchMEwxK&ntttQRU4Mh? z-B{(6KB5U1%`EGzLR@HiqLh5udg)oO6P<;z3o>y3^*?)z7s2S}rgyV*p5lxRdp1mZ zCtS{Z*$bz~<@5Pam- z?!5EGv>TT?!mCf64sbGfL=t`lEJAX^u!-{0eyh1&v5GuRo2~(ZW>yEhd`Y!EeVl7- z?O^+k4GFfnX{C(0rgxdf?ZIgn<93pU+a`b5RpMz<=_U_$LjWU=fb}zGqdDBc{u}2z zXgthqVO^)j=?CRlyzd^A6Tze(XTv?jf1c{WMb})Q*K}e52abZx&hMNK&Cbn=oyjyF z%>7!cq~Y&<9n$%JkRlnz$ZAp`Iu19y@+f47UpW%d@)KYdQUqiRYGU(TFVlxxc9n(< z+|@*lv^bS7K$pZx4fT0o@f<@sk-={|tX$w>8FV{=$siLiUfjbU_=*ZU%My_*I*KWh z$&4y?A^4(Ar4uZ$%7%-aq*WWirY zRiPaTPxe5CRvwHPb|H++udW$VjbQN1;L6!=8k5WB&$L+Uq|;a*nfyp3iny@&wp*Aa zFp~EBQ}EWi7|_Lq57}nF&A(hdAVqNNNOfL!)BMclSc2n8Uh(vBNYb&1$vcT+ZJ6~FD-d#jn6m@0Rfplc7n)6IdUQ+iBR^^ z6R~}qgM#x>rebf%D5yal&r1|3ZsYfnaG`8*N^5s5IkuoS&VDjex%M)_hgvkC}fBNhN^v19gjYC(%N|8|G-l71GcZ5J`;RHbB%SYGI~c&B$({+s~vJ} zfdQm?hTIR?qO$=_#Y$D~n*xS$Os!A$NEy@)Z`Tk0{AcJ^H-15<-`z43bLez!6Rv!~ z+RZV5+Y?S?hqRkjxhhe>hSH=eqiWQY58F&*s}QO#v{-sOhcqT>(l||e7FV)P0Xa?h< z%*XU6LmZJR{tB@&+RACrk8PLSA-KZFz4U?v^!pWur`3rwIq97OgUBA?V67=)34tzv z+}0;LUZnVykDn*@;mjTQJaMXV^G^50VB#eRsfLD!uzDHRwbdNEd@%F$c1O}c1rU#c zfJl5swTkDGTCywvb&2tQ#5dj5VE0ueX!-!Vr%wK2S{>&QmkgU)1?NQVfoXDJPiTHI z@9hvYSN=8P+}Yv;QAl!~dEoS0EWeM=cqX;Zm=1k6I`{xv784;4qqV5mCyt2^b}I|q88j3Y2}WC%9H2f~F4HcN&b5RJB-{}W9@ zF(Ul+c|`H|6#K6Q4`c_)1bHuF!pb?W{nI_TnTI6R29L#1(mu_{Ad4 z1A`hJ5c=M(m1mdLaL;@EGb0b*ItAVTx}i*hPkf~ z8sH_|3}NPZBzy`X5JIBh>VD)wwraU&T~XC!Me%URe2I zhGil)V+pJQ7>Rg)?vGG!C@)ZXTV+M=l(Uj(C$ z@rJxZt@slsq~!|BzY-_AfrISLwLzfh(w=aFReOCCF>#X7*mS?W zEdn$j6KxvGNXq503;odCSqE^1^cgVXv@buhW&1qpi4Tk(DGKhO8RO%}RTnKNcl!II zE|j%&qx|5+ZjLvJBDBznX3&}WezoDlBr2n%NrPB3f(9+%_=7wQw!Xb z!f`b&ztyLICzijDEv6nR07YJH2G7*Ae)~N+rM|~?`fBpbkc4-&Vej!uVMbXsUHxg4Vc`)|*p1V&Jd6(B0xZf&4 zkFJ0HEg`gWcaCl0*)u@u5DsU{0lBVkdYL6fGxr;Qi8J@3eYFO{z(ycyGFu$K+P#1p zIVLMqTyM}mcL&Iit`+L3WVk?rUZ1^XpLdt4_{e>r6kYysgV;xd& z2z$F4yqmBx?-?4^O9|V0cmgz2>cz}F2G7T|jYlYLoy;Y|Cd`$iuXx=(<)5vEOcsd~ z(`)a6nHdQ?LUDFM2?bbj+!d&dAVX9W63%xSMSusZr+2-GP-KU5ut2-WoJ&lO>Pl?puP1K+gsu@yw97xyE7 z(~HCn_97+X0InYb5s>$eK1?tS)udrAmo;>#(<7C+N3X1pVL;#VK9jeNkS@Xvg`@#i z%;PE);n+p~E7-dtU(c2Ueh{G{W=CWMvX}RmGZ&5{ue9zh!3D@T_%6vTfS@Cq|3{x zC?N2Ao>?pV&;)8E;7eYYs^y?@EMvaslGp6)ruC+G3(pUXhxrZZ-Sjk7(zs%Hta`}B z7M*v}euY|WkS!QpFH%!-&GtcqK}p^40}K?>(EJ6F_X#r1?PCf3p%NL5b;RYi0af#;I8 zZ^?dX5N$l?!}x|;k(I~bC&?>k?*pPS(LOcm4 z4e*GmRPE~>gK)YH3k!<~oMXZEaE}h`g6&ZTUt#tV@;WsJ*^~j39r3+fk9;b@v?BQv z8$EQ1Rg2By52)5#aF6q^;dL@AD7hVhPyavC-aM-5bKMt5Dy@RnwoHPe*6yN}Ns0jk z15VIdM^pp?0R<&U7^EQt2qB8%z@*l0l`#s=kPwCtLI^<=u+=0YkN}wwY=UNz#w0*U z@_W;B_gUAjdw=Jiv;T3i7Q-^h^L^gu`Al!`W;yQVbO*PFC=pSL=@NW}ro>(?rzvM) zkxja6=@|XtY@URa&?cp?RjWnRWfeA`#Qv~vb*r&)F8K4jEB}__;0f6ML>rUW{Vzw% zzq95% zxO?lPP{ zEt8_{O|T^5?2=NA0lf*uIe-`EIELuWaLeN54o0^~F4OTRq%yySU~=k~2Q+?yj8?Ht z;M=C=3l+iYa-e40eB7^}ozrJxKNHrTwF&e5z!g+byjD2XOt&l1oN%dEdqhej4H>=1 zL#_R@&nZhjQ@pJ0bK>i@SA;y;4|*LA{lYOWZ(3657~B3rQP0i+=rkBw>fwoWE~hQK!C!ZxN)cWNc1 zyR&UNZR#|$img|a$NrijBja)z>)7h*GaRi#jjAk2^J%TJFv9_1_I|Gr-w^s z&~6>&4X+x}AN{f`#0n)2Y{M}cl}u=}_-jMiNbI}VTvrFSY?VV@O4{DsxJ`3#UT%8p zb!;1Ku-D(Q*~5&wWBu`Q+AY=7zsnv-EPrJQQRi90A5@f3?8A$1EQ?%JC1{a0?n|oT z?7B&&Z;aJPMA?6n06A3&CTiTakK459yd~ntcS}Bl_nV4ddvqKwQRGiEw}*#lts@{s z_KN!UgI~8!;8`{%jdbmv-b!}U%dpq?3cHKGw|@(JQ2*eS)6O}wDC<&#eZsqyHorpf z6Sjo*Ssm|V*?&zoLKLb$<{1$m4Nltjh^y^u!{L*bmsq_`*6k4M z9UjMrIeykKh-1Fn5GCx#`UjCbG0m|P>6!FTRp{6CwZ~XicZ%NF_a?_CHB3riH|sfm z7!Rwq3ccDXNY+_gN%0MNs%Tpcd)|}FQuMnQHBh`b4-g!VWhz^e zM){W(K)7RoV#e{m*Xh8PrXuJC(zGBT?~dDk&ZVx=$dHmLk$J5UST@nnJO#s&)#>oL zeD7D1mDXL5?-jGKqhx8q>yDbz5Q|Dw+*WTM37_zqN}{0bUbr3TI3;pAVb*zt{jL@l zO)EMosOu^?-HH0-j3?SeY^@CbbmO3+rEGLAwF+B{o-+D%7`eA`qdwf&k@e?C^^~ix zEvs43=O`Jfcm|@|H>EqE;)$jar%^ic#*+l0T97YrZo9!2?;wUqQ!|&*d6ImDoc2yP z8h;7=n&OQ1sETi8SBWJBVz3Wn{%0vrdU3t4%D@#ctC=^(o~)o%$ugt!4$rGnXI5vV zmH*E>yRsEbDAln+j{83@{a6N=Py#+l4Ee>kXxt+Ou=#NrwFMC&zW_DPJ4z#%Qet7} zkkd?`AKKJ?1SY&f2MkJ zDK}#lG?B0gw;!!dD{ORFk>V*ImR16d_nF59M~Z*3Z-=cR(g;GBdm7ip_L|IMlf%^D=ycG!KEGFQSlbZPS+3``EegH-I;?!*?R<+PT`OGsKYJE9g=mShkW< z<+#^|E?a#8*{7z-nx{_tQI6xcf5(BxT<~b z)SnXUl{BK0Mj$&dJ9tf}Vo)-w^c#K*j4u&5;$*AF#Pkb6^z0Z_7YSFV@qcoCS+OY< zPLe9>td>*PRjT7|k!0e44>4IvBLn+9H-jzliJ?kS*mjPVgadJHo?mzR7;o^+s+i|V zc&$h_Qfm18HzKDxtS`R}Xe~y2O_G5e#}YX-$G9pvE=5emaDGfy-L*VT+^ZiinV}GL zDfT{sIgOf@x->9WIyKLN?#H^0@eo^^umYa}nm(m&@HCF?w|qCvto@;I&e`gfwZHvYq_G~bewBaeylC&k&NTGI zTU8f*X8@{zt`{LZ>+kKC%k(adv_*%!@0wLf%Sufi%Hf!7;`>N%#@cWj0P-E*@aHg~ zGwicBY#LslhZa3?@{y}Jr;puk=|L7(d%%XFoj{9AuR@+_TjEf^VGji?XF#Wd>K{Qn zn4V9Rn9w-ISxHY!;J({bpR^-uQe`hQI?8${1Ad4jCg=|QBQ>F^zBbb-ahS2m%NXGf ziYlTi^)az*6yXb|{g>ezQ_eI?oFpTgi_fQK21no6<7fg4hw52DuJ#m{B0j+0*@e1o9(pezTXQ(Cp~}RPL)q_ zY(Di>39OW`{t$fy>i5Qh0QWIo6L2MoW0Wn1gCi+l_Vt>^zA05s0&-BQ4lwc%{7*`^ zILyd%Oub(D88j{F?orr4C!mRwgIUa(F))A!IeeLtDtQ_E40u+{a3bs~tL-a*n?Uqg zuQ$f%Q#Pt@e{cdkSfqiB+@P#332`^?RX+R%GcR@!rcRc>!`+sW5SvGqDIt>|Q0ZCR7ps~W+XoG29TuET_=&M{Ef9~`{oqZ>XQ zRgZ8sA=)uNiGhqwKu=(<%O!E1oj8vT@`;{8tHy}YJUaBHKs^do8ooB_78-u|b@#us zIX0T>IJ{?e`NuO&yUa2@alUapY`QM~C&YusQ%8=`LcJf=?ki5VKe&N?xU&K2wdBYW zVpz$eHIW$)E%&>i2JTP9H@X@l>>izteSA+g0~p)hnIgOVx4YvyX8VEi?|ICfhL?xx z#)oRrRx0aOMgCljt_8axX_%8<_4+~2L0tZHCyqp0A3nhB!Le-ifMKXZRC^?66UIY2 zYh8&mYjalCQQp@0ImD+xiyUemw?^r<*pXzp?50yk`Zv4@-vp?{#%ED=lhphbRuc{p z&%Il!TiUtGx{ci0EUS^CZEar8xcsK}f>c9J(=!sfiP>Lj(v0%Ar$jmY9{A0#io(!O z5#4T2P=s`7i~G5(^Nz(-k8y5n+)M*zWG^(h1&RjsGWS%@&4llev*GDZGd) zY5m_F;dPBXOUK@$qvjMv{^F zeJdTANPP9{PEyP1IGrevia@?*O6GI>?JQo5FB$c)9HrSTsNtZcZ+)yR^j(Ofb(FMa zx_z-%GVy84Jg!Hz{;Y4JiMQE&-R6rp+@e-tLiH%{2e{2hl^V?1a}X6xId3q&Tcynh zUyY8bh(7m5)Lmbsxbn}kgb^Gq*dhcl)tG7yf2}?LwNIf41nP{ddvFi8LnC z?z_M`1TGF-xqJ9gFk!mQM|=3$qxa$-9O@)H^J-5#3bYzhI&T!`IFfAlWW!82`O$2N zgVx~G`Fn1hVcv|D?CvDZnwiQIt!zMtPeo5Nt@zgWQVkWwDZr#QFWgag>&P*ZvTi`^ z5p;9c0wU?y0>&QStflZDUXy!GFiuz8LUKZlH#ndN=fR#}-ow879qIWMmq87tUw+|G zkiv0K9AOaRgevY`O4L>|ZN1nj#r;6rOIZI&LYvOn<~xVqkzQ5WkhcyGkX~Mvyw0uh zpjm34ENQ7s*JlB9fpIJVT_u|lb9E&q4XSsz;{xgacr0&J?1y3c$YBns6y+*Cc#j-h z>KF01Uo*S_eb+#~74K<~Tb>s7_aJfjd`*>y15YsLInM0@Z=i*?E604^*U6&gjWo3` z4>EQE%cKCOfHQGFFZ0v4SssWF>3Fb%2kPYDp(1&n?1B!Q3Zz?~8eI+>6jYBq@mrCOX&owr(FiA5;u3F5%J zm_tk-2Z~(pvvY)gRCQ1LIR2jEOir88K$GTjv}+$0uF%D#duY>O%i9c{2)gc%n)-&8 zDZ_|>(&`wHIN3&VFsTWc|DrPg|H^ngZ81puBWix8eEdho(YDzwK!r^3wQjc8L@#_- zl5T%G;Ot4mi!SSLJ=}RQoe$SU)+G?8nLP(UX<7Bn(RzO^y5zBExD&M4dZVJG*Zz2} zq#WUS?B3e_y*h`_-QWGCZTr5c30)?P^M2H8^mSYsy`1bn9{Ocd?*u?XimpGYJ!tiS zo%Gr%#>pJjWYLD?M*YHo{~|IH#c7Q#IuSO9!B0Oeuu}O zM>aM>1zj;Q%?M=wZw&_Wz^pFa1?&Pf!!;MIECK;{Hqcs-3P9l?2c5ZA@Zg__Q)&Gt zFMH&2)fwkyMRzqAXtZGuHhDpJEjC22%@c!(rW+I)a=+(TM0ff&rpT}GD_ve&7goFp zXerO=XA3{8)Kf!*MZ@;%JZ88tz(vsbL#c_C*wmq<$=JshbdxsMg>qgwU6X^qh1{XNW?Mw zA=uGJwDGXVcYCQ0_mVwpsf}xQ^u1J8Gagvx!Q%)X8z@tUq;7fC0SeHKZH#{18v2@9c&8@!-NW5WQtHSN{mGu> ztWcXkmE~!m#%Yi6erOq0Yy!fGvZlz`*BxCB74OE_*@CtC#8IbjWU-D-$)yepQk0zH zHNKdoM3hX3^Z4oM#1ilsHoWzzSJoL#)2~d^@W6 zj)vBM$)sVgw`d1g`e}f=Tld;8kk6~Z)=#+(D+I;%$CjfKhM5)p9ji)C(I`V0weuI3pCdpF} zG18I|wA-a_(S&GlvmF~*T2ZW12%r~S8Zo=n6L@GpT<7#oMuTr`!u+KArKV6q!pGqf zbuR^D9_vS@buS>)RlSR_q+CC{S!A#uHEV@}1?0e(<9MnxS5>SQwn{r>Nh&!lXcCXz zkHLqSn(a(jl&SK!(T^+WzXcO?%f1Eq9=W>$KL6Vcn()^Q`jFDo93}r4UAHlu@U{ie)D9g03CjVW*{H*>uo(H0=k^WJZ-`iNL{k$; zB~W{A%}&z``4cgS1NF>kGMV-)DuWn$o78g@;3mMw)9=2X%kiUrT71_&g|3*#-8%r+ zwL&~r-PrYYXMD6vy@sFt>x}s1D;?q=)Ob43LW-`QS+4`WC&?RM`uW`|WojRS?HD(b>djquQOiUr*|3Ve%C3 z-?;Z6n3WT8|O zC>A?G+n}f?z}uT?$ZU|^?WLzf1TBN8;UHyhZX4_}iSl0DXB6mA=O0-_q6V1paW?r1@tls`boz+e>E|(q=jRb08p9R>4mWgIz;S;Taj{_$fQ$g1 zXnz?t?{&>A^trJf{+J|jshhN05-n(yb(GC`C^5?0(EW&&@0YCb{F%x*PCW&F#7t~_ zKjuhO{D;d-R^pI2z_5e6XJZVILFHSs?X^Tn#E#XRbZN_Pgct!kvaQMLcL!78WKmAq zhw`-vHhA5v}rk!S!iHgNh^W<$%{&>cbxO*2`<2XeVmZU8G>y@S`T;NK(2!IdiGnJvoxB zG()o|JgsN{a=nfmi|hT&<{2c{Q{`w1Jo_s^il8tt7|efU-(?I#wEx6oyEIc>&N%xT zc@+!SHXu?uZQ+rji~)`6ri)GxtmCV(ZEOahA9K54#a1)0v+ThnVcRLUl8TaH2_kr6UaOq32V!3V)3dtC7_Ix_l2`0v=*%$(n zJ0~w!()cK$G@wtk*JiutqfwRfG?S{w(`fkr`GS+}w&91dUpXieOtIq<-NM%I(KtD8;q-H`eiE zBp~zSY=!CYvAXRsEUSACWdW1;a=T~mI;@APb;dt{sa0aGea3RQ#O8^;Wpwr_wT4@% zcXFeDKBg%bIM6n{YFiV9(rwc3(V}`sVP``JI%_Az+t0t@WIEU+ zJ`m9#b2?)|Z+u26>03G~<_AO8YTlRKKF3FCW+`#iLkeJbo{j z7_!9$=fT&M8_vXjEzG=?)|}mrv=Ve&2Pniey%uF<0epnZH_~~|rsv?hq&nRb!vr2M zJh%EL@X9Z|)6D9KD~!usDjno&9zedyf#cunG78 zPxqm3wyrrERP51~55{=LI=*Q(m1TX&H$c~u3r7WuMIt(NQT#MBRM+$cLN=ml^@_g6 zZPG_ukX3%Uq}+Qg$sY_a*eJ0!B`%m(jh|-HHX(9Czmn!ePfdr%6AnmAV2SfMMo znqhAopEwnwt;wHz`yT?_<>7S~`Jvu)u!8hqD zBOE^<{-O?&DYUXMg{lbVF+{-;u(w;}&O+pwMxe)_Xy_-kP1O%<}8eFU$s3rhQ);Te#1Eh zZ)z2-GZ?_sW^odE=p0FUhmhL3dbOSDIX0tLK`i_h1zS$md>2a{m@R$^fn5OV>zv|M zzPY;8;MU-u{dK))yFUfGWn+zIzv3ny_UAga&X@$|PeW`s#p37!2JpVJYV#E>R!j88 z`QL9L=l~p&s`K*2QUKI0`;Z2lD2|?-2q{1CuJpDv>(~&tk>e1a(p6?sF!%zB3dgH? z7inXW7Q{(Dt=Sm#q<`O zpTO_$!9!`E{?D7;n)hRX?~kn{?EhG%cqZ^ab`XF2f(@@IVZX}FX2`!7&BYlW=V$rK18 z_Ebw|(70kFnuTa36ZnyKDaXj$BSJo@bUdHz`6;Mf^#qJ%n0NAs0R-tx_}BG)V4yEJ zVe^#(cg*1UWAvi>jp0`r%9cqxNc5eC^}|X6MUvegxs|@l(OU zni97qZN5?Swx(!@!!DV+6o|XNbl4{o7e9wk2{vp^NgK(0vH|Y>y)bP-J5&`Sb?8Z9wE*1*Bh>t}KB3va5RgH6BSA*b=6EPROeB_aU`ov1L7?uml zyOWDiDJ6LhhdtJ3_x;GffbDe|&U7vp z9G)Ppi9IRy0^9Ep)cxqqW3tg4`ekAS>b&rWI^<2^N)E!84#&tSQ&DQCV{%S4@^s4ZmmAa1Dqi7AeI3ZrGa=*jV<(i(+I>?wv0KRr6fa$g&gb%10EodDMcaT}7U3x- z3=H^?ukV+2YCK6iXU908d7lO#x_58QnfP4j4VL08i4MR_#-aq2$@t(ZmG_M?B0GJW z2{ucrG)qM6Ehw-u6)pD7y{~JD<{!K`n({9#fO0a=QA);a4?a|au9McHkuEU$N?mNK zp*bzH2Kot9rl4t$*69xQkG623DZ;rIJ!cs!0@yXf+fUy=raBK@t(dNP2#o;d&dZ*v z{)rle!pC<6`=;~yX9iyXEOtDPgvBT^@hEH(XJ;an@y%1yJZ9C$Tk*d_=l-9L`}Sya z!Yh&yX9*iUC+V>?r)D?0aAacUSVXjEaKjbc(#S!w)PM>-9=Dv>GX}wXA6g|w`*oy z?oW*1J?0(r0W>?S26qc@yt47paJkwBjr&Sy93)~!~)>wTR&Fq~B$m-)W z;VN#+Q6BKvx~1`^I<0!fmI-%09(Hhp`h%D|psVJDJF2Oe1UywQT zF{5_LX8z}Af=t!XFR8gtqP>hkHmf0Vp9xplWn+H@b8-8DIMwqB484zZU$=vIU_E6( zlUkaaNc{mdH7YP<2~dq_4{RR?v*Yt(l!QEjl!=m9+8Vh4aR3d5n4`>Kc6XBMYGGuh z-`Hgr#FyM&Z(ZI8?Ai;V*m$PT+^p!c5T_M}eE=lkfcP0>a_faT_^YI4lp4gmDuxr| zP>~w+4N>uj&pX_vxo&C3s?`aOHNQ!-$ixejY5y&s$fp$;~M_K!E5Z<_}D zxk^_6EB*j+EOJW;|Fs_54v^+P;U7q@AP;2Dps9u<|d z$UlzcR5dtM$V85oDM33T z##xXl>304y5$DH|wMbS-cgN+BCG^XM+_C6t=KHvW1&<{wm0%tWcAp0Z^SG;vH zNV*t;5f+;m?|W>heWo@Cn|KM2BCm*kQp2&D2uPHULZv~)UE%;tb?-{Y%W@G&b@~H@ z6d&Mk&oysfOBrV!-la6Qu8O|V%l|XQt4j6?qKZ1X)QFcG7GHDe&$*Sl`s%`6r#{(w zx;~|R$TMflpYx{G-mAR6K_36@m%a&4%W=zJ)_ljvh4pDtq=URJS=i zD&mC8vyyIwmV0iGG}cii2V~+zt}U*T!J|o>=^|xF9Rgc>+ZGBB?l03<{?Q7iG>pt= zAYh%v2;ym8*9WI-CpjnTcrRA{b_@GGCx??vSm86ql%X4Bv4rX+MXUCukh5c@DVb&3 zxrG+=bX(U{LyHM`9P_#~Zo{ondZt$meBjnkloSDK(9#)*%Rm>RB%86?)(DjHN8L^n>7nqzmMWRGF$z-2HgF z`oEy^{|V23^u_o_kj<<=csEj7=*!s^Y`SvtqX~=IyBqHie3*_LD z7Y`}Ej)V(NZ@k`F7}Fdc20*`%em$K@5x{+`B& z!_8{xX1$LXQZzElA1u}O32p$l3tuf53smLCxjy0DblZAw44%5?XQ%Vgp~B#tZ&Nf9X)-X6zjrr$K|{ayb%I zleAGEL(ZrzwGCCQ#MLwqO2IoXX%|-JkmjmV_U|+6CZ#X%HT}v6Jz07OHykvDw%wsH zfY2bXEVGNY9T&=y<)-#0K^47SoO`%e0u-aqln5u*acW2>jUt$7pL{k(_iX3#N
zD6m3`LYUNu(#fD&)3v^T%U||cD1c?p(83*l`uJf`lDba<+<{qSGMEE5;s9-O+WxHD zp2HU81Kg)C%0RCVOt#yzVJjL)6U{!bF62s(igo9(U5aH`Z?EftBSVj#aZA=7DI^wY zATZTVo8sZUBPUBO96V*tm%(r6yqhalq32B?jgu!C5==|#Zw>AIvm!A4HoYgJmt>u^ zcV~ShJnUg8ZxF&x5*FTay-xk~Y0)mU!gh^2(8yZzCaU^W&zb?P7{NJz?Xrpx(U4g7&Xs^sY%&0 z->3%xdzX+|P~x#rJ#8lTacdq|znODf*DO6*Gc4({w(uR}FhLYfn}1?Lu+i^Jn&oL1 zR1Z?va%q__N5uZnSGpoPG8Mc&|0I2bLDVt;-nyVaD4%-Ld1^scBkvhpf0xXcGZtUX znpYa0zO(*ZbaEthshox2ocj`?vKy4sDxjl+0I3L5id;#N)ga@}-c__Hf8ZQSu)M0C z9mJK(6cs#*{eE{Y?^*6r-z>qXBm7BTn{KmMRL;>Em5~GvH~3gznK$P!Cqjo-h9Jwy zNEBgEy?L~P_c8(7VHgtc2yx=C^XXvP?oy13#Lv|^uTT)eUPbjK6B@4gL%CabX8^*M z?jh9mZeC#ybZj%U=La=lk1sV|G6{ty+?%9-r&GsT1EzmBy#HS}DEzNcT^c#Q37k>; zY}?!%!avlH4+1zRG5q}lfR^0>Ng(R*$cIxy=WZ1FK>}hI+_T)xErN*KLVNq}{@{nC`BrIx8yEYPnvkpFo?=4s&% z#I1t+TZ)gP8zd=+Oc&cAx1{hp_2W@>5TZ9_bhY9tw>e)kws>=5uy>PWWgy0tC7Y$M z&e;T0<$p+z&nvHTHBwDUx*GYqueddYo0HCW8PV0o5-sg1W? zjP(4%_dLtiXGZ#{o23xRrIG&WP z+SK6%O(^BID+!>xr&F%iFeI8$R<3dviu0TExoCnA9FH~SoT~yJ=U3zW-U+-5*5t!L zH(=w1*M!$nTw{3OK0#&QO+OlfFBLj|JAqr zcW#~MwEgW@Ep{p)s*e6u{ z7=a3F21-D@pbxy#OS|?SHS1m%JhD6yLUaup4zLNfKT?q`oWl*=jis+bc6lut*0FHJ z+a!%Xe!nd<*;C**KqE0Ch0d#ECQ}P<1>jMt@>4OdI~E5)9K+p0|GEEh17wPaT-U&j z$vd?+!UbC!2FL5}2KzbQ-!OC&g<4QgqUt^v7xU~dyZz{Q=AkH%cOWyh2XE-&p;@+@ zs>Ve-m)pqqzE7XH#BymoDn9oK|C?&+vm*n~E+HWp7uyo|>*1XZ5D2E`()KJD+x3+> z%2cFhkpE+5RD^mL*rs}pEgGXm6-~)le&Q|5%UP5x2FA_k$4OS_Ipn>6 z{Ml!(JG94jcN!Oav(cBIJT*2~ab{)(l?Vp%osmuP(@EoVW?L_Vfhps0Z9yrC&h$xq z10{T0&hoC%yHC|P*2;sl5MBQEV%yJSg2nWEqX-6KZ9dBuqwu-gI7@FpYc{{{Talik zduZ4~C}!MGdNUW(*e{QfA=G)I#EK^?@8<-qtHcW1H*v&uECmg6on{J-IA>ZlU|Jw; z{-0C9<7dp5J={ts^!^=icJ%+w7_pH!#WT$lOeg?Z$t&N-y(ZQkus!8=g>t^?b3kZ3 z+t5CoVg7*fL1LTA*l(Iy&0p{C>-|8|#r5Ydq*N5w+n?WHj98z%^m|PW#Fv|)J-dsM zxb~orW8ntv(3B4OKHW0Qs=eu z)Dwf;m{NM`kD;#eNby2ig$ zZykf8oud(4>UDNAusS?ye=&~uxEXTCSG*cXoZ$88Uw6ePDN5s!<@>(ysxu^w-prCi zviy;6D~i|@$D%&S7@kO{>8LtqtKY|#*;=?aM4oP56N}O@B4g_&U*OW?h}b6F2XlVl zXOokcgM%(EZ;Nw1$ZZ+RXhvq%kf>_k4r2{RdzkQv3@U>mIDkGbKUhM9zLM^#6KnD? zRiRkylEApv0KK1Z12bU7nkV@4%_uDWp*h%Io{8HL(O0 zGU@yJC|Q19E9b+dZ0+XGpxtm}e`+Sd<}!P#C8G({WB!fHT+XP$^*ESS$^yv8kl-2pno}`ANfH)Dnm~fU+BAFChO|us_4N^r ztIkywMeZd}fk$6Z08@wila@yf+>w;|)Fr)grnwb`UnHH5*wzBMGc?V8Ip}ej zmNMw{!=A(Nek1ScwW3H0cVa&-IL^?u2!>SU2KJp}RoI-df0}G}VZ#Bxu%?3e)u4&^ zc66axV00Gna@PHMIb zkXUz3QR6ojZQG5ERqeoLuf2kE$gJ{~cWFzqya0O8a3A#||D+eCtTx+faBfMY?idyy zs%uEtvxYPkcbgH)3x3^EP~^mBsPe3q7AN@5rBrbKSvD)es$5EH{y97U+eu+Cv0g^- zpPVbdKHE@#QMl8xGEm}ds!Z5~@#1kRb0i&~m22`is8jc+nVOaioNr({=S2w#v;8&; zx*K`xlyXNfr&FfL&s3;KukE_09&XahtFLPEj2gP8ev&1f$~(^%NGI}KKwd-+jL<*3 zMd5SDKv&+^(&BBze)pqC{2K;z$SXv& zqbJht*LTkA(tMBI+kV8UVZnngg13~gCLHN`zz=!z!O+4U?wZ4Ano=r@Z`tQmzKm|H z9GC;cOAgN>=5Z$^fD#+`_@Vh~54t%gjlcf7ukN;H-%ipjn4|fOWIJ^9ClNAeR6+zU zu044o5&naWh)y;(MkPWU*^nk;6lb2M z-<+t90exA9(J$ym?{^5Q)Heq^4pcsi<;pVV^4Yx29qUl(xst@yn=s1|nR|t%&I+5) z*;5^ArGF_+W#%5*y7|t_s~BT*KYNY%eM4V-S`ygQS~FZW7?he>HrtS-jPe>ga*@nA zT|;z2o+3e^P0ZPplj2RV$%R412ernGcX@?7=rxkHmlC*o?(!r(Ejzl7(typ0uVT5- zBTo=cvY`5wwlb!YR|dKIG{|k;mB=L|{pPOny_rokY`aMOL_5^sSfOoXD|TYry!c!0-$E4r4>kI6tNq`n=T+wF{o%DI zJ@jMl@zlj`F!MsrhbKLJe-N<3QOL-LBL~maUa}xf-vMLtqLbV6dVdMnSLc5?qvO&V zh~v2pdyXt5FXua#z59xLlb^dQaiF5m!U33&2wNAO=J{Ep`F%SC zB{2ckCr-B8-kGD8Y`f!rcJUg?h-Tl#`kd_%wshFplgJn3gnjKKTEgC^WVHQs#YhzP z8^WdKVBjF3Ezb)tYy9E6r6=)kMsZcpYkxSqMXM0gjd^CEEl?hmDF{uRVzZ%PzX*ELt;2Da$#(i%x8w_lOrI2qirPUQ_YPej-_DnBFP zHi2*~8e_U|1%K)$YIR4t;#$tJsHK&hG2BO#bB=;jXDvXrJ>3 zSx&`FGD=nntr(aa6;t+QH=#*jmH_1opWk>^Qg96v>S(nT6<=gqBlkfVwnUoPpqbqO z9+OKZ7G&Sy)+(BWfVnE1Dq6F;&NEbyS-XJ~)m2+*!{-6Qb0ZZ6uj= zfB#L++@87l)DN`41;VH&@*CRs$xvDQHRx;n!jIXK|GhN-AL>~81Hj|v1vYMf^OT7n z{Z+k@hhLtI-xt!$^^bjByO?MX-|z+GSSKX%u=~S{BPEc9k?T)dCfeG%wS_NyKSn`b z04SmmG*$B8(;?T>5rRkKkuR_s3xuW^9ejo*>Jlb&0+cE-@i>o?`Om4+}W6SW6_i6mV<*^pQSvZtHbeN z(tPSY%}sUApg`TzTU#QdVUKWMBK3Jam#Zm7uIfi3#Hy#6*w14nbr zR5!j)_mnypyzxJ;m;6|c_VLO8f1Mcofq9W5`8z5>?#|Cn1u)xb7!V@Ya(^7%`OX?= z&)>j>Y^y6cq7Jus_{FQIZ0^P*+wQH6x9X@^*K>H$!;M4EGqP;`wtM+pq%{;k&GU!H zA9Y96X>{sK8!1MvZyja-=|>v!&{zM`0;sUUr?al>N1OxGnjS3X?VUN55AfR#E~KZe zuQ!10e6C{oVcFX`-WL-K#TC)~<(dvnJ*u(4Gv6?mrC?d9xMJeAw(X~EH>K2UjCozp z_4%BbLqbd(K^`EhIbCmKUj$UHTf-GSOJ)yR(K8bEm0;D|0N8F>aYzwj^z2MRX1!L62 z%GooOq8)c0L8h}-RqA7>8rBQv_B0?*nqHtbO>;)~>vb4Q%&ek+&^3Rwg&DdGfoE@y z?$50%gVc#xC?V*hb953!G7?nD`6uXKIN({k5GpHw`DP~o+c2whwR3xrnJ;knxi80T zy3Kd03j54i@n)4 zpjZOg(-8{cBW(jyJp%$3yvjp}|9B7bUr*7s=9ELAc9|sUFhM-vFX}get6?vDGyrfL z;tzY){9qhpoftfhSB$o6_PjuGbu!ejMjjlkE`JGIW|Pa7_T-5u>`SR^RJHWnA>R5F zRkdMIYE0%!*-t$y_s`AU(@)%))I;8r_e75gsPbjOm-jy3Z zlvFCPM^}W&krM|1MlU*)ncR2oYK6`B_S-Ag59W7zbQ)d`0LKyJG&S^PQS{z_q;b3Y z+F-hcZ#%9-9Q>x~Ii}7m+%|++*8)3v*0>stlMGVgrx*Zn|Foo zMc?lU!6RI>pX(Hase(@Oa<~@5&I;sSWcwpz`GJwA?_4d~0E}pK-t+>Y4G}jkcrs~u zBw=W?wl2$%Q;TGzF$lK%=Jc5(zt%$qtU#*_@dB}20XI~c2DDy1jqD}1VC zNwIZE7i{SZw1kyduAe?htIWmoPMy%@ zK&mvY_t2#+!MyQUSyXVeM9)34$df4%`NY8weN7$Z=sC}w9{}4-UI}aAQr(X0>LqO# z=$B=P2qYnF`LsF8(N~l!Bc7ea)RYvJ$I`P>b>PO7^aY)N1^;?%Xje9B%G;yQP|?9z z)oR+%mNTr;Vj03TH(I0W@Zxg*)b{U;uUuhUQY9mBUYMiiqi0moU=z?!SK@>;Zjv~E zUS+-j0>tZB%$V=M99P|r)tLi(x<8ni(Pd?$P0{MSGK@loqQr{l4G<3RDe`j1XUKnG zG*!}EHqD6J4B~HMiTXSf>SwJ+IQGdhfoW0G(U0fE|5hV>f5m(u;v2t_KR!P7&jfyX z^XSk*kt>XPUgUbGwNK`{v8|>>*YM>XNWR~6+u~rBzW9R1uq#fME73z%@;apRwkfb3 z2;_lA09Fn=XTQ_aM3?MIs?_QW)p!yJn8-hx0_Qk4M<=ApkbTFO?jXuy>u|O|R*D;I z82G9{o-u%{ldncW#kmR`{-)}tf{?(tZg?D=`kF09utx9ZH77k;@?;kKQ719QEmRxfat`g{_Lb>^_9iU2-v?vH zBD56+r);g=f0`LXHY#HA22E&pJE1-fcASV7b%uZD9Us|CLBuj|fFN(~O+zUH=%tjrHylGQgTntsls2zJSl z!P!~;Hk&6(?@w=$6E9e)_H{U35S|^Z!Uu23j$A&SMz8uK`#$ZwVN&tgd#&zm6kA2X z>_5u5VP!Icd(hG*6;!cE*nvhC&qEx$-LiO7`dW;S8Rr5Qgmh8$e5Qe)YBb-2`GsX3 z&`eT$*)?5a?Cb*uAMN1lerja>2R~k56S#P%bc<`r$h0TCBXeW^%#+n=SGzz4>wS7H z?n3WYebhFFVFmz!e_bba_DW(CBSv~t46>~-Ih^&UXr>ew%_x2yN3koAg~I)EMf`E| z{crX2Gd~;x!|*_->hIUgSHZ@~WPVtBko!!VX>4$%YEyst zyuE4|l~rNLgwd~+p#>THa#hFl;C_zm>yr()e*+P+={mmHn56^he2F?>yf{`YV688U zc%RnS@fitDZ*%*JmpxhP2fDV|y29|tz;g&(bt`8iO8fs&_wMmf=k5RaP*M_hQ7MON zcXgyBhjC0&Z6!oe&MB73scDSU7!tO`)ZG?2CPfOPbvDL0q^TUnd6>aC#9%Nq#$Zmr z*K~jP^WE*Eqel;oChvJ&uj{&=*Yi5PaVNQwmwO!jRqh;#Y?K2lEBt6R zr~|iE7wO|fHFUVhV5;3I6pG@bH=u~*T2a2vTTm5rd)-Dc-Y5Uc_X)iEtQcPP=_X`kC<*`BQsuvnoc}C5(KiJ(V05~R z!@56;P9G^Qzsq2E!1a{p9RjVy^#kcY)ExS0XTf}a1yP0hoZ(D^Qygfa1@c={OJ@z! zbQ2dX>(6o-7X}h>Irn!Uxq_VoqpSc^QkB=DE4m}Y^xe>N7WzJ_Xttm;P56`)$=;mn zOx6oDZh`mB%Q`mMkmxpyNw&p!iV$p=fpNg_=4)HQ0%a`N9NJD8KZ2Bh%3xd2J*!uy zUgAW{X{ew>A)+V@6y67kCW-21gnxV&ZHr9o3{Q-Z#|?)40z!~V_MzKzU~L|O$_M6x z_QsG|UA#B8w*tiBfG>X%-qN@so+T=5r;d|d(I}CxL8gLU|HhfKcX*#SG5PERREFWGwP%*VTee z|9GCCVdy^qnSb?20YXr-Kbwiy{-bt9!S@%vnK&b*oUTwvk@Z(WRNqg8D^LQnO4-Rf z8=Ds~XuCN-bb4G&W1>{3iem5L=#w0AWh&x9bf3)S4+fr-D)#&YTZ|QnhkfnXtsfs{Oeai0)zG z#zgnbt;b{bkx8Z5g?X7+Gr|Z`xL_qLNdR-vh3XK8JG1jdHPf{MMLf)@IyjJ{hi2*F zGKHX*_7kjpIt#Y68EnSWD_EHbZ2CBxh!V`81rIe@$4Sm73Xgn z3STb2BB~nxEVfY}Pz>nc17IP+gMsds_DFJ}Gj$~Hc)I@xS{}kP71D)QL2ub|61tBs zs@4i}^#mhjc|gBF&BbkDdJiWb%}g6-Ef#sGF4nLSxu}h2;8?t9?fQR^#=pwvw^#Ht zMcTuvaZvo@f1X6`D$PK5hu5FIzGfY5L6k`z+(xsFBW~e0U<^4jBM0hU)7^%=1uMOz*D+J|oT4EJp zX8~WI&^gQOH1Q(Fd|c$Td-otZObp&FJ|!hKWTf-?VgFz%XRjT&16(KuC**?)9^KpR zU~aJ&br+;}UaG}-{j+ug4@Ehx5zl-_QYwmkYu|V*2 ziRMrc^>#;iqpVjotJM(q1_x%{ahFChlY&>6)2=#wH2=*5OwZw5nIxMk<1`O)VQqq3c!SUHf_u=Nz zX6A?D$*0f9Y%*=*3T8^^rVJD(^nWjG{=Wxhd-lbMt zssTk@0vgz9iiSC4-x2jtnYk6oxzW?^vE5z4HknQ%+1$YDPA;N{MVkz%{vxkZK>&Ke zA=CP-9-0$7ekYJtirrD}aVc5)bkHN|L#ExZkI&>PJ>rAr|>dgqWFzeT5 zEIP$GbG^YDbrl_=k!8tTYCgXLF;Eb8kX~WWF!DCdxEx&G!?vvMfmJvXp^OYFnm@3z z&tOttzCLxiLDsaN52BX-Pevb{gUGCxy#`EJDK32Zqps+ZCLD>BUzPTFuo=(s8({cf zGE{}*87oz9Zt-nyKtCDa=ax!5!Sso9iOH`>GY&1Cv* zPQQL9Za(Eq)i_JQ)qyMlBdxZ2PD*UB2F2ZW#{R@?eaw2t-on~}2Yc;h#3N%SL<1yd z=m|Bg47|&#NO>Y`_M+*^M7D2#bqGAkBHMJ+L>OD{=ayu*S#=R_Rzi zO0CK9=Y}JjCM!mb?vNLJD+XS|^R=o)1E{t|aNh0ARDL}v)y1ae1B!KZfOv3=#l&s# zGs;D!8D!xaxI;1)#+`bJ4J!8=prBSJbE#>K&Cck1cqM0MWBjJ_S3(&TX z7^^t-=st`)wrQ#3*(uy6mtgnxGUHB{UoAgpp{T-yb~p-~VNLwuHwW{3x&Gc@{Tq)Z zn*%ycqeEz_Rp&SuH|7Vm!Hh*UQpO@eR%KdN z*SL!_1EBst;=t_={xS(YIC33j$tzszHeV^SGh1UoRL_ejW z@7#3fNf<2BU(g>;f%$Jphaa1jklZR#Ju{bebCS|-?6jgphlqVr#5(Pu@| zevD5YuQHD^ht0 zHQvqB!vd3qod33!os}rHTeW9Wq+M6+|771!6d3y7N<+y5r88+cx?b@90+S5PJK3zNH zE*zLL6Z$*sbmrO!Xdei}Glptof})s>garJw@_+Xnzq4=PS!?<7azY$>5yN}5bI!gW zJKt^Zs&eNET3!I^>xFlb;*l6r!P4xlof*ByXrxRo%9VwQqW6LoE`z4u)VXjU3r88* zx@XC_#Bgn2RZJkIT!l0q*G<2NhKT*%mx~1J^{xIXh(DjXTebo1c%dQexW&?<79p?B zo!%$xb1##P<~-8P+4RR)=8u5+FAw*(4<-8xIOJ6boN8g?nK>p{6yJa2I%rT@o&}>d zsN>XffB&eeTV04TmMn68hp0=A8VpP^(#mA{LtW;6r-}eM0Yhz-m30yPdj7gKi&cr` ziS5SIY8%zqnAorxZ_sY;EeN~@)1P51A{zu3S6ExDn@i_`zn^~NA%17HBh9jK6h|?2 zGHmPq{L`;)RS%%d4JflqgaZ>-Usjl6O&Fy#jp)t{j{C1QrpVq4U!tc`{^o!rp;`we znoCG^Dn3b_2djH$O$1A$z6xS#2L=SDa~c8;0P=pLkGwLV$=sTsoUxfU9|}h|;Thqu zU2`CMqUxwadUs&B#MfM5WH4$bP*|~a22=SYeF2js>v-x_Lt%*c&&v@X-|eKny<8tT z`VY&|k;Zt*8j>-m->iS`sZQ_s=T|Z6__?7o!c3^q3zx#wFENJO=AMIUZsQk7fr%?p zxUFiUzre9v24IR*J^xt~05_tSX#3zBRzOFvc%&%Qp9K@qdyk3LE)#oNEM~`*%l&DC z5w87(AfT}V0pL1FP@&-l_5hen0W)lZhWge3bR1sns~M{wSUbK>Xvy2(bSg>HHz&^G zTPABU@QdP#KeuYKX`;J|Y*L+;z++tyR@do6~*q}D4#Yr9O`=QQSv3YS#3m5THgN8ZS)t3=$~k$Rpc*%w3+(P zYTW~?Z*g=sJrR{wBGI6jtgMo}DSPh$@-BV7Z1Se!y%)(&wSLFU2%r{j)&=p{c8XW=heUo9*e#h@3GC_bk3(9zK0DE6$`Ow%VK&X;6 z1Mx~0V(|jYUGv~p;=n>$&#jMI?db-)PX=q=+xJDT_)qOlElLiUPT2`e{qCJSJ+i^Q z3M@eosyEtG;E4M~5P~vMzfph-`^DiRkf;m5Y$r|})qi)9O>ASO~)C9p5@Rlk|j->dUd4hYt0c5Pf)MbKHr0e)g;OXK8V z2tD0bfxdSMx%_d=S0Oc3f8e#S223LX`Y57{2ku;ovd6j%yaAFea~rxNYs#A&T2ezT zv~LuoTD#Hr-aro5$N1*EN-U_K)z`waIbbI38r@BD)7NT}ToV=7Kb{3UFia1Q=xNmq3c;8D_o_sf6P`u5 z6on_qxxK1h$5Q0|PZA`kMm$J<^K@x~I8tJ+2irpq*&?z};t}$U)Hc_^nw=TJm%^B@ z*Q_5_+>pjz98}_}vr@)uJO^4BT86|eTc=Bjjf#foGmXMQz(+>kvk83t=B8}Cn(mu} zB(R|C(t4$!!Ew<%(xI9UrD!lVC2TE0sje?Y!6X1Xy1zs5f7JOX*yLDHFDHz?IGAA=TVVs zzJ%7kEfLiEBv|B2T!-0|9x~S-WHAC}TV3U_tmc(af8tfme)5X?wcfxjDxjMstJ+07 zI}_P|iyM4C;kR0VSwue6&%I7Zy7!3H?Y(DsOgv|h+ENk9o|*+^Fw~#$m6k*MiQV43 zszDcF$Ep3k9%(&be_whgz4hc5UyaYts1*zB`!ZfncHw>E?M_7j7qmT>LEPN&BEFax zCJOwq_(e*gwX=KGj<5vw%OYp@8m07?Ke{MLqzW4Pe?k+Rvu?w>X|JBrn|l66`qywW{dwYR?p zRq+gGOdnKTy4R>on>3$7_6Qo}0B*?b17OOrel;<&VJL>Li^px}upo=|sqtDhVC4-p zEX>t)1TDKT>OKD-FT-Cc%;%@_+c9&@0A4Y9Y`Q16AJd5#Xq@y^coYWYRR;FJ^m%0t z90^U>0hbrRG(+`XRe|DR{CJJJfXwU(7+5X7ewfV?Z6S73;H9#_g@L0>%br9@tlDZZ zw{r2jF!}kty~xRqXVL8jQmX}`z%&s#`X)hRN3T;!5p2>JR3dt8SV2q|qTgtfjEV~? zd)-)p^Muy+kW^f3t6}!r;Zhj6tk<3Bgv|wKm_pyIlFLzmT>a3Mv_#APsDL~3wcTbE z4rmgx{CgF;op{W=)O*XcSw4qGYuS}%Kjio3zLiO3vXE!Sr%Oyk;Zt&RUppkMEXfNts$G-e`!;F{$8!S9$>&8<{hIGJE9~q(;P4BWWlk+37MC-Vzcv<7BS&`bl&`k1^eE zmMyU)f+fBSpuH7~;NSw&PPl6|u(y1iLTH0Zas0rYRziv^a5_W%jq)7QegTq@p-%r2 zTINqrG59XX^Qb<>vE}j+m*CFtfy7?LEP956&_%Mn=2Ecy_SvRp+g#mz(WaG*TJPW6U&tZxx5 zP(Db(^B1YqetSCM`ecW0&*&!lUhCe__3IaP&c{fBp@(AwqQ>?xz|hGINXQSR?i$*% znA$tyfiW(FHN$qwfl6Rkym?{4MK5^>1cfU`zp+kyVl)0-Ea=TeNO(IP;X5jvVYu zY=4yzHe2beMG#>TP)h(()M4uv5Y;^wyRhwHnl9B9_*@&OqKP3uSb?JDf2C z`foH%6=aV8n-BQE`qo^Vs4!=te2M2W^qQ|e4LX$ox&LABF|v_+)g$f&E!=oLT7*yW zOC#|(QNY)Z2fM5XBe*jIj{f*kQThsrty!6>+jUuwt{DsFheC#43)M&L!BU37)dB-A ztba+y@>J;UPqRCy6DYNarQy1XPo6zUa4A9_Y}dQm8%R%Czozx4bbJj*Q3zAvU+w8! z<^Zmo?ugD9TT{yT0NLwC$WAcNxNy#OG#g_=841(A zO}9K;G3Qsa`Rh&<&L3=R-sjFE=Gf8vsBwFBx>b6K|YRuh64eEfhX>GOIa8+f+xD6<;hCk8EUkD%9^S zA@3@(UmNhIB~=fM#iIHX`1bPzQ8v_&v_<#{vi$Q(fa_jZh-j$_^QU3mt3?br5=5fD z3LS>0c>bV$znl%4`25mEc{}h$#+>V`no*FG{#KbNqMvH>gGYR*Rm7rFE2CF(Lunp2 z(Dmq@OgQ#;_Ue0c%R3lqQUi-^HL(<7q~0B6W9Z>Vui(is@=II6yZ|p#YL8-hFGUr29JyIA7A^4_xF2+8hLOTv1JG6d91F^=szqpv^Xanh2m_gnDp6g5+MONO zC1wo#h`T)X+;R126gy={kc8kiL+7^eQ=(lw5@iZD=`6{2Fg7zmnMfTN4+fYcQ8F3C z#;>X`Skd=?FUmRlN8#6fD2)|I1CjT;P_;yG8w|(q&&1XAbQ@t|VEymB=Vk3|8ex$F z!->rCR(Q>h2$s$_wTYdj&GQD#eD?&tF?YK76BVe(3f&;;`Hq&?@5gK~QAq8!`1<6~ z(nf&OYa~7#pI0D)D?-%e4D;zK_j%h_U4n^?GT~3ev>(L^@u&jr_md2|bZJuyQ7mE1A@m9mY#v}2t@S>ti z?X~OnotXw)%&it!z?@A#x$p}X3jT!x)~1W5ujHk1(svcDf$TgZA`E>4tHvG6{bo#- zhN7cw_F;ov#0cYvd;k-J zhSM+?!@An>9XHasB1Y&<=kdc30w8bDl-ejs_L&NtZPN29mDS4rk@R(lL>Eb@>eco zLk3?n4M?I8CLys8wjZD|kuO-JT)Lrs(GlPjI)h#Oum^e%UxNmdA!PuKp6I%S#X^yA z;-d*SdTQwcC4#KA)!AO`^{s6|ymcHz0_#EJQK6^ivEHBf&fG2b)BOpp&dpd>>lOoH zZ%}@OX<)t1FTErgL!nan&7`mHZ{>nNh~n5v#|X4$FlhefSbR!QMSn_FZIM{IJ*1eM z+Wvy=2UD=5VSBAuP;e(C7sKN1$hx_F%Fi$c*1 z3}{FB>JP~z1An@|0x|W3yL;^?f@^`U^Fj~nm>tVPnboh8-?%$1*`1}|T)hTj3T%V3 zh^?nm;`ad59!IUNQs6rh)OldBxVlMi|HY5qv!u9zcy95_vluWG(&E%2f)A`Xi{Sw(YjcCUs`&Q|&I4n+iRb7MhYw2Fy-0uc zsA4#c0L!Qt3FU##s#iS>1_E~VKyxok`Tw?>$F_|-=! zyaFY#^Gv#Y$Rw9IqqEQlToV^q8pD@Ly=p^dFV48;<7MLP!tpKsIuHRsFrHJpI6$Kw} z0Ee+)pH(xOSg9)fekHpQ5fRbBCDX9FaTcpc*Y;t*IwJ?t(IR4D%5W&nS-1t8I^+Vn z{YWiN;n=lqqeTIPF!AbUi~Lk73rh#MX#pT>Kxs#SEnzYvXi*>-l%Lo) z0eceLLR(-w?WJAV(CWjpBs2M+*@~|q165rBYvX1TO&*is4tMX{OqXJ6YNn3bB3E;`V%I?W8b%MifapNh4v$Os0MVDI zyhD_81+g?F92{*KpejI6x$+X`1jduI1}E8R#g4wvzu_S-h#qpWCo^-s>7EK?hZH9} zlEGjedWf|9DFCf;JRK+MkWq>eis}=5jYQdBdzX+aqW%shAj^U}##EGyjph?v{$ot? z7Z~f`T#MRDP}gIKEdS8%)b@Iz_1hW4J+;V(9?ChWlu~8HtcVCjg7Uq8`d+_*s#2J! zt^;Xb=+x(26CNT;pV$sebmouQNq*e%wbLCS2%UWiSSZvO@?+=P{+DlQ-mQ3H3PK~! zVokgdR4Ulx#zmE(@sJ2mTSE;VxwSJ$ltG^br7L}J!q@Qz-lfl}e*LGCeFz>Z8bMI4 z>{eA2tRE>_8pN-rxHh@dnL1!!WTxz~Z)%l96=k9tSn96Ty=3pikp0+qufC*m=g%W@ zxuqacETQ7%RcRi@+9EX6H0n)}-TI9JB&|J@%;1)J#3F-FRVVzLEAYSkR+OQrtYOt5 z|K%VFP+T&IwRV9)C;N*Bbs+ke@L;oB=$#P$k3@b@clzJ)({Z~&CQMI9CB9ZD?+_is z;K(n@uN0sA?=|2*eSD*f0Gwlb_(7j7g8WJ(TYwsn-|VU?+piyu14ghmTa}s$;Fcid6XIr7Guwy% zS3l-RjN8y}wE+L#-s}5|7>$`k2a@^j17fvcM~0#77`t-U*EcGw6T$w9V)L(mfkn}1 ztudIQha=X?Sp6;60LW$fFYMEo($WDKQCq%NQNz~{Ge14rylL3qb0ll~J%HN}lb)XY z_c!c+dq7~6hU)+Jz5n48a~wd*Lv@v*(cdFeaLpzEe|ycN;2NJSog3JJMq6%0mSvK7 zzRs_!Vq5z%nT;36jkIa;@Nh@ZR?sdY$f#1|uJ{dR^Sy#l#|SfYwx3Iq$LIY6UwS=i z@&FpqQ(O>)sTZl$QZYisZ@Ic2myQ>B^#qppbcBF8nCl>e#RWk$nQC;Vd^-f&^B2gdha$+-V0qv&}b?<&}F;-SF^ z5%B1yYDXdTP_+*S`pW||RiA=`(|YIMbU6V&Yl`DWoffP0XB6toe%%KQ$N=W5wR528 z;i{++_eN%qmMOzRRJ~r8JTZuuzHRxlKiZiKTULiTcm%kD$pe0GLKAhwmn>Vayc{r% zS~psHgKunN*3?JCkDFDHxKXVLVVM^_)Kh4(i;NHk-TJJ}_KTlhHX8`IA=qB+25ism zMNvVesBqGE5HoBHm{6ROynXFp7zsaYKL7L^OOOazNs zi#I#dw}@5WXN8dPipku{j4L}Lx5NTat*k7VS{0w{ZvJCakf=kK*obUoQPW~ z^9Pe)8{o$BGdxePMZd4?_5O$2I_~q#f-M!-va3w*yPrYDR_Im{o3GdpJFURXw~mXU z^mr_A**y6l_Vt{vEzoKHNku+{YWn6!MhfEkj3^aYHxX<2D zr?>>u0{VqSoGScAY>V-x85&@seZeB7%A6Y0mg3MYo%ahTQ)e5(P`n;w4?Jy55d7J) zo-Z}Z0Rqweq#3_@GT;|KRUlRk+7XyrVj5ROtE*(+#H`;`_PAFbWPc?fq>qS{w+`4x zaFT(i=Fnn4oTPH6r;Qx3dFBvo%1GXZ`{BM-kha13`4Ul?b1EnaHJa|tO%SG!hguc) zbRKJ(T?i+tpoPiA3$%AA{mB&$IjCP}_PA7-e^{9KKUyq4dS3UZT8Zl9%LqGvf__uW z4?}faB-1h*7fOif9vcs(wHMEevM}88N@GLPiMrN4TuU4({R*j{gsLDc7Uy$IIAbN8 zI>y&E_)(IgHio(iD@^mAosYqw$h}~zFIYrcpF00RNF2l$>HS0be2R?IiH#SMW~U0L z9$qZ_aIbn06(kg@-6I!kdt6xTor0Z1k`s{Pz4Y$`-rCv#AZhk(l5Ef(>!Y5RUvIs4 zRsC*A5zbKo@_gt}98&|-kNuR?AXJ6~hlXA(sj!stfx9_6E@e`#4VY9Xu7W70j9XxL zuWjWc+Okf@skCkt==nQ(F1Kv07&jQ748s!`i&`%UteUd7%I^@H)(K&Zq$3}Vk`z5d zu3hU`?fjxEIW+&AkL#9EYOGLW1d2V?lV<|0I7@6C9v&WhQfu1c%=Ys4$4+LiT&KXYclY^|-YPrLZum8t+`^Y&$w@PXO>`^q+ZjW+{*rnsgk6Uc_ zj;?#WTq$0xu&azfpzV`Vu!TG}hiu3V9p6AsQIvT&jF!^v9Dy7zJwA+f$%zr>S+Z8& zg&uCHLo)~Qu&VpRXc=g=_Y$qz*E=?L_*+J5kMHwfj0A=EKk9#`8LJ zdS^&5nf=mPZ-7|MN@M>mCnI;*jlFuU&=IfIN7>jdc1g`7{3mh9IfH$cb>31fXNNL3 z#yqX;g2%*9-m7yj-_Uo~S<3Tev~Z!=famY!mD?w@T%6Q>iIL*7o~$Sfa?gqd%0TRX zru@pqmCDa$7!=MUAdgSB?3EbzZV&d6F>LF|LaWlld`9YVdC`X2ZPvt_82c7Swgvwp z+eG6|?V7dQA|c{`!ymOe;x~m_7Uz#`GjCV8c|J}Ebvs?>E_K58dGg5^fr_&7FMK{< z{qkvyw6K$q8#5n*59J?ihsg-wpRTs}9#E)T>@_yoncLE_QILcnQ81i5gs4sU$ED0w z5EUMMTWJ-c_=aRLp>oh@=7kD8KEjz?pbeX5Xk{R_HG5FXP>yKjYm>`` zm4zWsEuw-`%w3cP^oV9{*MY>U7P87JUt-G+)W`WpQLzRthpbTfdn!8W(1rxTn?ZP<8Go-SrTe)Gd=C?{0sU-iB<=R)oX(?IHvqd-kd4tIH}mUFYb`H z;jykkUHkGTte4}jrlA>#5Beo47$}*A#H8a}N`mO~qm}KtgkpXZ4|US{Z@8SI8($O( zE@;7E@>PrYgJ7WTdAHX@pz-V zpKnwQ`{=dcEDAR0@&Ol>v?MVfgj-*0YwMT*M&lvZu&)$lM>G0O;_5=e&%HnkkZWFt zhmk0dl;=!F1EVhC6H-ao_W=G+m(0y+pq60 zfT@kr*tCu3-?X#V96`{?KV#-D!*7~9U~Iqi@CdzR*S@{3_Di(RT&q+Iv5?TqCma{s zx##j~HD~rm^ORSQ-^EIEuWh~%yJk;h3-^4U^?s%+!|Sp88uDbwnlb3rW-%C%NVJ^D zdVeKGSc%=E4Fu+`^vgprPbK9fGrzE~uq2eF2WPLjto8w$LH3eYTMDsnd$f{x(rDod z+%O@=rM!3*Rr&67-B5zk_x5pX4D3k5H>3*SsR#92QP)yCYFX!ISY;7~1|0_UpIz?1 z+&H7BmVs3zO4sH$9O^vKu*KLEUqi7;HBdgdYEO5ex5v{JU&evzG+5|>du*tmd4kzu zT}B^4I-*(beO0S6NxCne{+Kd}=uVV=ecd{fnza26!ojud6k-XPbds}s)plnuik*MRU~}%aKT9$+RoMDK|bS_)Pq;s-d5# zOA4)rp05V&% zQPqj>Ayq!D$)jZyFJCn7L0G%4=^G1D+BM2CqBqqIKt@d+UE~%{sd|Y2!T$iBAMKXMxxyG2o-1LB*c3PXH{eWUGD11x%PNrMIPt~>&OyjNl{(s~ z>tgjRV)4Vf`<^y(Zn3{y;lJ3sYEpUa!#Xz?f~NNLOPPC}5h{@9sJ*;qvCA?(wls=( z-LI%+BWm8^Vj@P3X27#5lsavfXF}B@?w}6c`lT*ThRzCpzVL+H@gq0j=d6`cYrKJ7 zVMYz!67ywBjN^_4ZOyp}C$YS~+ka|Vc5&M5Rr$P>X8wDB<)J$lxvwLoFS?f52( z;nK^S?I!2D7JcBY??NNSd3md(wVTh9jMYOk7Fl=VRR+quJ&HCGdcD8@JJ1hsFwF|v zc$js+9u$|6=FSb9%WugI(A1Ecy0-N2B!8OEeMJ>ZVH#&8t1aQb>3J@P9Q=DVxW5^-8d%wumIHB1v+@%&HghCFO zzq7-rDz)lrbe(a(dhtU}jp6Du-1jwP-=?zXe$*GvQjYN8N71tQnw4om)9cZ`jfvyF zUFr$Bj4rq&KJS-j6l|k=NFiB9HB)-m_2+3sb#hC{_^ap9L&`?>l8{iIlj|CD&))3u zxHnc;R0MSEV{A**^}El;;VS!*w!@*aU9-|`__nNw@wpRBQ=%=qdpNpKPov8;PVR-B zT6pztJ8NpN-%Ej!3A>8p4D3R3Hf1Da+T#cM9q!Y!zVOJ*#dP1mx4D!&gRvuow0- zLArwWs0=;h@9Nzf-Lv3typYlZf17qPt^?XuAd?86NL!q-KJehhsqLOF*9;{>q0Oz_ zHi3%VICxR%!}y%3#Le9&W^{+qN+l275x_gHsI=widfiQhrAlZ7gs1K&A2AsxD2;t;Ks7>C0WLwEMG|vi5KrYIU$0 zMLqgsS7xYT94)V_>LSr24Sp7TWy#g?ZCdn)Lj+^8vllhI8@5F);lcYfFPR0F!#m3M zl&MUN>;ol1E*eIY@ot7fHNX$*&|N&|*lo%&UA4p42L>S2^2en%qxW)nCwoMWKT3K$ z*no#M5tQr)QCqDwoaFJ{Cd{p=x#YKJxvb=$R7mT6ztc{*mFTSj&UD1|`>VJA7X7PD zSt}*)ON)0GMCeBVb!Q@&eM?X|D=Sg75#_Vs>rM*wCD5_jWzWH_%8+VGmsL zJ(Nch3)}M*4tQO%sVyhl*}Vmc|0++TSZg;qMcOE%8>MLs5^kJ3wgyh`RAfqo0b05sz`m7`N0PSh7I8JlF* zcP(CfXZ_m91ny*)>(T@v8Wn1nEL4q#$_uE6)&3s6?A-`3X4#deY89Ej!`D|Xtcij? zK2;Yk^UyXRSsbriH9k}#J#RbluwhE!!#T+)Z?%iZw>wi>PpQ;A5Q~-(%BPkg?xr4j zZ--gcTjCNc^mZ+D<+n2^n3FM2p_@-{|4FGMcjn*$Qq)hB%$1VqIn$XY&pp})+Qs7i zT-OM^ane`eOPSJ#bP*?>teQXg+KPK^R|={$eG}74-6xtJd2)t2j1JAnf*(XYPelzJ z4+(9Z2*IbDE9o2y)mL~mc!q7T&`LV@zIoVxPq(h*N?u|puUa&kIlBGef+ec3VYI`X z-7Np@c*M)D2<32fvh+SUB!il3Qb~~VX@WEYCCfs(bg9QN^YByU99ymdM6;n?If#yW z-pq%YE*3D75LIt&J)VGlvf9@z>(DMG>Cr+XQg!-N8>6z&oP8M(k(4-dDIHlOQkP_D zx9rS!4U-Eg^!q)&>xFQxk^dE+gtE)O)dIZW8}75ZV&KD1i;HO49;x&u2?W_*t@qM0 zr(R-?7u+WS$RvREckh&dyiievC1hewJxGz4S!ef1>>gA-bavFiBYQ>JCGV{7JaCH) z+NLeo1%$990=%v0gc@=14EL?u;ZnQ?kK{3f`l;EsK&!2R`0A7-WMwItUQx5`f2Hi` z-@xID;tlS`Z9Ha8WBGM-XT4D2#xsj72N|+$-r)j+wx;bDU!00^m6Em1D|!XOBQxib z9>&EIHHi>!o1!G~PURQ5ZQ2t4(3>x1a&1#fOdq@;W&vWWv&IS2AXEs?snwckIHjVT zWLq~JDkIh$^n#`oNe_BaAaf#h=l9L-SEq2*#e}kl(c+Y;GU(=(;V_wRyMlaDqg$^z z^{YRiBt?I;>#6avT2r@7^NC|%8-#~sycq!&F+G#?N_0XD7ps+~t2q^hca9b2`Niq^ zpbLpq0F?nWVNN&X&zVA@5E37e{Dhv~u(`7nXjymKT$+8@f_7%%mtF4J~oUBmFzJt5E z2#zH#D+b>eY4G-A%t{Rbv~y~QccWfqo$Tx42B6t2X>79`xTpIuNg8~swFAfgT7FT` z)yLrZ4SPPP!Hj-6OwS5xj`-&PDb|cq6h)4WYcAWSc5XbA8+lL?13lEbJuFniyhP#7 zn4CuU7^GNQ7Jwb|D!ZxSZHIibx9P@(?q1t$<{@49Fg_Q+bnKaapm51E&`Cj{r$3~&eRYz>!vWz>Y~;5h?M(M z&pX3EIyF5uk@WO&mee1gzvm7QBHcY2C;7t8S!UFBQqC-?kW9SE=wZmn+daZstryy& zOA0Z8Ggjv_JW|w@{?WVf>Ll$uXhw>>>MD|Vxhn#OmJxPV^x4DT^V>r+;7YZ5T3x-* zT}oZ4J~I0Goly@2#^ctO7)xW#Wq-DwxspmovM;4esM@yw{}40|zGA`_3&^|=M!vxO z5ArUvh-7sw9}Bg6CKxkacRrbPxAZfAJ8SmXR*Ih2na;Wh!!~+xI0yFkN&L~KAAT9h zTpuI(C*0hSA4oXhw%Yp?tOh9+XmS%!xc0Np&&T#x8aTiH$99<1^!vNexLoMaDFzv2 zU}QR<%<0DDd%xROv~onLJ2NXt#kHo6 zYO6Asd6H=kH%EjT>Gb%*si>feCyC!Ac$KyI+T{g}F_Tx4ts2SzkP>>Uf%w3*t1QhC zs_ApeIF1qc?s2_zelvSXcI|IRK4&e?PFUT4bk6#_y3aLmU)!S6cLr+fFVAI~3+=wm zsXcA>2WiVW{17#6^k9yVXWp@qM}M$99r3{Ppzk05QJHxpX{y{KZSsfTe&qk8X^iqg znn0F8&7dzpyXTR_CaffpAlUe1|H(ylCJ zHs=~ofiIihouy79Bq5Z6sB5anoVQmi zHc>s-gBaJEf}AF~UYK;~dNMNwF;^S)DSl%UU-n4xYMy~|b8e6q7394@?#ruqB{?aE z`v^GO=p(`sS&bJ>C~GdynlEGBWm3|`a$%Gu#A8&{!a~9WvIdO^Dh|IX$LtZ1oy1u` zJM%~CG>W=5z`TwfdQ<4^|9Z=3_EvU(fYMnjh#SLAPQaOi|FZG3yqVLwxeLa{V*jds z6=5PCbuiiZ^y-frOpWbpA2yWQc?PHGzB`QFqGb+IbQo_t%y%-2kcny9W*E026hL9L z)IiEcLGBP#QdnuHrXZDrdeJCpyg(V%3a9AUA#x`&9@>}OMxE84sFI#>+ zQ`+hKPVQjbQmN%d$+o-tn3Nl{*E906Ds0Mj z%loeQ9yn@GDMq@p*}zzLFvWbehCk8r%?hJrd!%25f%xGgy zg?R5PRZ~e)T1tY~0)^DS3V;*=jKX6%!ExsZiO&w@HwY=xD5K}bs;OW+x!5C~L?3|Dbl))0}-8M5=bv04uxD($}gG(pz{N8*25)NX8Si`OPqU0il8x(yMs@=d$^SNu1UYr`}jzChH+TW&4S%sP256|>K`8~ zA{ap0fN*^}pO`>@IW2ZYit}+~&NMD(%-YK>d1c5zS&$SJ+ax8lU7H0`oJVrbvgzNM z^SXq9Aj<48X!zz_9MN+0F+b1m!AgkAnmjf<;_Xq!6}Kl1!Ve%d{xOiBVR(W3YlCol zY};}6shMt*b9=6~x<^yW&85!efI(nsX&^0VpuPG4{dT5d2}vv{CCTM-GSK0(h6@*C z`K8^?4mk^NpufP3?fzBibGwJ!dv|ByefR}&2=ieY_1p9>}r+nZn|6VJ&e8v(rYCHb#-R) zKj*x9A8=G68yakbsABHvJWpb@5v1`=0-rRMjd><-2Q^dlGWJuLmi0or<0>QcZO3M) zW^uq#mb3H+b$KNC9_~;rl+-Rg)0x)ZkDQD&e()mOSKzID(4o^Ts83jRraRYy`!3Li zU)COcX1(zK{j2>o(hslKW)R^(Vn>!i2r7i$JP>W(y;10Aofouc39G@w%0Pj}*-{Vk zuo?$SWIiq=zdaZ&tNn`>h9EukJ~X#~M!M^4y3)y<(CX(VlEl0&f%2goh@m7AD%Voy=u_MvwDVDu%CJpNa(7#*q9kS{gGm`MhFD<~fpnmrBsr-RB*J{)`4Yb`=x`RgUi9O!3W3#Qo?)H+F$ z_;n#^ehK~$9p>WM6ea*U>UT(^rz$rmzMpLDKDhbeLFI(^0Z*^(#y$J?k1kPcwddOZ zkG(hlhjRb_$1RmC(T%sg_c7G|x~jAB#{K7NmcKF!1AtrZFX8xE!=hN^p`RG$ zr}Ly818u8zbl<*G8(wSs-F=i`omiooYi0?1^_DRYt5cnCma2lF(?_zJVs=pI%sZi-T30m6-?E^|t*YV;u z4{qf>|L6f~rk$lz%0ST^L^3N%^tP`S=zTr<{I?Wk+eywOa6mk2La#aa{J6|i#ys%@ zn9feO_epqjm6FFCI%Om!1#~^<_PXbtY)z)VX^vhme`W^a<0s~+&R3i z+-Uq>$^0z!2ezGiCCdl;Oa_xdqyuJ5>w%|f6aNUuyR%!Kt-`!Lj#-cJrvLNHe`sHm zC_GAi#8o+gzEIMlu|so_P?zHg31e>yPxB7aKR#=+g)%)~P^l*wNB=nPEh&2CAOLF- z6H1I(=O1%(Yq}(zeeCXMo{2mMBu~MW<2gwlz2xcLu|oXK4UcC9puP6WIk&mC`Z%>_ zWsfDs#~?5A`%l?Fu-HE~XGlAwveXhwBrczJ+ByV>&uecqGvJPDd_jAWAmcCBw)Z#c zkaDc(GoP%4THNjj87Zg2d>vD|b1%HzPfUG+tK_-B&@{Vha0|>MX-KM0UA^rgn%Lng zYUFb-K9sNs!)Gw0-Ik_$3+2Gp0O(B!fA8)Eil-stx){Da`_wy;Rtu~&J45554BH
Vt+~bP7H+jP_r`9zFg~`XNM;50Rfim8`Y}wh8{HF6w`D$N_@BFmw$IWzwVFw*gH< zkcdOahGu`?xv|qgUeu)%wyhZhc{u5CBnOJ4f07@?kryJPm9v3<=vm&jgPVVM83x@w#0uh># zL=5+!DW;J(7`kQoC(9y}_7~75v#UdiLx4xA&4Zst=9g>xO+e^RV!mVS4q-{jA$LWe zt5eZzN`(ovUz3OrE!+Jl7Ph<`M!*3i5zVh*o(9E^DgomZlHRgs$dGzgLs%WGo8p#H$PLkiLDQ9C=;cNMq)i2~d zU=~FG?n+ACNjE&xfV9~yOFS2`>;Ja`oqr^xx6nIaNr`E}hn)0IY`?odu!;~BS zVXc^j2MhELy1ABY^k1|CJf63lwsF?yLBXuab6!;1H`=ik+>7d3z^|P0`MEuU`37o# zjHj*10H=%WJSikJH4a!djJ`AG4Q6g`Mc8@R^XFk{!?fG8x6f}(vheV==sm7pR)gda zR!xxTW46DD)=zQM02`TR$Nb#13?2HSv_&U9sg%Z|e5b)3v;JJN&3mgoqP7 z+Og$=UwFL&W<2#Gn!(gLT<3ss`qD(CW0A?0+aF*zFT$>sw;X4D;kfF+ay0ZfPMLgo zmHxJnJ4;`L5k>j6J)L5)pjRtC*Ugs8BfUXoC86$Bh(ucG6h|xTURIUt zBc^(bUMa-oEs&s{d&BY&F2X<|Xg#Dpb}oUL+u?|Pt0LSAB`}8@&{x_w%u|B^gTH+V zCUCGZhhZ6@k1%ok^2)YLo?ugYE8TERSHmZn<}9iMii5{Eg6efe9Kl-YNYKo(NH3x5 zc2@i7OcM+ug?O7sZccyE&q53B>iiyjRp5AlBF7+@|MYdIV6dB^A);?;CKG0lKU z@oakmix!{^e4X<)?}2XNFq!fO4|lvTFi|SnvtB$Mu5Y@Ku3y}V-!oH6qP-YEl};_` zP?#ux=}jwdtdeCH8wX7?btnBB?a|VkdUWL|8Q8Zab`2A*aC&4GJ%Dmu(!DR_hcf(6 zSPEqL^yRydi7eZ}<(-p-fqGUGfwz#@DZejmS2p4)uyGgx7O_tfo+X}hxVeG_ z`)TN#*9C$CZxlvN4%*T6cUM#9R3e16&$NK3l?4Ll199Z&Yz`IkL{c-WTJ!2v+5{P8 zN6M-ab?qQjiFH9E3LG`<28%||o(a5L^Ae04NBqDbMA@*%GG07_`xWZ$dms^Sa3)DQ zJoBtB$ClRDc-u?mNtnIW0JB9|)Mn97G}Z#U=kbkr_zE<;~Ppq%jrgQzO>h|J@J+b>bi`{@T_z*&ED5vewO&&-^ge~a*NgC?bd z*TxyzvYWT&Ujba?*MC9Hr0^L7)L4W$1^1HP@X*L#zy)P-i zqaXMtour5u>|6ZcV?&wb?@tM1GAnT*-Hg{P5eTl3!6-r4&2IB+klSdY6pxq*bU1HM znL&3{dEWF*D$75KBis@!g#Lkv{ZCl!5DA9Rr0*Z`P5Em%@-OlH#lES3$p$^xv+! zn`5s}@+FDz(e+g}wGrssu0>$9hH^#4oBX<_O;p2r6doI=voL$Pez7GmoB4qI&skIr zXBqAo*zHh3b0`fuxjMBJ2oXKs@qlUUKyEvFaoOO`^t1wvU-@G|28x0_@>wltta<`W*N_B%qdq&Ap6{>=;QZwAPuZ;!e(sCgiw^Ho=b zRLQ=unpIA`b`Xu{F7){R)PTMheiCQ;86+1fVq9DnPIIPhe;27oM{qj?_b;KgJPUIbmnHhq84u2X?=Vc?c*$&M2&zFjH&cA*i2->TeU4bG&UY z*XnZFy>qk(W1f^&JA7cEd3B(VeKp$Od?_Glfe8XS@@$;4>~f<9cJ>8KpQdreXK-JP zF$ld^rMR`B?ALdo^l{gN46eX1IwydqO=;aG9DFu|$?su?KDvwJ&!|c-zt~2_UCW-$ z9JxZ(@Xv2kU3c|b0_#K7+Qi1IechNJy#N|-!WH;XX-edj4k@INVaIC?V8KZ`;>vjK zo~mJL1DrQl^t-oNBF!pWLri9821~W(>MBtiML!*Ss%X~j7RlI<`*L#m^^j7j)$~`e zWAHCWv()8b75+|@N5;r61?3{9S8lnh(puVvyA6Nc3(Ahf3ESdD_JXf^3Du|7<;?bd=>DY0@QNryWD9KlVSB94r-+*PZ!f&ei6Sm7#}Zm5 zeYPomvOb>--(UtZ1Us?(EHd}~emoJIj(_t7o7su=aeuf}G>0E4yv<~KnxVL-j^V_V z8*{`$7bZC=?8V{N_b|BRrG}AvMj z3+5szsHI>#+p&DQ-V=82AT(>*Rg50VS}3an_>c6RbwGjnkg?L=TaIoWjnWcI{*sczj*Br;y7sl;WY^18GY-n|wBcyy z9vtm#xya29TAzApwqaTGQ(kYl1eTV+oQkBRoeVO?>8zXx9h>xP(NUD-%BQ*TKDdA7 z<^wdVO!-J-4c^VPS_^|DJlP@8gy{DUoUJ1-{f>@_n6&Ik?85~tw0sZ9oyt^!ZSSD7 z9%gAjXwN8vz~YgF0~I18t|aKvR|ZvM_?{HODk8tA#hsXx8m}ebU3s4%vVZaP5Q`mN z+3P_b-$D#rJm)*ciK-9y2kdwz1p~=Vcx4qgE&d$1dHA_#nZzN_ z-vQ2d{Pld2Jnry@Or+A$JxLo+QM)JIN1A(qWrAHYf-r zT^q!TfLN|ozR5^STPp5(q`8*9mFKxq_6!}x94nlE-!c0FWs04JUp9qbo$P(l_GndF zY$as@s?*ldD)3ay ze%|2ny?V1+iwtV{7CQn1>;zFAku_gN}M9+E^fOls@8PLT%d$vq;HQKYa;VrUo(bdKi)6_+z3R^2H29@xTGqsu4d?YPxDfpwcOHD{j%*$= z!V!627Dh+B^7pucl(us|?VfL}Go#{rs^RD|X85HI;dO%wVDPiLp1&)>ulHSuoOezTj6vl_vg2|?w!UfoKqmNsKmMt`EsRC6C5z2mzv)E~wykHVNzZu#9hY4sqc2eArtUKd(AGvMO8W#F0lzu8e_JC!Csi$= zP2&hlfsUC(1)ZvIuSV3nj8@1iY*f`ddgA#i{IC&(OeVvsb4?vNj>9z8V0}Q$-n@(J z>n?N7OXXr+L<~*amqSX|FI-j|*T%_U3Fx461f);%(Ty|!L(g*1(KSa0?#VJNE229_ zI23QD8j}b`p89!E^c|=0s4m#KZboUUlcqHZlOUndpD;DqckBR-vO z9g!ziOnhio^~?QP&247g`htFbw+E|KZg%fo>$akCIX=6f)Nkak7_as2eEL?z~bx+1DmxFqW(?xWi1ffhk z2s`4*`cm;}t(q&aQ-Xlch1&9nqq()O-fY_tiR}vcfQ6dEXR?PNNr997OH;pFniPv% zprC=E!B79nzuR!fpb7;bx%>u_6iJ?0e z5lD_>U$fdq*6qJmLrh#19!Ki2CMI0yVMx|@#K{1Ah=;e>l;Kh=a-$&x?c(xDuq1G^xVD)@tAzmh6kGsV!oU@^{&!Bs_VtOrMZYAo_zxp1VR^4hocisf|?n z^yQQblJr_U>}V(2ki4inDmU#cwVnVW(|hjeWOgxW^YinRLo(>$Xno>Byj^yG!W@3= zQza9S(=-6m78nr*OaP@>WxJMyR!&~LGZCwd!A{Vtz2>ldJV~FQd_;@#H}~#+xs9YF zIHC1pE&p>OHN|`g69hs@3*pV^%Np1Dqv7M?$Q&M+uXAC_LXzXaIH^$OYBQ zduQrG#?^S2WBe;9?IzAa`)?(^$@fV>qJ|5})Gx|fV49Q0Jk`Y=7 zx&iIUG~x}GmzQs0j2~whAwhHtA7>7|S`rlqf4c!4ftq^<43bC*skXRi#eJ{*pAQxf zNO$htawB5e*p;L*=;=GqQL7pEF2?1CDAyue;}&D=tvFclm|9*n-s3Ln{_zpLM2lA8 z4$R)+z8NuI)|mC9=UbIdlzUbn8tNG)7%Xal(PA-c%(5)K)2Hi(8KD=sw382OOw=jX z8L{`epP>B|u?{Y(i@@>!L+Sn8>p8rLd_F;NVRj!k`Zns|dUxC(QBy>i_qC{m?QcWm zag(ricT+eQ!hq}JLZ&Md93y7@tZ5TI$p0pnJZlPK{plC@zh{rZtv);C5O^LNv^--U*w>fs%lgK!H_sTskbeb^^Cu&A@v%z z;lv{0wqQ$Hpzo4Wev~cr(#`1gzTXqdhd5c*>Wnm@oeE6q0233oJ}78f-GAp^94yxO z_TuRqSs&r6!pWG5hiZ27;Mmw_-e4gX*lgf?$1)sfnH)1d*WUdi$_bZMQ?kF|gtqh< z;eR0XjjIlQ1Ufup?(D1=qe`(?xUbptQh5p%uXhs7oq5Bqi?5)>DEl?ZLT6%e z$&4nhiF4(YA7%(bIRf_M8y~+plKbeSrqKAIb%I{GrOxt`~z_9pB-Z@NOx97dzLOGSdb>BRpoQ z!k4!nS&2#8_9OoJA2u6O+6<;n|v*++AJoyn&<}2Y4 z4on#Lx(oeg+whUXhl5M2^*#QvqyD@au$k|D`YWma=?~^Q>$y73$6)5_`YM&@4Wr%Z z!wt*g6aqQ<#oDG7WVp*<4~^PKG|0xy+-=fa4M95zjK}KLG+={Q^go*7)BcnTJ0qGx zo``+lF}*vChufe`rQl6#fogQ1ISSl9VU>b$<}6S~?3rwbO5Fk4-mtBc=tOUClZsYq zVF1i9GJl}U8$2V$wkhY@EK(P-@J1KK{_x+LV8LZRmNYrC)kRE9OgX-)s;Y_J3l0q1 z4MsGoQFG2bEZV%DBOs8vVFU8{`hJfw?(8XJ=;$n_ZdJiOCmYf{H5+VCL`OUFK+QQc8`D^ z>{2_QKL*#ge~29guXRI2030T%V}S}YZA7nnm$K(`1{N~^?!z_aD&{Gt^g& z)*;xEb7y<$EoD{7zPs6(JZEcx#m`%AZV5Fg9FMGwiHX5n!+wTBq>1-vmqczh^XZZs9-IeO zxr$9iY5`4$e{I&^KXqRR9|xDw>WlgzUq5+BTN^~ue!SCt11--wn1G8Pd-VGTWE>K^ zW(1d>faLVHh}*J(b&o&-PIrXNK}2``x*bq@;CsN^>@G(8{X{iCR$rsNHm)d|c3x^D z21~F2m#vj*y}`IUQ2wUE5s(V~FVYQuJm;US9Xz`NUG)V=$m=%!5VU`KS?$)E8i`NS z1~%LQ9%&*9k}j|ASo?)damR)*8QMv$J6+R{|GEY1Js>%5I3Bqtut6YSgSWQqJOl$D2)ri`EWb`TLh`_Id)SGH zB$NL#MsH0@P1~lngk0Y-XKayV;(R^vyw0z;JN&s;S>5sc?XSNCg?465;ClPPF;|-J zUFUTR<>3(8zYJil=PyfIc%=>Hf^ms;z1+ah}0?%aB<=&=VmQ{q3h zA-pJ zbHJ^asVN8lvxtHaa1ri9Dc?3^8^DMSWMp7Na`Jjc(v-lZP3-_%RR^3kr`IPm)Ng^* zUYbA3h80rvs(u!8|7NE8nzuIIZ^zoOY*0MK)&gC_7@pPjPXq1K{}YG5Y4`uc;cpfF zf8y{ztMmU0ahOUw^=AyW`7~X}iuS2POa1PibtaQ11hHl_;}5Z6gOZ82rlZ{3?rB7O^I8rkexX9Pbaffp2VKm)o7 zh^n7Q#~%!K%0ZtxIfv^|cr{I)v37J_x55H z)o_mauUXYwGphva4vTdU1y9!keJ8BiAbHt|&*MM>;raRr!w^RhylC9=BGmzjZus`` zkv6>f9=@#Dw|DW)446GgEy;)z8&cNTH3+M(uoYPCbQ~nme6HwMP~(5*>Ha3ECjhQy z0IwCe5tZ?TOoOEcSYUl(^7^P?t6Rt#J-01XYV7jl{}_~h0WP4}6!fh6@R!SUR{t}?Oexn&gy&bn&GCR2 z>G5b+{CAakYpZk(PmJEU%Fwk{#w@@!c6-um*m1dku5*h2tdg{oYei++7$Hzea-Fcw zN6UyZj~TV$UC(2SDEAg(`c0GeJg`fu+>j%h%Lbo4UHEVg?-x(g#}V>Yrjp5;zb-hq zrVgb2@~N4r^-!p*x5d}MOUV^n@|&eldC<@+u=-l_v;ScL&DovK;fQ z1pp>5VJQw$3E{4dktFgS5Hv!1La|^?;g(FLLeJp`+^4PB7>n&&1yp<>g@bgF=I` zR6>z_EwDE!l~xFDnx=ZAX!p;@I{5q@OvI5uos?_quOM&Yi%z7^4d1hk4o_2&o@910 z%aUpBukbn&1tbMKPKAy>0anY0Y=zk1MWy;*rQHt`~s8=5>ol7+7cs`fo~FD(g?zhDJfp*-{q9=%z=3gZL8mzCXlgJ~$$5a|rY?QU0p zuRj!B(e%#3snWl?k^B2UKe+a5GadN$c+jZ1T7V}X_GCVQM9`y@osNz0;JuchT6hU? zuEEpRUZkn_O(%Ug%U0rBQB8SEF6 zPx)BpB;pjtHI{-9%5uv5yNp_=Wj#Lh(i`}&&F=VSWw{KYe>j?3uqG~{g=_h+ea2aU z=SurCY_F>naB_+;iCe8i$q+`yn2Ld6V(`T98hO5s{3`4Ec|<&1^~Fzt;v<4Vlz-I@ z*A0GW?Y|m(#uZ2Si#Pjo9pW2yaZ87Od$(b1qNWejt>uX)UA$es9)_^XTiw03e58GUwV?%AwjGLvZ!0jH?>Tz3r@kN(dR7Qd3T@NY4L zsDl;p9Xm#9@E(~K+5=u=7(RC59KJnE8oRA=ej)|@Xqul`tC7D*>oCyLl8uX8>z+s# zxiG@mGexAufoZ)Lp;d!-C*nC(YttgJoFx0T;?`}+h%LuAwL0!PpAwLGLaJMyr>()1 zgqX-BI>|n_!VLxv_B^=!SXMP(rHoZ|+C-??%Lj8ECB34tPe5P2DHHlfO_?5$cXM+{ z_Ar|3SSA$pZ_({6m~Vovz5ol{jYfOj!MEHv2aJ+GGL4Yj%0bEs;rAqd{T_Eg4Sg;Z zq`UV!-AcaJ{%+`dEoj0wp#K3mvCDtqZNPI2Z3&*ttx=O3}%n zx-7yaS23m0tOs6bZ<9`BF6_`(V%Z0_lG7&mzj(pzIk?fni-FHU4AKghvb%6ojd~~j z#qd2#lh*o=sEe5()+&tiD#^Fat7H`l$ur+yRC#9`G zyd$1hb~k=FkWMpJS#@&rRP!$)Jia7_C5d5c(r2b+%2+G^x*X>I^(Zj0BF$6FurN`DC)t3 z2W3;!)3M+olp0NG7i099zr#2EE9(DhlG9pZNn|}H2E+DH&^Sp!M^Px0z3E2x1kr7(Q@ui=|g9WXmq$H=Wm~9KS6m2ul(2PELJDUcKa7IgLC2xaFOB*C+ z*^-`QPu`{$UN16;jlXvI&wmMtsJ=xs2DNCFW}rl%E}X6k(!-CYrx9g;CM5 zz~b028FdIIWv)^|%v69f8q}khDPAYXhMkgDqkO2o7Leq{GsN}4>Gn;;aRxk?Zs^RQ z`MZ@t*_2*;=E8&rV&1LP21lf+9}L!>?a*6n5f?cFJ95VVI5d3spl>vp;s+l}2BsY< z=2s8WOH=Y^%iVAB1ATrPE_HBY=3OA4=3V!dEh%PPm=#smu36v}a)e1h^-?gYQbDKya`V;@Qu^K0R<= zp(EewX8rz|_cM+~G1@=q8DhNm3taMLgRevMnKO6=DQ@w^i zHYq8#qoc!VK5A3>`1weWA;N*upqbnApN}Kx2Kkva5ab>31%0Nxyd$4AWJays41ep- zShV7NDfmr5Gk~pzo_trGR`>G$FOToh2A}LC#*_;K?85k8e4|B&vUa<|1N0T0siEF} zrxnRSz%?$N;l(6aU8KP9ug>rW9zz4(paPQ>Ds%?GuCcfz`%;ncw=mn; zBWT*fD|DBri9n~LSyJ6x{vb^PQla$8l-9{WU|dKh!u#1@vIUhY-sdKxionBLkMK+% zS5&$2^316Bs|ul|j=*#2g29~$cF82D?U{qY10Jvi%C_fE8tgL#1E=6R@je$xQHHXR zl0!V6T~6$=k_Mj^pIhnsg)(2MLo}>HlmE%%)@ZM#H}&s>fKo$0UK-%dp`TV}1#w45 zM;8hMZAMoi+$npW4q(*K34^k6Qi8i#RGzZ~W3dT=~gzwG}l<^6R1qwC;oqSVpd&yK0a7C&cEjF5zg?pWNF2*pX~KDnq$yXjQF zQ9m1O$$ZqlJ3YwkR6hzWARkq+4VC|16etXBAZf&nQ{h>oTR8V^ za`NvPcy<>3#v*Vn@9L9nlyW?yilTSMw+Z8SIG^ua#HKM9P_f>g&dmvp9H zm{0gtq?2Xw)OQ_ZNqK(lFSCau%_VJirHut@>PcCsC(! zwu92sVi=39q6ThaItFs5O5CB5Gq7ZJHL6uwJ0<8v*nY&xMQ?!Nd)IMaU;5j>-~S`%D`;Gc z5EsVV)HiCtc58odv)#x|E|4D=|HxAvEC69swWqf7f#uIMQP{5MN-K=bG=>pSE%9NR z?{~7>Y@yOccc^;d7IHT#f(t4r<(`j~Oy_@U*>OW)^58jPC29*Vy_ z62C;iRLq9HfBkw%OINqjwunoo5}K%3^q?E|cBD;&(qy;a+9A`~<g8HShlZEJh7% zdC?CTFfrfiP}-resxz?g07q05Vu5$sGpFenDHv)@BMb{pVnNWBC^{3ID0Rd6Vy5dO zWzz&iNDK3+V_8NmT7H=?ItmM$e8xw{*h|Pl>1x1C)K>}T(+wVC?~^((P&Low4$fRO z2-gO>5MG6qK-(k1XxONvI@Qt?tLNH^#Oek5Nk+@NMr>mBPtMgvEX)j$7Ptc>@1UU0 zP1}}sYxdA$Lqol8SrXDX9Ip?*7YOMsn@^N>x}|nm0;a)IjM37y)a-E4jf^fFM zPBJ!OuwIV%ywqC6;F~mYveeNMfi??!L z**J0}EA6gN`uHHjX9;vwU2q6m&z-~2@7mjyY6Ksn4zd&nA`LfXGA6X;OFv?J@8elh z`gri<`Hlee%xW(Eo#W#)8nIY#fz2R7G1tV9e!3`}!EDl$`pB>=#9MK)n)<-$L3%VV z6gUvUK4;XG<0`D{Q8=`4B^Gf~;0Ods)H6x714I$PTNB$jb!&jYa(XBt(F{943@8sa63@@-{5&@i2vsUv>QGOv zHY+dNfjV`Jj0TX*nTpv&BXQ~qpMNa)vM(bh_`+d~k@#z>p--cD{t36~cqqyx4t#HN zS>=QI*X$X8yz5?fjE2u$neui6aiVK`NjYyV{OvTJGYxCfAP054@GikkHbsDb-H280 z@=^S8PN&g#JhKIssNIPb5$sgQLGemB`>OjTgtLZPl+BmQ1r6NWUOUv`s#$MsI*B47 zz9f$TMT#HY{t5m|m<8f+E!+n@xb{dXwu?+-opyz|yEjKRE|!-PCAf2SVw471v|GJJ zLprR@%q*I3J#SLy@Lpd?nv15+-lF+BvS21DEY1Mo?`zJTj;LFRmleY-G?ZpisLNCeRgAh8 z;7LBdyo;qFas0)MifTUM+or1z#Gfyoj(DOabp^Qb1d1oR@bNCQk`}(NMFGB<3TWVW zJbOYTrU`=Ld|(e#P!r4Lju)@sNt-ANPp;e!HLh`Z71~eipYQizwaY9Ufoab2)w$c! zpMa%l%mS+`XMvt$MPbmejpEQC8~aN3TN`~ew^A1s)V7K5ttFP-hA3=}M58$6l_*Al zbfShdh#0l8tjQc)lNA(wy_&_M$+eko5%O@Wzs>i0fr^F3V^J&B?3KD@$}~nJolGvP zECZP0&==y$&^U1@vaA1--k{%m2i-vK+8Xu^|Xiydx--sw}F}nS5M5<0Fy<<9>rqhEL zy7*uzH;DP&u`p=yhYV-@RQ0HUHB{plNGZlVSf+s^}hnImu)o!&q%S)GWBv(E)%(5nf`xZ%}LQHSAZS7%`hrLoZ~D=yW|9+Z!Y#C!zUG1S`Wfz0%ugg%%XlKy|-($Qjvi7R&r?#TAePX zz|gC=EGAQF0&L0m_}@&Xzf~r}?RE27%EN1kkgU$to%$C>b57G=b4+#OvCGovz6ZAc z$y28>#aI{W_nC^zX!AGtl=g;HLrocW7Do-bbc%Thg5D7}mi9Qv_>ar(7~=T&@?ZE* z<_xCZZ5dkO;E45IuHtv375+-r&*ML=jY8NJ@n8>~^ufNL6a7CkXf!a{OxBLs@4m4r zpRzMyuXlk0m=04a>H7Ss+!Y~%Z`_+h)R7k;muZJyCVRQXnjYhOJfQ$u1;l_%7RB=go+EIb7E7D+}4mbN$=UBeOKa_L6g-Y#wJiZ>OR zR^@?#>BV_=laswv;{C4)Q=bPB2p5|haq*4_gppD!VNc>kKC!kcE?0koHft5xu$R@o;~7qDueU^L&TJiK!_| z{g&3Z$mFE!)=;>6UT0_9Y=DcaYivo$-jgw^4ev`@eS&KXgIkQ<{R4e0zy{JI*rrSQ zJSuOJRd|A@rgL)At5eNG{@~eJ5fJbuM@NMx^fVHt$d{vFtHLJ}P7s(;C2Jh*C2Kro zyQWllD(|16p*sgFimJZNOHIx0E4NAL#BELITz@iJTvKYmbLI8{jS%U}#UBJEyl%yF zFG7X3f6*aBS4J2EmORe~`Bd5OA*@TUUCGIJ#(WUSz3`w{Pw(hSA5L4owW;Q+>&vT0 z{xWO%KD1TV%BT3Ew5G09?#9TA$SAc2w*^Q!7u_itsgo+>|ej^iVEbqM~s_? zo1H>c&+0dcmZ<)5Qv#@2c8TPziEyh940`0ZG8|*eO-I#u;vS0ivD;SghU6pqPO3o* z!zXfgk(pCdZtW6jAGh&#hcWuM0BO}>VVk1_Yj~*qLx<&f>LTu+(ni4oUtEJs5yx^m zov&adwtqU6!Pk>)Ap9rfl--m=^|z{iW6v*v(_KOY{c{61oHm# zZ)OX+BT5jMHIDIA>%iWR-(~+W+B%}rR+^c4ks7riM@CNcc=XZGEl?y$_0Mm#nfpM# zb% ze_Zxtk0;orlx!!9`Elp37u4WQLNI3q|CloyLO=dofA7-HEe{D++)F-&G z*SdMO7Ednzt@bSS)a}&W+>5`TyLWqgVHcp5&>M3zw{-JE)&2Z(*IHXz_VD*#(%l-$ z5!4m%CX&gx5a9NZwgJe5!@rUERulwp$jZpXzI%5%Y(!GxsY++)64)*rx#d%g{`S$9 zRgnX~*&UI!E_jfz{UHj2!6fP$*lp?{>6-7%5?c}DIRK&C^Gz#EXt>|3NcY*1AA0It za4>LcNAReSD(efa`YY=;Cf#RA?bO+ceXD&pD-_y*e1_FcGVvJ=c9Xs=S%+<(%s8^C zA{mi{#agY%LUzr;z8>KL63|onb9h)6r9^EvDtw@H2G>H3VNJ~i*!?4>Yac7}yFa*? zf+ru*Uaq7Mqub+eYg+9*vFlIq#?a?idbP@*B&GS-kaXf>PFz%Z_spCG`Pyi+bh)(< z@pbICQGo~I2D84f8&5J;EY6aJ4BY=>MsiITm|J=Nn4k4kk(;o*+EhJOq&t)Uw85@R zrO)Q%w(ANmStgea;g_GyTkl{mo7Mqs%6n^ol9CcQtfH(eJ#e^-a@5AoKCUbFs0(cN zg-ME^o}N%G%){krT%41Oi+j8C;d0oX9YbiL!`3(B($gKawDjW&J1jQO^EylHlZ+dF zxEcKD{?r^V@dT*efZ=m%UPKMaPxBba|PS{49!5Jg#+?m#1f-<1oLWp~@-E zHwVrpmHr`N^$2fg$E&9k_cHs^%j8#`&zAOKFzMOZM~_QMy)jQkZu9mmP`<(=9Dn03 z(%m=&&B`4aJ-@(wbpv-}Z-|uI=_;PEl)>{V3VZgrOia#1`%ccpm6x-=?|)zU`p`pQ zb6dl=D((O_7W(^CV{I6sDXe`@MpR3iU7l^Jxsxy_qf~rGY;2mfha1#kbQ=)18oV$j zT+VVPeTY$F?b!Jz`U-GGd+BK2yA_xDV&?IV`+VDntwgQOTJUmqbN(1;Ji_nrt}y^pX? zLDaLQWgtd3kw4U|Mn~>>*$yTdPm^+4}M@ z>ft=b3*Dx46fPzrGG1vuD8Th6eVC;+5R*KAByE(oM$xf?Ed6_P@Cwl1uYtzgV?(wh59AlAC*pQRV(Wm+^~Af)0e&YDnkA94r_(C}}Xc zsr=Trvk6ygW))q69o+c(nHuSvE2MZ%y#QQ2-=C@(--zD-wFQnKBmTQEreUC-koca!{v zt>Y!yTVDu(8EyUidcy73n=kF{)|f1)LFda4={y2>#fk#94wii4;h%cBJtrsUbHLU& zV$em*5aRKt%|o6%LJI94$6g?uxNf2o^0cVx1&srIi{f|k1-UpJ&WbiUd3;}aY%2%3 z)2g~e6t6x3bsfQV1I0`D1~OiKi|a_qyC;?or!IBh{V-zfmuu$Cdwe64tHMqxJ`c>1n|BtY@j%xbv z|Hd~M-5@O?jdV!E2&LPgOF%$CP`XvB<4M#;u8=b zHhCgyK?SF?1H7*yS|?+&6+l>kDrvL;9C2w{98Y)-&iNT%lqct3Xfr-9Pp$Y`=Y8*q zrr$&JiIS#^F2Sd@H|^0vtzc)`7i>3QEv?Kx{QgaAX0&1h>RL3lNIS28jEz+(31j*$ zN88B>75YoHzE78|s1twqkdtCfN?^PlA%RCn+i*j23W{5jQgTwlU)DdQ6%Q4~H4GPo z-2A{B+%sz)upV$y#-U+ze17t!th`L~Zkk{>28kqaaPWTgTBE z%q#0*IQxNqD2kniO=)1G|1!CvEHz>5#m@F_`b>kA9X7T-=AVB2_@iJ)m8b~GX|7|4 zyA}0dG#8Cl43e7!T%1f4@>*r-;`Uo7jzGh{of1py&BFHDhuHG)QR&xb)cwr2YWi}( zmQlX3q3(%v+&3CWGxuCw>#7btS|-Wv6jL^?nYY`;)0%lv>DKz&fZS89AD6Y7dU#XN zszgb95A=EFBF~#monumJ2!Yez*H zqWR?_uTZXmuR&@8|HqN;&vbXILNnGd-B-d&J%oMA7*2!@X!pI+B%-V}Iav5E{U(@S zHzj$?DBEi3$2Gf^k?_vD)WYFmANQ?_`v>OOc_8Q%m}A?2-nJE3#h-X*4Wyt#({^LTh2f_XJkX@JjnpAbD_&7B)CIMhn#cxz!Y0Fw9;Otf>*MsCo5!-Yzp;inb8)&T z%u5!6?X1;KDfiLN_$S*qXNhv6Yd}?))H#5<_YKXs;!iTb^{0y-X+`LI2*S0vxR|!C zv7zDXb$~?hB{Hs{_F>4S^bg6@=U4Su-*rfXYxnuPhFqnPyG*lLn9L5qRu5oy30oX4 zw<6;@;$WN0d5OLC@UpI_B5ykR4w-S=k0fS}+7mjT*tq0&A4_!7g#igE70LMCrb``_ zS&=(%vG$Tb;}QnhXPYY!aPMVilCDVz(#6OCNXZW1+B1eb=FKjGm(yp$x#*BR#K&~B z-`@c&^d8=A^kwO2=H={PujzjUw@~gsM+8RGFQ@|GUBHzI z!}-PUA(wWo1Z=|(mw?-5-ls(CLc~4ga57zBwomJB4BEPtFDTVDa9(GB(`W3yBvhA% zu>QvKwke&MoR zTnTGSl*_^XI}LIaey)V!92}$~3?Jfbmy?H*(`YR$F2*QPg2>NCky?W;$LZ`U9E9xP zWwk+5p|{Zi_&l*kF^Hs?wQ~_44w)I7jU4sR8AmXs9#(Yx zqT4VCJ3=n%tzVgAJX$t+Mopny6SgQfQH@U)Gn9R|u~$e!BlThu4c(@mdCAs(^hwt% z*Jt&wCIg!zOpGDeDW%YvN>U7o#uJcRn$_UiOo?vA?ZFLg^=Gq*j*dr`?migHn&x0o zlRyvYu7-;;WH0&hK4tlKJYbPj&Ztan?_J>20K|hT3$!*!ftCCvWMK*!@( zaB9A|P*mG$fh54^V7{FMg6fPvxv#IE;OIj1h8+N*ou6M$&OUITUz#`m21zM~{ywgC zI@#EfDA6?8u0Qq%VdNDRGioJl1D(X>C4^{>U_$f) zn*Snw8y;RiZFoSnYS>e}JRVP#YYVNLIy*xZ7K#J-d-;o_pK|YKJ}Is9O<5tMhaq8s zVVY*%7}{Nt3&n7oxj9+~GiOd#Wq@8T<@Z=&*>d?70-bzuDhDy-?QuDhJfr2z*1PZf z#(atjJCu2|IS&lZcs5mh1Sr+t-v|Tc77$-THN3urPpU4>IJ|s8lFMDBJc%W%kB_r; zL;Xc)*kF67#a;PStE^17L%ANO(Hf^!jEjHJ9CAc!3!fGgeYeex>71Yk9F_jlS1B-H zUt7P^M@o+nxmVERe4K4@l8CT7XNfmOR|h@q(;>mvs*10HI zVpr=?ps1uR_)#LioU)+F)EMy&omzSk>o<%Y`{Nmjn1wi8_;P2M2zC*`$eNAP4HLxN(ok~p2M=Vr>*iqh_n zeuMd+KkqCpEioAYxDG33bC2)9=tO8PX!N?Ha9kEq{rKQIf6&ZmNFCn+7@)9#KwZ_c zY7VqGDDfW~c_HrL*ZrLFHH|HgsjR=b7lv|2?^jxxEMzQh_9Odc%xoB{Sx1EMS3We6 z;NGoK`iZ}OMX^Uo$%CI))7WUTE-Sh#Wyo`cSLl36b9$eYs1JE#y|<+itmL2lckikI z`V;vmpZ)CryA=FS*Zb!~a48Ns=Gx4u@A~iJ{I4UyQGbGCE?z4QyI#0d7lfnjcesK&n;8M8M*^IF6yKaWv!|Z|L5&M0yX%`Eb>$l6H0?hMDB?2R@rlrQhMtH zIp??ARqX0jvSB^jTeVKmcj{KCv6Ymw(~92c#Ti?fu!|hmB;{0J zR-VXTcz5{RIm?c|7TzkoR&u?p=!mf=oNeF^k6wtjZ{AaCD$$?x^Yb&AnVt1?74fnYZ(>=rt$PdpFa1&Wc8*WT`{it{9TyC{qA~B z=FB8FT`B2{O9zQwTR(syI-CFqN^vx0r-mX3!Pd&~oGoFQt@m ze$c^DJ|!)Cy-eTkF4aSwUXHxX$T?C$^?P{KZ0rikz7Y5~xj-8bP*OBc1@z@o{jC1| zut0Ou$Z&v^$UWMZF6CNU7Vg?|yTiX9*pw^mm8Q|()K3^@--YyXFqobZAG?S3XFr8E z6IwIira$2kmsjMRe3&9Gajz(7rhSQJ9=0@HQ_-Qq#Kg(Ky3+PRBhkU%`_XW!D*X^> zWqxO{M6j}KU})$&_A!K9_~=$6vjAC1Dl{hIH{dQ(NHc`PN^n3>M<+j%<<0=1BUnee zp9gvzofwxeJTTYfeR-y>l$#PPEOKS3|HLHS@rAGX=CiMkUS3v#*T|yAN`pWovZ(qF z4jmt;sz5c@<}=+2QJe>Q@^dg6r8Q$>LYwS7votA$8x@6Z%h{I{6TsQ&Jr5(o zwBA*w`T_;Hb1tOP^Jgy{)o9X$2jhzghNgn#=&#Lhms>q#z_7r0ybGv%b{Ge^*U7)88^^waJ|^?0SVv#Bh3`Ps29O*)T#G z^JQ@fU?b)r)cxnBrG;O=8$bUpaL)Q3686E!tkNr!#~B7eTilX`oPm(v=!POG{F|A9H~ zXM2uVB~#XT>%q*X(AT!K@kQ$3P!1uC-N%kR)50z#%@YGCFOq)KAVN`$GoyooPku^? z;>f=~4;$sRkjriSF&y1tmLqh6>a~3}6@g*UWO)XqesjeagGn3nG4+r+f*de_4HX2V zWhdyy<({&?`BGIKTNHY@gGMs>o5?+KJ#vjTOh*=#zbHMbkaE*|ed+Gf*%bQxx%Mn= zHGP>oB$+{sqt_<0q0C^$n($_QxxR_y5H|{+ItqA3Aq0H(F+H{H+N9FDq&ie*_Sv`T z+_zxD*;PHC)aRF+N(OEE|5fa&<8{v&cNoN8(_UE8iHu&hw_cEd*XP;{{*L#lr3>NTqmSrV z&D0dG3KjoqjOdx=>L7tX4lKv3VZc_UPXWp}c`mS#$UQZ}U;kg!XHJa=JP!|hCND4l zw16^HbIJmg$)FX<&fVsHdLh<0>fOv~jQz5#s8lNz)v4N5PNGvlq;)c&`&n^;# z;i(Q+qBhiT=aMp{9`_g&h@*wvv6YQe#?EWmLW?Wk;XW`oe#>o=I_B!{5p&uL#g|Z7 z{KU@N#@m+YA`mUUT=%++Y`5Wj_%^^$tafjEQ7@OG%!A~lQOP_+DiyWVbt@6MdVkObMb1tR6ck}a+|;Y;mjhcWD2ig%j5!&v++kv9A(-Zq0Wg7veW@d|;j^+~Xm5!nxcxKk0XK(l+ z9mP>5CBJXg`Insk<|A=^U!%zdlA?dF#qpq%^L_uCG2MIm z9JXFGFQjUGeEPBVwqHVr6BIA|4Di}g|J+D#%}DVK{6UcZY;{XK@Y{GN;C1*{h0~@H zIw89EI=A(j_4|X!`bly_#Bzp*zlku=3|`xRe+X)Ml*F-uH=~xLWPO>^F+soJl#@lE z9nxSl{^m-TZZo|aFauiAEQx*p`Saks%Nc5B27`ro0!1ju;|Cz)Rivcxnj!A-Q`6I$ zR_^f?9&fqtKRq6(_?alzmb=q@NO2Q>HuMkdzfx?(zQAeKQ^10z*Be!f#$?%yqrx{u z`kdaek!`b4%7&7MxHN_sv=7>Z!H#P8Pe)7)z}-@F6z2PYkSi)!iW+R|yyNb|?KN;e zTnzWYdbsQ<6jwOt>ZAx~1l|?fp^95*^lItstPW>}@8^%FlyEI(%>?REA^nKZRWE7L zwT%r2s=odsAQYIlE{7_{Z~wxUZ!k$dRGS$$9?OV~&f20p0~V5D!1yQ;g(iwHFhDsN z(@QxQSWxa6oS&P~+$OSp_%(ohT|wsl7xFO2v` zCOj~wM##yoAlayPtEb&%+ppOA^zFls=uMkOhTQSS7wCPs1aBN!l^WRrrCOh_Nm+Q8 zgi!jxu5_pLrMx|ss}bl6tXh8CESkjTr5A^PI>JOqF#}?ESpkzL z-q(;=w%^N35_#h5%c}4CnZPd;;Y0;zuv#&3tJf$g9@#J zShxSvXZ??KQTm7C>I_7N!2j2rLZKHCONtSQ@umW~MLHM=BxAeTUrXZVM3TOaXCE_vH2mFs6y zY&^^(V`IB6Cv|#jpyF=(I*Nfr8g-(nVt4kaBUtAwyLTi)mMRAjq#TtVr#kpK<}Kii z_SVF8T*O?sCP+%G-#q8qfiQ?U|sD7`1` z{v~E-v{O%Fq1HBdp};yXi~$@gqC+ZP#W(v=j3?^a$mZKU_gHvW)fikNO0N5=@KE%_ z59&l!|B@Ts>{TYDK(77e%|86{zgh+Zz7=d)`WQ345@wWhkk&gi1VY#d(y2@(=!KTh{jfWtT?LC5~FG1K>CW1Fm0@4Lb~ z4!U^VL!7SV{y;T9 zOyqj6&duXn()W|5N9nwU9JGlQ4?SB|CQ11&ZjNp*82LX1dgFWRrhK5?$ay@@3J1G+ z_%&9s>F5MJi21zNvH56B-=L_aMP0qGZ{z27lL8ZS%J-qVM*tN*gS&3-PJRXrewTHY z2$Yu6=;%=KJfr*8AFKw!e_){OHaS)=wfHx&?v>} zZ(#=daJ0LV*0uVFkNa@AY+X`Xl5SpqS5{Fm5fDibThm-GB=L|&Tv0aPF-W6WA?Vn+ zytvY)0l@3#HZZgK8q0PMFOMbcA07_;xXOxC2;@p6ex@9D@#)x~BXZVib3;61viBu* zUq*VmqkD)DQgNkuSjWZP!z6??K7JT7o>bA}%;xHK4ekp2DUq`$`}Fbs!SjH@tvQL@ z!NHg6`r?eTietB~yE{H+L?Vclue~4FKYB8&>G-(5a0uA@0J)R%Ncw;(lLBr-xG1Bj zM7O`OVJbWZB*lr1V~;91A-1AWCNFoukinBo>Tc)^WwwnS@+E3z`Q;!b$x^FAoXi^*H4|a6jPc!=VoLJg#x9899AXD z(WtXWV^Dos-*2-Z3|RZV?CpCUxoH-CDSP5|kZlzbnVD%CaNi^Z4=f{Zk7op<`8y{c&c(jV(s` zPC5ZW?1_u7Ire6^6$cdjd@i+3{1TTn;>pe@46Am(?-I6T3T8MEvlmAgSAyT8u$U^w z@`CBF9OC;252}^w#BNE+nT>`Cjlu?Ux=<|*8g>SGu+{mf)Qr7L*V9^BzbESYnAgnT zr=YACF^Uhi!|ZLIvP+|0-1&US{ITOWd7O>mptRyL-q)7#_t4Ng)@KBKWISrhV4CE2R^8|?r3Ex+7@4u#xUq7)$8?XUd^H^aBDH)Y=CdeIXX$|y6bJ(=PIR!}I5HMuxH z522!zsrS!nKjNLSHc6pW2+8S<%alhwXX&*!xF-g2jfN{$Ru4 zNjHC+zgx(Es59)5e12>UivQs|~wWkQHu+#Fs>TNzSA!5I_Rw zXmN3oc7dL$YXQHE(%K`g5tZ^)e6wtki+T1QXTc6cYW{C)Q|Mx4UDrm zr?~euI}0`rb%dHP62WLg3v=@cTMrWCSCRA;J<6-z-upip`j0v>6&D+WG*)1`rWb>Z z4Hw-je#$_6!wn~riQ>XBPrc=|vo6ze7G??{B2GY;F#o`_?T%nVUXY}?=(^S0CH!3p zJ~nnW+oaT4Ex4FfQ_tw0+cwNb-#~xkOwwFFvA_TK@%qaE1+?k&=dOjiT~UBtKmic`-p$X$ z;EBnsgQJh$%*p>fJQvZP>l`^5;V2GT zy;-uFhb^Yv+*mb_SkqmUnOVJ@4H`|UO*+p=IXxw5 zmVpjk{fN0Oj$f}h&81+PSMy!_`}fi>TS|EBO3101neAVIYS74_&Mf1e(32-J-s|@h zB&dF|3Pf-9y?V?+Dy2*s?2mBp&H-*S6BrhW4fLcmswf@w3?FEv1^8t*GE3!+;ZL9 zo&N6n+{Y51tWMT(f7{2uifta<(NRq7tnLH4{V0<|v_BYxe%KVHsxQp-LHGO1#N%sc zVU8rW(`S7Ol9Ez8zl{6V#egURP}04&_Ig?%pH$-?8prOVfX;j3VmB;3o<8$L=o4{x z+~y`v$ZO+#=>5C2hYVeqeThS4Knhq5v79?*o%>H|{Oju<%J#`-J_N&%9nwg86OU-= zR?N8SS7W!_S-!I9{Ia~^B#kLi`OzKKG3Sv5OGamkoLMY(y1e!{#hvV1UA6lOSOLOn z)sEph_K-_>#3zcB33jJcUK`8?m!C}sry$mWb-;}t(-dU_ zn>PP^^-m3cD*cp7&fyxN&|y@@9nnNsz6YW}5fX;@%@`!3gmezItEi1)<0qSQ9+&Ei zX57Fugj~($K<5<1d{5&ao`@@RIr?u~5@Oa{#w!zS=2wCdOJuzoE}@~`4&!EDhnWgx}ijPvtNdn7uFl+ zc{Fzilv)G_TgY45hYx)^wBMXKG})Yaio5N^pc6Io^E7&mp9*<&bd|LR6aRNs_=7_t z=5JvG6I6=20(E4lsfw}ICeog%GSHtrk9(cv35`xu6nmq+|P27ndjl3YL zwhD(tyH0}v`W`?`ya8%{_1$6=k`3|^_!-&!JJn2cgsKce!0;(!^ zTX1BK?C%^~Z*80@AwH%RYb+h`Q#J1naCYkI;=rPwlC|IE5M=X-YQR13-ol;1mC0fQ z^MZW5;zj=5ZHz$bhmftLqMb32Y@W z6$G=W!5zd+KkHH)G)2g3;l|x*3)6MN^`G$Dj7si^C6d7>ePq~k_(E(arg{!pD2d4B zE{Kk}D#O*eZ>MKQO}Sv07$4EG3_33aPdRDvq^_ElQJ%*R8k{XAD~xwb7KY$5 z7#kO>(7SNz&g8xLR2+5_c4i6q=O+C<|J>an|4rIz*xO7^tQ2P6(t73+>JWmloVgrx ze0q=bh%RwsX(C_HE%z=+fJ)@yHxm#y?o(Rs$tPdelJ)!Q+$u#fmN)Cp?mgCYQj2+% z(NBvqvC0{jx`p=h<|R%R$Wy!l)3Qwo+Ec?)!%Jc3h+3S_h< z52Mm9&K+&y6^=^NGD7MnWgQKPW;5>MFZhbYm-dI;j+@s&Q2P(2QjlUOD~GSGqcRhC@pOxjgR|I zmZ*@7Y>_LeEKhG)c)sN2ps&yE75x48*;FAG$>PQG&;0k@9o%%))%*YSNRs!YG()W7 zC8uWY3chr4e2}hrVr~7QeQ612#N9?EI(*_)K)6c)Ub>IBw^oo-ab2UIrmId$os_he zt4{507DoqrR|_er##>xXbuyJVU$K_{6cfz3%?Z}migZjk9vdAjo|nZV#d$A*U0s04 zj7y9s;bsReQQTbtSDd;9>7FevET@+run772`8s-JM59>nYRSOwym#4d3+jK>bJuzy zy|X-f#NYaE1~Q$7LuQa*@OuMsKO*4lUL@u~ODW?aB@ed<@*B}vb(5sjJuS6|wdu-- z6FbCOCXe{$^eRqX{lHY55Tk}~Go~Jyl2dTb{rag=mpi=$z_VqU#fjB1`ATv<8tCHe zW~kGfVsu@7-y70H3nhQ`Dh=v7ag9c+fNJCwQd8FLWtGO9U8TjwDBWlMOASZ}J`Q~b```)9wRnoC@r$G2|% z!5MXni)w0A>lJV#{X_F@bvIub07>MD4dWmI?h|uUWe`P)P3kvy><6u_?{awsVJC)U zwedc$G+LZ=x+~`)8$(mmA-0lk!odFxp-m|}cSIe|Qp7^2Sp#gW+F+3ooUo^04N zJg#(l_O}Y0rZ!e-SlCfWT586@E5wRyg*QaZ)y3UNlbah#{=L+d*tY+9bIaY7&pnt9 zXl&K&+CQ>CSmB@ilzwoh8#4kP^E2z{k5!tcT={Xiv54A-)eO8ism<7BW;O5Y;J`v| z6wr%U*hK7v>kFn_C2l@}hv1TPtkqIfAr2P_fMfwh?O{>X#cS;hO53BOdgrxFdF4}` z&IDUVYXS|p>kI<3Pa%_K*y5C~8#9-(8!Gf^8vPm$-kb%zx1>i(&WkWcle3jwN%GUv z(M_=>+T^^fq9ZM%N4lAbyib9(b_wdrjTx5#PK zBJkBr=(bXd zgkO!@jD92nzCIgXGck=?1^RE(CqMyFb*^ZCNxO<}MASqNf>e8OL0prK`+VN~D+t|> zJlx(MNA6RqM`4c5BC9V1d7{y)jg3L!4WyCI=Zjq_Q_$-Jul~(ABm+g5v;bcC>q}uK zuS!ad9)-oOH>qVL@3WL$t+A9jYi^Ep>_4Rf>h7NyW|GB!RQ%6}{{vck_pc+L?;X|t z??*h}<4`c7o#5gmg1pborR$(L5Vv027MExCz!XtyPJ$ghu)K{gFMrY91wbwQ}`6%_bhI3 zffKNpO#6-g#vKK!X0A(gC0c3V!UIHsQ1&3t(;CQ&g`!ZM1$GxZ2Z#Htih+f|H9HQ- zuV0@N@a%&Pauhxk8{`gQdGtCZ+;*xa_nShl?O&05I~EL?9+Zl>I84fkXi;mNIU*@1 zQs1YFln-|6K(9qhet&IlWDnGiJSusg_bQo8FfSJ-;c>@w45_N6EZ=oP7w++vF4cK} z$q|jsOAG8WN|yAi3)Q{EI16HV!3pQ^Mg8e`BF2 z-G8O4c?1@UQyxNeHbr7M5U?MP`sob@jxJk|hIi`ivrHA!2dCo%k8=3WM4~ajPfQ|# z?XD$S+B$mLWGglX_A%blk2N(ynjdkk8-Iv%3CMyD@8G3B1gxxGWCUd8ITg_Sh9uKD+GBG;qxqh;WcX25d$=HWsdE|F}2DyB| z#Do3)p^J+Ix1;@`p_76#L7~Bui2~IveEe7kTW8JTn5zIhT&%@l;A+14?Ce}XIkBdw zM9cgmb5V1N(dw&y?09|Ss+~ciD+D@wVYRDcX4;@Vs%K*27uTpB*~G88Z}47&Q<9T_ zP-`Wq2;VLJ%gh)>XAsw$72v&kAL2UP;F(_&99Y5Ca~76^SDv1H0B?^rh<%ug1)>XUu7s_(*?)>!OP0N zocQ)*qfO2HC=L7pg0BOi4K3z>97OG39-1#K(0YCoi9x03X%)h)|S(&tWkWH-)0OeiYu{VcmSzJI4bXf4qpG`$6Ajc*Z0yKugL0v zf>y3T?f_9irK7vIg>8czcJ=Al$x9wJOXBzla?Q?4OKILUW4%$#ng~5Qdf-I|q`D-A z3@FC@dO$fUp2iF>J{+v9IAc`tAxmmyDk(2bc{XFWDoYrph*hLb)_M)zW*bKCzAG!Q za2~u@<`);BhjUA}cRI73u$!UlRtBw1GyCv+5^4jkLadL=?sXsG1#IpXivt9De%``klD=Q+;lOZKQup_aO5hhE27q2LutMMxHCgzKe*0kcH;M zS$NcXe*-SETT!qhFsixQ2|_TGAJB5Sv@9HX)g85qU@TW-6Als!o=)ADpSPbp+}-_t zcqEEy8F_#8^SEddET5Ec!*tDh>RJ=p(;jnGT9T{uDePm2>-J>tr3b-@kdRQwYLH`? z14J=YD(}uI{g}v2p>-6~LWCz^t|`Z)|SF!`Pu zeP~0^#<*axnF1d7Y)t4XJWqPF?8CKGxv`~mCB3O$#h4`yxa#b@nG-%u7nI@KbpfF`=o*fLWES3X- z4Ofo2@0Uj?`s6l4=)54e=e-DaNsnGmE~AbWOvIIWj)A^1cO3l@>`-M>nqNV`MDFZE z0U5sjHcdI*=NF3LhpdOvcx+k!WEIr^E7hzJ|2tuo5G)&bMYc?yMi|slK>z8TLbdTw zt#bylLd*OnE1zin`QV+Ols=K79J{c>t6+W<1@KGZ=F@i<(yr!#b}lf4G_aQeIJgQu zyGQCtu^I*|SCf9^Ju4^b%mJLBp`jth;D$>}N^)UWr3DTts6c&H<&*9))31vevL5x2 zj%i5CxQAdh6Hy5MfX;|(x`1j+p-=b>`Ikwc-x) zNCBuUGWeu?gRlJ{3BUrlJl=D^;MP$f;|ct(O$JKY$cE8O@JvSbK5O@-Y1CAV80)g@ z&E^e0X^f{CSZBf&qUJNG z0;}9e8Qg&jlP(nGZrg`zE)ZVpS#2kZ;U`Ouj4vIU;B1raYYrkv*9PZI*|=H^3yM^5 zcD^jxTSh6BiN)Q=nW|}P<(h2i& zSc(rvaJZK1?Z7ylZL>B`Ik3&wolstSS+fP%hjS|@V_AZSpNF32TTWgKaZzFCCRDDw zbUSMlDLk*7cvV%djkK7Um^J2K(%a!ciF`6y&WT)wq=i8HDFeX(c(SWM2jE9aN?ch* zd1-YcY~h#S-0MPPEye&zoCI8USjjCe);nN) zd_Sa3zAsCMVEzbf*dbbjl6{LhfbNAcZs~&+ur=|G0032!L0=;S z#yTNL%*E69Zf;^8rQ&jd>TL37t4$Q-6|sY~BuYw4GZv6?*i>K=u6EYOp`ARt5pv)P zax}HJe6+njv9?A}w0FjfW6?LYFAB(ui>oYeZ?|P3{^<%Vn4K!ybE3%XLPzYg?GI!1 zci6n~orU%Gc74kh-X=vVg{QSr@#VK}-7{DK)+CaY)1kC;a4>oPHCFJ^ljlUD9*zMI zo|rtjwNGJr0*5Q_CH6nXv*m<7Du^N-gNkgdtyxkJI;;k$ns3!NVjVU|2YV@|g2KGK z#jjok-i?jbsDTIQZ&n-R-YSq0mlIA@@4sc6rOU&WrM=C-AxfA_8z3vMm@>bk^~VyI zwWFoQlRUAsSiHyTyizoFP!!2@>Fej<=%|HN@%fPLdL?+b_`iwh#wGp;z^QWr#+6ib zSU-2&s{@Ia%*>ZSZ_{moONBa4bo77~*4_7?LRe%^E`#$NY7{*oF<6(&M9vA$#60 zM@qp~c%+O00M=-VE8yezbI#}TgEn>4F%vx}`{*@~v$XSG^Md=XuCUFS;F&fUB*OPi z{<-33&d~mrwJa)b*Quv>n{s69&tp1swr(-3*CU9}R4jtG8KANit4P`STG4MD9~s9J zCs6zN>yu8rF+N(?4Kx0IxB52VO1M@9Ky!QN(R=wKUk1_!Z6eEu9Mg|6UkV=jq5`U8v+lz*Q< zEYdcSk)JpE^)h>7z0yZtA!^~6B`N$Ubd0AWiD2W3XJnv|465}lGVR^A2O$ra=`Wna zD_I4`9OX5(n-+obIc1;J&ris?u+~|1LrkejjUJufwRhMgjGCAOy5|Q z6}54{hzT!C$jg_ibKpcwOL=Sq1Suix>TH@PeS{qi;l9cA&OQ1D8WK@8M#*OeiwxjJ zpELK+(2hB&^S0nCyoz!-^hwdnqqz}W%ce+=-Ni)HS;tBX8ni|U(4-*tCyEamIi3p);+K@KT{?S7aL z`*Lz3AK8M_URJn&vT`OknYz3BVQx7$&N^9>hL#mwufZ$LOfKc_=+Qf2_Ih>(xk}T$ z#5$SmRQ*VmL1fJa!wlt79HJmU-r%Qg;Ln_B_qVdCTn{tzZTxdBs3Ul3 zP}>G3m4E3I(d2))-o~L$ag+$w@{nz9IfONse0?Gz-u#Lyw^85GZcCT00qU@aU1o;i z<^ZsJR_k2wGz*ky_HDSB#4GGnq!(LFPb_{;AA9|3$N5{*=Eq0Cj?1FZja6&QO~-&s z-L9}<`C{?fmmD;ujP(Wpt ziQl>6$$TA$knik*nC9}U`iMdrL`xhMa+01?_qaaiO4D^oMh$9#3QSCu?-pG@xVRVf zF?G54_r+L6Fjn%qsT7uuwFb&g?-jVxqA$)VnT*_W&o{zMOv-n>);Ohn`jw zUiSlOx=vd34-;0x7$_B%7M*Ha!agJ5Dm>uO@#KyNP&^Jp2R$DjkZM`QjG~uhECJFI zZ#z$gWKmvzf4Od}59@c+SpjiNMuSSeA@&kAqk3lMmPtRbz^ZxXtde?&eWQpwxI=cA)oneeng zz{Q$0BTJaz@85vy6mxO5c{hDB|CQ^;%HKDiX3Y!M$Kh}OsN-xf%E!%Q>>Z==K3EHo zK2>P-r3$xZn%(gadCMP&glr!^FhF1$Jsv?|6xQT zKK)^GM_Ux`BXU`c`}|B$Ho>vdohJUuOhH|pr!h0gkQPr-CRooX%sIA^QF0V{!MQ%? ze&~X0v%waIx`VlbhSh-z@hL~ize&t70OZ&mZm+~vXY^#a8@9m1WiCkYCG26!A=tm? zDAqTO*Y2cxq&crWWX_h>EsBIt9}xyZ=&lRSO%F$5p%>xQu$C)HwJ;uVtw(*&ukF~Y zX#Sl~SH;KcpD1#~R`%)@#jp3q3C1LF0Df3{W8p>JYzHHn-hJ!KQd^gHCdKZ3%xtBN z>)^1YS|??&VJL@Qgsr+%2BvxyIl^O(ykIflTJs%AZTh zfn^#NCaKC8x3O=WC{jqq3*=fZ1;sSi*QjGJTei~|Ny^$teH1}oyL~llYFYnyh6h|n z8)+AN$2g2(v0L$(YZeqYXWjHRE_B{a&y#K3X@*(d4KzCu2FPy6QANNKRug@11FKg`v?=6hhZ6a9a*6Kuuhv%v;9wl9^( zZrw5@%*40G7o^~n`Gqr5&c*jR7dz`~oC%vmi4W9Axn|Jh$=#l*J7B!G*#}!1E;La6e>GXtlBfN<@890;)aJ4 z72FRfH}uBa8D|*>gmg)(CwJ8bvUijjaR6l0{);W7v-f~E=>tH~gmC0|_rpE+U(S#@ zK}1e+GyyK4pna6hoZ0&WtXiFDOuCi}s1r=M`i_eOFveMxTbSax4?$o}U6p+cHA>8L z!c!Syb1+8Sqhf=&u9c+Jk%$N)GruxU9Vzy=kd-tpTn5VIm5|Mh^H`(Fc2%i=YXK^| znEADYZI>v^ph%o(=eI<^nA4-XK4T9jZ;ukdBgNY9dXUOVad&duHXW(A+aAGE!VP59y z3j&UHwdQ?trH)nQyCevr6Q)*NeeplB_B%eHa)R{=`IVCLj_Nsw51*w`_nTz3EpTbenG;!OzQ1BBdK3=-|63cZLc=lex$!gNzR;QE-vSe9c&3(*a7cbFEvB?D zDGJd5h=7+rXg-=MUC7AY2QJPprLg22>>m#HAzQjn!`X1QN-OZH=vDS7!)exDmJ3iN z(&uIa!a*~$08iz7-1v#fMH&_MYh+h7^MXSlKAtSl z%3dw%xvJI3&-tsYi!cullM7!zTIzYQC|3wB?!@$x6@Yfixw))KzlfOD^9Td><>~8`G~B+T8rfxf1zP&dEw6??&ejmWUN_6I&NLD0cXt ze^Lm#`V23IcXz)8RF;;`LWLR9ui8QbZ`q(nRzyPBuYdf5v%`7+ zfEuGu6>njk*)p`~$5H!fs@?FkaVW(0Wua~>-S)c9DsALRXS8c{f}4I zN(&ue4k8!An-Y%TH@Qpfevmmg3fB(n)A&ic&`{LJRMax6dH?N5hVEOD1yo@#cQiqM zHAdnaTL*N!^En*63^hIQ7-5Eh!vhoy@-Ri{t50-ESG%JWFW_KE7(S-9XFYr@ChtM; z{ALP1*8cB2-^heWMBro1`Cw5ex(pSTO}{>jyS*sDmhieK!(%}_baBf|(LDAg-Yf*Q zorQaTtUwqtS31cSx259yhRt3j2;ke2D+sw9^^3pQOyqsR8Ldps38^K@`=AVm_k79) zD~x>D8wDHa0$$?vGTN3=ZA!|)SMRSYn0hd{&O;JXQ7rC&tDpKZ)-#`%xh3sToVq=2%E@4G{oBP{@uj=o$5UCw6QmvEAlp1708Oy(sNpzNqiLj_jmm3M}S%* zR&YS%s$c~3WVLs>yKaneFUWW<3?P_&Nl%gjsU()JZV(xB@9;_(_!Y%%9;&L(J<9eZ z5FVKYTTkC)g|4yiNx&)9wq9O+#5v)29(J}bXv(#I1SwAcFH?XNcf`gMf-VGLf%KQg zO*ryfN=MS02&~~wm!>d2pE2T$L5m3Vi+L*XK4?MI-tsdtfr^y1q5n2kRs+UYs=JSi z+ghC?*pxkytW0W6kB+aVx&5LKMtaK`0J#n>q#Sq=@%t=0{8h3nVYdcuSsOsC2PQ8@I(Mci8B9#MP+LRD%wM??@_TUg{a{|>#6r_gM4mD)@O5Ut5>dms)= z<(k2T*kX480BL$QaFXwS`vfAWH74Y;zwf2({nXudBn@~AOS4f^X^N|;QGevq5*B6P z3E|0yP*af-4lNX*MU7q;#!`A~C%Yu;At5m!B}zz(bazX4Hw-=CF!OGn z^PcngJ8PZ4Sg^J%mV3{>zt??V*XKUfd0+dE^N6tKr~6%g->r&YUflI*#cgO1x-+n> zwdbx8U>^@){hGLR?YPl=y_eV6^umFk^2zfwz)HUD>x%bl>_&vF82M?R*)CG5+EV~B zz}g4dB;8nZD1g3;%=T}PPLBjg2|np}^J+x}8waVy#>~0a%qC8Lem4o7L*pPU$Q3}w zA`CjZrBzs6Qi_t)GiOPh3B7m@DcV}r28l!@L|=)CNVWrPJYN~UdSyqo&8pOFIgHpQ zhgUsb_?{Y&R5`AKsoc{MM(V>1Z`l3pAXs1TOU2Zam2orh$IE47VXpQwo0+L8&wpvA z5;mo5$g4sg@|J_MvwLNX{3-;2*UoR=>?{(gpvT7Ck{V!|uV)q(xHwcWG(tmc_HKfK zkdwj_%(j|}22Oq*or>x@a_}DlcoWqx&^6}I7%l@q=cnoNr3z}_)7Rk=Y5HtZBxLmM zi&{=-gpJy2#Lr{eT*-^hJM{@h#%W2Npt=YQI(_}2^$)?s&@sq0=P7LSH^S7~pmK<` z;O$QgsEBsQQiPLTVO$2QB9Eo=_4|EQg_bKd9|dmsp(WjQIzzhp zyU2+tX>EV?;NQ{`+Ri|Jh-R^$DKjIhcZdFSR`20Z%+T@VW`R-}-J{gbI3glqoo61^ z9agTHzF9|f2vUTFwLzo;*F=WYgu-F&gkWFUEv$KU7C$il?(y7%-WM>VED zj@UIK0;r0g%DeURzWsVYLVB?dx3mDmK+LpYd7+`bN<0;apn?G}2>lt5@5U+BH3| z!pV3Av3A-Q7D3Tme-8Wv4s53@3*3XtS9aF~_rYZuK`~=~H3$o0sn;#47ZW(THu^S7 z4s0Im@R&<_Xbc5wQ!DQA<2hBAqK(3M)2;6jH|@cU?}dj`!Y#5b5XXZ?$^+~SBebTbNdupl*?LbUR_*qo#Mf_Jm6N!K-b8UB77i(0HZ&iXrY^FI+~pRi@aI5U z5P!MAoXr`_(zFf4bzmw64n$$)Brb!fVpZPVqP0l$<4GZ}NJUOTDO-)P@N3bXxymk;0knZHC!{Q8TD2Iv2$XxSaG zx1eK&g*OIQa~ez)5~L$FDyrP??)}-SsDTLm={An&=CCxzh}7Uhzxk@&hh`{FL`yy` zv(<6d25?;eg*~OtaeDr-2_v4iv1h4paP2fB+2DKk2|7S74()QNvIyw9XXcy`53Vcz zVD!d@7%kV0m$aegbL)`g*xB#vTKd%r2c_{E%v12@NWb5qY6MqH9^*@Xgyu(#9NREd z^Y_;JRR)Q%?zVkt@M;y9}u)Sqf3IJGBNf1-iXPJl>Ny8Tnnj!7)?zQKcH5M(Idt zi1qonctyrt0O--(`WXR7cg}|-e74ZW#<7KQelx{|gsARF93oe1_wTP;n1Lyht(TVu z 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
VVnMG60Kw!jPU$oizpYVJ9Rf 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]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 `swq{PcN5qOJAUA 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

_?s`R{N`te03Cq%6x72W+W!$U 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

mxG}k(o=m`juQ; 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�H8 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

I@02e#7m>wJV&Yguay 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∈l%|XTzSo9`hJ>+Z{3z-8I1=-^A7GD-j z6(>rs3irLivR>Yrfv4T(%v0~jZ8+&Q_xlX8ld+&232RGls)fLvLy52IGiIu5`Dc9; z;d|g1bTpMmTX=M+0|UFFnG$HC4($CM$^5{9w|I_G&84aeNOQdl=1iul868_*1J|?6 zn^0KOxFFd%CQQSr+J7(Ug$qM^C6a1Rj7cbazv296997m+V7ENOA1zSPHPQT9&THx7T{l=_nXU8=cC98=HHJNxXuS2DxzQV-HG2|iKY^gjiPwBi5xg=e00wlSor;!tdAq^iCmT0Rt3Y{BWs4`2$JT`@(bYmc;YBV#MrCULOh;OaVT}mriATo^P20YGeYs)jjCT` zhc5N~a*I#6o%9+*KjxeYWqv zABmWBYl}AeZKX@oD2JUD0%@N>P}2m6trG#9bu>CMeK962HYafg$+482w?1)G5Bbz% zi{2w5uxG&vvB0%b?ZJ&j^O2HpJnvic}J3)zR2h-}=*oJS~ zmgox_xFndl!NTgM9{ic}v!h16N4H5r`LS1uBnNU|pBs3|Ug#*nj{gz=s`g4bqa>p- z*kTi|g_NNNih314`sBULNO1nH!|46>v#s+J+52ba(+5D6)_;Lpn>ifVd+gT} z1Lty(Vp27@xiUu;wC8apTH~n)slcX1`|b1n z)2`Ps?S0e`h#qFp=}ui@2gANY9K`O(>x3wZOI!v@Mrg7x8^6a?hfm&Q-={4?s7SX7=<`fhI_d52D>6&i#X?mM#90q%t##mBMlY zVR^KkKe!8bt2XfhiUbyV%cyYRV-1D~)3#*w_{@PCW5%`UGI3?Q{=|io=n%j)3O<#;Et4Ej)N0VHwcP*7j=kRyp8XowWnUK$v39SPfFl<-7(!;Z zN57m`a&2RPM1i!Jr?zd`d-#iTU`~56tpLm|P~N}AR|cNsgj)-j6GF(NnF{bjr(Qo( zYOf`<#GAbF0~s21qt~Pql3pZB0(_Nfz{QBK2IrR50$M9^fvz*Bgr6m&;9;o z-2wz9(Nc34o+^WcJC#k4-|b9lSY6N6^~xrV*VzSC3LimYe&DAE`SFPnthc;zRi*!P9Wwto5^G*S`|am%V))Opv^%nBruFFa@HrST%6i?rbnx;05eMDO`E zLU14?hi0(kv9D)0ln{0BMB*Noo$5$!t^i8BQj9#=v%9cfmn3Cn^@Hyo1V9mu&_Y zmR}}Lpijydgxa8?1IlJQ0&(>VR7pK7^|2V8?;r=$^D!Q!@+ZtN6FHFz9TZ#h$UWI%0;>onLljv`|p znWkedNGSZGmcwb6eEUIlkRJ=>8QY529H26QEjdaYB;>e6JjogDE$I8)?keJ|jCB}1 zM(3U@59QC%&H0rq58>RW+goqhcoFy(P)xYMJ;FIU-3ckAN_exvOSv_WLx_5A${P)J zhvoWkV36aK`j3a@y7SsxrQqr|Tu!8o(vy{jjA&pVhepX%8H>(GyGU8jYl-{IPgmb! zJ)b+kM5&OWYCKt_$ri*mIl2zG3L}iZ_d*%(hf8!J%M}aW&V^6BV?Y-lyNz7Bcm;@e zPCRxC>hi4MB|&8&(dcYrx2*l�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%0AbN?uaTOfl$d+#^69q7rf`S~dD z>LNigMqKjH7pN%#n}pJ=fj*z?!wmPrKZ!P6a->9g?~BiW59b2pq4|URK>v;VmCOWe z^#d%omKkekZKlci6`zER=_jIbJtu2e0c!#tfK$yAacf4-ufO~ zaZ-((zM7_rf@F&R$zDc$Z3-sd3%bnp4AJo?p<(;MA)pUU(!DvNhfw%9-r;esNHh0* zutTcLPsWNTS`myl<=6M}M(w=>PhyNRvtVY!?CAJ8Q?p4XJ}qA;`WgON(Tz;NyfgmV$;|nc2djpc8(a#THUDH#hlk^;V2e6~Gn;o88 zV%EizfSBqj3d*$k;T!c^r3%v=*H57HE#2jDN~~5$8V|@1y|F{19UM6e+hulowCM)y z_T2ASao)@iIts_fMHRlpr!9yPO7@R2s=>tbh8m(<1j^7$Pjn1IP6FWbCbZ5(+XkAY6lf4`VrZcP?Eqh-5X5D7tFDu>dc z21y*qGW~_63-DZ(&mUA8(!&AJE&&b&XqZ7b8mSipbGnE{x^Ncs7C~9Q2EmP)Y9*F@ z12jey-^ZmMql1>7$cS0Xe_t#Lgp@Ji_W@eUcm@tx9)CCyxy!B&lJ_+Cf~(=|mekGs zDKZ*Ai{Kp{3KEpOijEIr7~p3>!Pr@^H7RDoXc-U&x#aKJMj@>qUfG`=LxTLG=AkJM z$S#6uPl5%$Vpkf}#uQFDIi4E*3x~eVjNTa7AmQ$fQjoO_07%yqDTb46r_x#CoxHTM zHuT=^{-7bkQBj$6ANIidl#A`jC7}AEUcasZ^017<(TA+=u0N=W^7W3bk$aqF@~0=Y z347)7kjj6YR$S)g6Ykc~EPt(Qm>;g2Z(A1J%y0V; z`fvVKSIV*TVE4y4V$F8lAeyU`QMt!l=3;j!r&W>|BXlCh@Ff?4Tvu0$p5up}_ z#U|1jH`Crb-h@?{bIHB*$&g*Wl*N@#h1*emrOHQBD#|nzOnc8{DdI+^UK62Ojf9n% z*vpYPetF4SMQaD%d+-TPZ|p)8-3__L5V4^!ag_hs)-wcU5YQ?S+gbm4Av zGwBYneCLtAzU9seZM~5x-1-<4M)jRvJk0ktU_f`WxC^>x>R(EI_6S9jPrc0UNg}%i z1(Ic&EEfX3)VAmQWiNFh+yW46;!D#^mP{^RB?WE`UkLsf7*yS}CL8YR_;+_-^3E&y zR6bP!v*mO()CpfyAwRxcLbh`2*rL)26kcj6q+&d4NI>_t;>SJ9C_32jPKz1i* z-=Ticinzr9R9Sy1KnI{GxH)|*e=Dr>%Xw2eyHh&$ z;3-Q1`L;Tcm$$148L# zbU;D{OFV~+;BI!{cPU9g!0(3EKu1@#O-+&&rzx2a1QvCryezboOQfz6^&&93qPr|f zg}}4Jl3;-@un+d*?IRU>p$%55sH8hc^41gcyIzUdaStceV&I!DENYiE))Ip43V%@Z z?fCuZeD3;OZCxHihK90@bg1_6|NUvhj?wXzX=d7rRv6l_XTj#W$JS2&NaFETF&kW35vCyU-oKfekawZv{p^~Dm^yb8Wx>7R()~5A z)el=hYqmF#sR4aD;}%cybyLtW-Y;Ud)0AB5I7@N{bkre3&-C(fWs6MbghgA8v`a&6 z2O3qi#k0%5)#k8sIPL1UN>k&NEN5Nu*9cK6LShsAKmw;{S*~|HMkCtq)8>Ov{r$de z&ub_jdsS6%`4%AA4ip@}cB?LKC(1~N(A1Tx7D35Jk*tu9;j{$8&p;N3Rz0l;LdU|_8NBPD1JhE?KljLU#7njSvnqw9A z)NMj$MLD8XP!hXogiDbKyC}rP_%;`Xf&C*!t(2Q*TSyV_{~!v6(^@7kwO*FQ zQ2HwXk))(-nZN#6@c11&L7|gC{zTQ1|B^sgwg8ybf3V!Camy*C*kgZUUc238W23b2 zyISBvhxqB5QXpQDIkoiei`~v#fxC1{9KO;wmt|WWz0cDy%$}pO|11t>Hj>MT(niF* zsmIc7Q z`DbHF;+v8jT&iJ7MQjH}kd01Ae%`iyb!U002pqF~9V=twC;$MJo|R^gfy~gyU!Ga!xgU z$rqs(@E|xEfJ5{@>!KT=)*g_Xw~mlAk{3~tGl=tj1l~Ydsw4@~dw1Iqw>2gqH9SX- z;hd9>wt&B>v18r1Qy^%?^t7DndPTIaS2$VzFWo}HBwqB4rl#wsnpkrmR;&IGT1tV_ zaO=ijR|6jP=Ol8}xKFoUPuW+B`@`J+M7bDX_dq1M}6F}R<-5y| z=tHH0xlRgR-c-SQ$*Yk%v<^}ll~z1ms@QB@x!#?Sh|AKCVEb5xF>S&2J;sgYX>v}@ z+AHa+kx7d$yTMxQi075*$_DI76+mHFHJ~?k7ngGKl`R7mT)0+TYOn5Hka}nSbOiss(#NcH`Z+@uGL` zY901pb(ntX93*596G^gjiM2}Dg_;@c{W?)Mi@KbYMsLeFL8x#iWdq!5mbHF;x+~)7=2J&ZkO7<%l#PRQl z1x}z6pq8B2ab@FlhlK60^;X;8#wLGG>bnp3c_gR#mYEL3c2A-U05r-JDe89ve20lj zE+=Q)y5B$+qw+NTo%)h?r@GwWOiRUxu{8e}kS@bCxv|3a$DrGu8_}F6hM5sl^hr!L zRBYb@>)q+UF6(rLNRq$fEAihRCJSg2bW#Df)^dOv6fLTp79u!E_6P9j%RF`UfI+XR z?g92n3N{?e znQ}Ev2n0cTnCD`Becu5HvkRQ_P^dKkzNQ020hp-tmT$Mkml=NoPG9Hvy~&NDk$D{D z?Jt-Wu<|5I#J}K8rA4c|g%F6-vSL|fg3B{qq!S<-Bx?@0>V0wneAeF3&IS^+k)C%$}2S^U)xe;RY%*(^N8Ve5#0C)A|V;KHA) ztA+*+OG@BFi?#?`w6}I*eknik7{f~J;}mBVxB31NFG3{{=D>r|W(Q{@92_7Rcl)Yl>8xzQwTn>yV(M;;Y# z3UV~Ey*z>y(`bAs@My*!^S)Yn6A`0NHxZLKU0(7)ROHf~;V0|u(Y(cpq&!pr*4^yq zg%RqS7*P*hc?I(t753FT%?rKs67Tc-k}+VkeR3*=Z=7+|BRL1r!>cXkzwU`HZ)7@m zQ;FzlR#NniSVMXf(n-5#k9$iI!Tj&hrvp8)vqv( z0Rf9Gzh3Pugjp8ALiw40F52^nVO}=^dntY~VqD0l$~8pnr3W?ll3uaLC*+X;bB=Ez-5wHTXP5_uUFb^mdapV&hrTW~Da#fNT}slg8_!rn)bhm3 zE7;d^X4L#ytS~7@+)htKvpYGPl&e@eYY^O4O_K4BnZbv0&l{Y}BsA8Blm%*w=Qa}W zXF*6?8zKj1B`LdiXUSGIN+>linosZ>Ylx83Sp0sg#`%&5v&TYYoA%tR7$Q%Z%0)1K zAImEtrPyR0=Nu*_+3*ecRwBgPRREM@y1{DR^j1xPWp{Q?qK*r_IN|``l_k?)l$q79 zaa@#jSgL0tS$=eJmzoEKT0=n7`FDhBjok!~TB_#q(zxeU@WCh1Onp3c3U0M*d2}s5e}--fl`)fcC_qpYPIElL(9CrYHATqNgHN z=B6U|`~P`>cRY~3L|8O$lifiH6QoUVo&V1*%S6e)_7dymXLlFGghY>(>zyyXkFkOB z8|Zr6#uVR-QKH7H?VO+E@Ao}E9LlaYSgR6xnp%!el;HzIN2*$a7hl z6j^)OT84IItg*RlCU)O|??*&Db_cVZFUgmrSLm=tbC&4S6=IIhpy^m!oew6i*y0Wv ztTFL^a|};Ofn#G(h+wILaNNk{u3!{9Zlj2$H&R%_r^+Ug=W1TfI)2?qowfR|lSyOPZ7tU&^cXnBc1c+AS0V1oot zq~|d8ucM+5Uvj|~%Su^aPurI^=al|&F~<12nM%JyHs+SERp(_75^6$E10$05rNLs; zj?(2V>C!JMU>**uO_sbC5sFiZN+Rc6x48SB(C4{(s7s!6J!k3EdM)^d-JAE?LwCM{ zjPBzj*cW~d#$q01%!8m;M=*<{BiIV?tek*E9P_tvKcK>kAn}`Kh-_VPm zLN1HA{FsX@QHK1s>3-gq#}^riM2(I7G|vyr-tg_%oYz&1;deHTz~cFWL`%BZ)RE_# zosT&@c=&BiJZ{yR4pc*qU@}kUAMMzF@dy(s20qdcmPU@Ru#WV&Qhq{qt`2oZ;~K4Y zERko>(o3WGi1Crl%v3^7V(fhnzgw#vv^RJT&oFIiy`_8z)9|h&FEi9FvG`R94fOO9 zpH23<#Dpj&bqQQmDqiy?u}VBQvXT4w^R`C37;%ZYkwb5X+_fyBDk?!z@RRAYbFQ?X zeZ&`V;Ag#Avt3C9(Mzp7;vyK*8GqtcE0U%L0k2l6Zbf-)-^jG8bmSL|S7-G(PkU+H zQ1n>rO(4VNleZeFi9`>f?7h=s>}?`G_KU7v=R*{!7%1llRF5br$)}ML6(^U7*QwB3 z;k#l)ZCBvEck2Cwptf*aZSk#47Z<*UNI~Z0Gji-0s_yhyF2&-b5^nyPX>;~~0+B2ssV@)k=BG6ViUZ8^JRi6c%x0Rk7M%Q^axuVKUF>-0(Z z*l|~>8SyS=U)8JFjlw3JsLM_;?M7dwMy@s(+knWDr{kKhmV>;E@lL$?RSA^gPB^~|5A@-`p zlCSvE*;ZzrWuc>%)EHuVS#It+pNq;~ka%gse8!`XChy3-wj=IgdWny(gnWx~&Cj!& zqm;AnQOd)(#J}SAu3eC})H~MH5bv28S$sctMb6$BAzK}?M+ZqQ4^Xp0ADlI&w>0$( zEsa=iqPJ@PWH_a;9>KaBjtMs!Rtbi{b%T9xr?=A!cLg1Ld`0--uczYf$T;G{=hL^b zY;@Rf`s)ZQ#~s7?34V`14+YX2aMO0N+uL6A9swG7x0z;Tc`IWb3vL!et+cE9#4%6~ zT%8>F7)h_IoNL{TV~OZ3jqq4}X^X@*eGK8()kqC7XBUM^M zyszvfvLwWP*}stQMqkJa#)3ZXtJj*OtsyQo+RxCy6Zj}59^U)vY$m_*#F&Wu=Vk&g zUfdUapYXh~^D=^Lb#h`PVGjz=k;f#H`!Lab*d2`dxX$X;R&d@i<*_-d^wr z>+XiinCo$tko-sY?nofiR^CS;FLe*uy&yaOq2WrybHe$Jolz$N(71$=i{!Ym_6kh9 zByi5PGc?q=mpFMUC_Q5Q-+&zD<+jJ%YY1?OSGV1NKrHo%ubJt^A27tbRV?Zy%}Nhs z!Zwbk-*suNVgXiZXX)H8Ras^c7H^hxbb8SL;@nJqZakEkV^;;%SiBq$ZY(kcmdCOd z_qLFT&$YD*?~B>@*^Db-xbB<f5DCB^CMN$qM^?9005uQKt z&KT=F*^j_YQR8YGHftY+?*n(S-?uN$Fue?2b+!A^{M1qS>5&@OgeB}E-xL=E=B)0z z^5;lChpVxWkV4oxeZNcesw~g1=$#i^FsWH`|7#wjX9((v+L(*%IbE z!;Xk!(Qug|TL_UJG%26a(lY0>F*?RO*?=APzg$eb8TP=;*$@fc_uqS;*Yd6eF*gy1 zts{%z|F-@-waJmzVgZwKAn1@>?PfA{R+n=)Z8hz;CxP}A=4;x^$o)kD$JK}Xw@i3NRA(&yyo}1JFJ4Q`<8YN|B3LkB-Ue% zYqt#?|2vn4Cm(;dQ3$ncRE|2{&^b5W zogsrUfv5lB#u~pYBkvt;3njY%d?1`ZZN;?DHiUXaQ<8zlIa_Q{|2yY;hC$-`p zv!mref5wFa?;QD9nrG~$KHV+{BSJTr!?*N3@^ZEL-(;Ij&|!Fm`0kAaAF0tL$Gv?p z9vEeO1hb(KC?Xkp1#E=f+u}a;F&`hrK0L}`O;{(U$JiXHZ-^FfnQcCPtEdxOGy<%r z*Vd|2@H#FiNKTVVb+M!vPwX1PC+ct~W%fG@f9`RGh{Yk!Zw+xmix9hZm< z8Qu=mDzfWBfxf&@%suQ#R>0sK5lGHT-wn7dOx zyYK4GOEHT|{TRmX_O|}HD$|qjllzawD&8W7B@$+HRq^#qnthhuB?a>3(IGnDWw8oq z<=sGnqQy&Ssq>mAdd$>7)!|Yi z#%M0sT6i#T^oGl* z_lqAs4pE->L=+1U^#w8kY5`tX zn(@(J&Va+?=WqHd-|Xn#G}~1h*aUnb12QA5icAmKrsyX>+(h4Q0pCV4L81v3K`| zvMXyG)uPaI$<&}=cz-wQ0sWO*g(??$aVXIS^IlOEJ2#)lghb|MPN$<5_I&`evTqT0 zDx7QF1e3XnI}yjgVmwuD*r0mULbi(ib$sQ1+wysU>mK&~8e-KIW525L!_CX7ZAj() zFQ|N6D;*2jcs&0$u-#nkCB9FzvvKJ2l+)C>IL)u5++A_L&})7#{Joub^;kpKeSwq7 z70tQ&k4QGG&HH`Bp)cRz-ap2l4$60r$*`tRSh1qFE05hy1Y&@BTrllvydH8bCXE-} zaM9}i*ht2KY=OVwqkgH$ub*PnxIL(%)oDhOrhQ%Zpen9kY8JT;N2%AQ2jU%b=90*c zvsC*gye*Gi-R(rFZ^US{leVocj_mF3j|nGeABjr_oJW834rJ!9O0a9{Wc z$uN@o@^nd|vCP4Hl!38~xcS=$KDs#c@X}|!!~>UIs{No5+n5!5+2;(L7fwt0dXr}H z#aq}=C`H9-B1_ixaRZZ z0Bk;boyF=EPRimjUOK?Ug()hl#1&V%ByA19&`g!Fxy)Mh!F&ViIqVj~e9mH^Sf;2w zsK~vR|0REcv5p>-(IyXOKN(rPj#4I`*v0|WlD(h!Rb6I^H{+H!{x2+Q#x05^ZY5!M zfbh2~d$P7`E%EWjjj1%gVRb@qT6aG&`ZxD{v;p;}E2DH8cG1!5>JOI~|7zUo>W#H= zPp0&z6_YxAD087l4Cg-ti+l%DKeXnrRX$QihP-WjL@yih#gXxHSa1a#>c%G*H{TJZ zg{SJ{`)H{^y&T=3E}*W7WQ=QjN_iZdy{6dvvavT5#b+C1_spM#PcRmC2<>~G#CpU@ zZTJg$es;)j8W*BlcY2>CX$0KPY1ExK9K0hbCF0L6wi^5BDrA7t?|Kn^t^1#=mh@p? zzA#FU2>LT?ST$zGc-?P4(%3of@n(E}D?jEo^#{6ezRc|S&LNr!KEmO-r4J+di022^ zqZ=A+);ClX>QMdG%OsjPiU+Y@A`%p<1^DJ-Ln36)UgItR%4fvmN-h3|mYtI^q(G-T zE;FK;Ki=Z*G&g;$LhO8uinfgwu!9L z@8fhnwSbsydi=@w;u=fTIO(WAh}A4s-rGEJ(N3F{wq~FVjhn1%F@0En>j*}j<>mc^ zj-P>j=jC&ujpEB7IGY)cr(A=eCj1BTv@f7kzTE2W#@?w#_w$HhoBcO~PU=@4L-6$i zRtN96g2dpJe7c>t{xEuAWA9RPW1xEz8d~e?2u2DU@BECA#~yxhZ(kTOy%xK7eMi-0 zd#ZdgcJG}|+>7Y*C-1M&Xsq%H>%Cs#_2AeEJkQ5gMYWF718 zB=neGWy0zfpL-s`oYpsmW3r6U#RGk?xH>gdkym<0RRUkQ0UwoR31iJ@yB=nx%O!l! z-Dh8yV31uT+&5#cN%X`rRFXSX84+XKFHUdvc>z)*ihm=gx--cK{0_p{bPopo`Qe5d z>K59~Q#%?FftoUXAe6-oC%0?-6}h7zjb``Ei~AUNcpS{yba6_7Wxi**$OvnHSRq%e zgEZdAX^Kj6iO9(^1rAuJGhG`CQ^0U!zEcp}dqK-6_UTEa$LWGw9`tuV(d+WUn_ShD zO*1`T)O>B}ao1BuC9&4H;pC?9x(|+4O`p;crj1MU2H^ofS;&B< za7l7|+!9$8mcputt>q3iy`BsM6_{cA&Vbv6UlhGO=PEyU>E>vpnuKNNbBQfXNF<(? zG!{+uYZpj1<&8cUyGf?)>6n8lkETQol#)2Hg=x$OaijVHok&vy zpSZ|7W&x|a%CKEsYs~nU$GBmmb-M73mfVX-(uuH&B`!EERV{cey|BSGJaPTA*UT5ELYOphsLgN^ zsy^jFYE{(l?5C+u-6A%aTRAd{+x%Hoo=Z}(_hfEvlVXQY_1TBp+7ZoS~Pzb4rqu|OMZCA zyVRg49g(81#H0ptSM_6c4Mtf=veZ%X8p2sq12nxb2|I{r6K-eb@dEtEY;bCgTNFn)duj_L^f53C!KV%qYm_3=Du-E%I zj@Mf2717T=ahBsf*pnPtqbQa*b;tNk~{gh+S zC*|;~4ZF<|b`~Onhb{>350s52O$~w0Ur;C^y?$@W?ID3JTYDV7r^kUeWufdaBw`7~js@(ig+>CQPwlo}Tx$kqG#B=A3kgnWn z=azh*)*N5@So6|GSt!2YLRkI_;x2j!nvJKd1we;!08^gXQNc5WF&T@pRxs6tC1dr|yWqn^!d;bK@3w z`2jRe19ZsCgsZS3p=Nkh_LP89-jPnop(`HXSl0_isv;MMDDKdQzisEGT~f~ed;jn= zLCB+_#{N@Z9JmjDFpp- z(Oeu)z=A^VOZ49VZM)CVp;AnMq=vK-;rbtOHTN;IK%onS1)MrxtkoU7-|KKS&`@zQ zY5e(W>>8N&{5UEdaIUt3F99lM@)Zd+dUU3)+p|P{N%>}$n5Ig)+m%RS-{@U|(wkla zL$BR(I?oB8BzWi!w;Qh==D!EyE-&7IoxoK2^1uAqdlx-2RQ(8?{;a6LrdH(N;mabj z5G`upS+7w62B^BAqJFTG{{$L-PH;>zAcCF<+52Y4ywP~Z#fdt>k7Xhgn%M?TLPsN=YpUP1o4Yo{7rEu3 zBHdIGr`CVI+@_W9&h3#E0}?k>YD&6R0RF8EQT#o${-^+oSn z8HID| z6{uKOodT2=IO=)6QTONE<)ng7{f6WlGlm1dTc6$KXiJCRcyxc6cIbN*#&d57oqEpEVtg7ZX?2NUUAH$%oYcBVtF_hUC&+dbb%bL6;q&Xf>qQSG@R&cf_M} zz4O&U-LGYNSq>i;Y!jgUjK7LzFQ90k-72y2bU;*4T+Pu*@*VGDF0#QiAh(n_0_svZG;*VelC@_ zl|McW3;@!8-G6f5RlR1;GrsSL7`VhiI2I(7e4=F;c_x>Rx8AY%h$01q-hBB!~=cB>rIvv zoTs=U_j0paYK_JCh`l)l*K_8lcIcV0YcJJ%c}HolCdZCLWvV+B8A6lC#STwazE^JM z(zo7>tUZ!$2rQX3Nl_JiGvb~Btda|m=f-CkXT5c= zT4+CNXCr`u9zuNleY*askKnh1_|wK*g0K6lkiw}(#!mJu!bLe=vRACy#A@mFyFb-b zzLA;Xn<;b^1=hnvz|FOhd_}jlo;;}?``Jb^=M68%N!ni8*E6S6`2R$l{f*K{sGI-% zH~spd`t&mLQNLjJT^}a5!e0*O(FGyNeE-e^{(E+mCvVpiBOTU;E3v$7{^roY67c^2 zr)P$STM4rb_jns{1)q?B-Tr&ahttu?f4 z*G?Pwz+Qlb&s;cKXLc0yDXtng9YqD=Fqkhl+C?Z|WQ2tX&@ezym^sGTK2wTEU1}qt z4&4SqO^{1I92$^ZYBp+mSV9B}tp>#lxSly>c*W$fi&-BBG?Y(n`SD`LY?HL8MKWN5 zibST)c*{Z}KR6^Onp5;4Audv|TgK&D@z!}zR>5G`jle4|?t-whXEl>{A~H@zwWrSb zWA|%q4@7C*JGXVT!b@DQoUbBBC$#I_PyrD4d=fX1>$Vjvf$~o^@~tFwyGwe`)sALj zn#R!AgcHWk)yqWw#?=kMh}GYN?t>-LLYF+e~is%~=~*iO-@R~Ge(b5Sq6@Bz|64siPH zSfgzQ(rueNHT&)QuT@_%a{ajFJ(oaj)PPSE|M9G=1Wzc%5>1E(rJMCt}LZgK0@VqeI} zvhw+i*#~nA?35vxH^P>7EkV}ZsY|Ni*2;r;sZS=<`YkM5fnm(ydkg2-E0~ilI$4@= zx^*kWo#V9}%V@l| z_EhRRdEC%8r0>0F^J3cy2$L#tbg$&FenD{UM)?r4@Ud*MA?3x-SM@VP52sn$scw}G zHEzV#P%qjW4w?TOZ&1=wRvSuu+jCjat-3{=oT;NZN8beP7`nvy3pM@BIUXZ&a3N$iejTA8`=0>jYW&*hOkOa?g8nv$@6LfI z9LuAhU})Z}0bm$~dw}d-qx7BV-41%RK&%hmA2aLkKm4dMfoc5#9DkF(@n z_SWXpt_g}$`HbEcnmjXn&FiG*!+nho7Bajt23HJw;C}^m{0|i(Wxn5l%ee79q+k6n zCh~D`KXP@U$Jggkcn-puxasfZOIDhUkpm+->D3#6^?1WB>quzB^gTNuoLu=C`cZz_ z&c`QYQqbzcBxW<)qB^f@<``t|W<5OW`92+IbceUlam43xDcqb(4x$4mkc@r*|oOYo@V3_g1-loTONJa z3-9ykKsdJkbXRtkzBU(&({|;B9zpjyY*8&LoD1t~HxFt}udhEvJ6{~W8CDKQeShNI zR-~@tQD(Utg=67e>9hH?t|XlQ1Dx*ej4icI%2jM-frwha8?ia6o!kqcOsPpk=6s_w z@oIZ9(^vheRG&iJ*$#zrkXV)$x`#X_c%*}HU(&{hmcyEYDa^yd%R>EuI)R9<4P z$&4l^9*lnhSQvL2t@*G}69Wrrk-;D4fzh4O-r$uy+PsP$S(+FaFN*Q2u;DD*c2961G;-DwD*3?jNr9+vHl-IXc2xCs3kA$ zcQo@)9j5S$iED@Lvoj9!XL4IO*327D+X|D%sbIk!YLp-(dtM6SmwMP-U6y&cg%f$N zitW+*J4LV6-=wZ{=TIof>=|ahpinx@1NqH8nfzloR?NP%9z>?hO9Mzr1I0A4lrPW< z+a}-o4(Ls-gEOZtAJ|>PDvrz>@!}<`~1#Yu)^-7`6YR#bc-fnR`LHyriD$) zs)St=No-7CD-MTNH(Kpe;U8@=8|ix&XYBptPP-)ZeIzzf30mBFyA(dZU9GaDXfM%J zDbLH-05sGk)Mmale+ek5imp2HFmlgt;yWkeREJ3aYpNTLq@t=K1#+%KP-SdkHt< zbtlQo=qTuT;qKM>EBp?gw_X2Ak@D+H8xP)zU}XpLQ1EFNqrUfb3k-1EL)_JV@Ex<} z?U~fL+lUX#gHF0)6}AqnZ5NwLr9iX&@=xW$=V7Ef{nu?w94LzCz~PK`6UaG|9;A+f zKOFe0(5*kVt^?{E@l(Je zmarV-oRtGzdZUEY)g!Nr9$~qwE_lM1f^@y_x{KsMwU!Qae#Dr)VXpkp8n4?v=Xo~h zJ)VvCu&VR`Mz_yG7${S1%cofGiivNrt6A*GnJF?ew=)_|3%@4E@U-&( zL(c5{2~LLXvBYtm+x5NUpduP9I(bh&v5LQPWCh&#Z2?p?y3B2Tk-Zc3oh%2a#4EJd z2SX`U%WZU@c=op7K+7!#Aofq4)v0qQpn37P0TKTo63t6Nd39NcoPq~Y5GOT@9#Y6& zQ-or8tF4XP%T$1wq9f_?fJ$_*`+MIL%Ua`3Y{GtQELN6_x7G|}=iQ^I?(s#Q_e)wc z^nkC8g$a27xWhOiEBCz_TlK4bAn7*9(^o^S!7cR67j1M7Bl}w?;@jp!G0X->QWttD zlshQ3cHsdK5pIcmS$t5FI~cM?-u%ejM5kG7gt&NpwX%dv#Fqd%=i1c<578Qn`j55Q zq(>R$l_OK-6|(Of&3#4o4k}Hrb8J?CEaZq3a4wL_lIqtIs@Wexc>t*pSWIAfGRMOV-lx4wcNR zX$EXRCb1BpRfeg28=sbMM?Xt@wWvozg+7?aW!+gs6#`@zC^n9q?K@kN9jYzwC)X(21rYC^q1W?8&u0ocIc;qNHV zd+sVSd4v+wdi*af@<{*LkASJ}hbMf`_wv#Kx)Mn7Hiq2Y_g!U~aQH%v!U;2aj|jK+ zKl#S1oY8l)+mx=aF>O9_t$<~G_Dao@k1FgWl~Y8kY2DE@SO*VAkvT1fUW`#bBX8Ayg(O6dDW`Tr#w`Tr}UGk?qI z&q2R_<^Pn?_xoYoulX^Hj^d}|)>;5EP^zZJCy6-XnKUqPz6w-WNsa+)Lt<0$l5g(<9%)Au!cvbILF=5*kaP0X@1ZmV`zxQ?Y&%ZpO znEzn`dKl9+gZk&5A{a+T=ufjAzO-s6T)S(EVzvBA-yFN{eVmtO;Ty7fua|xVv;OQC z;DI!DglyL!cBk_BHoU{&!=I!&DlMchOW1!T?zD%!Z_}C@ zm}3w7j?Zz%or`&m9<75S7U0?1&q1L_$Nh1n zr!YOkM3M0`f0F$TUCu)mY_Z#Yb+Ds(Y!C4F6izJD3=~7^6d3qzk!LXf=h-R9w5)Be zXw=0vBR##;OvP$3BiY1?En&=`DE>JhSL7J3i}e4f!n{*~UqP!aDJaL;CQq9sx9`HL zjhr$eyW!_#ccGt+EYWLFDS7D01!%OPqKa6M!P~qwkJh_rZtK~kyDjTKEZn{&kOFE+ z4)Z4|zrF3z7k1PX#*!#S$YfqvQxXX!A{~fhQE2%R=d2HffEJ&s2PiTCgG*{R%t>wt z6xIGbazqu*0*wP<*3zrTrg=qx1cUCL#0%`Sl*dhgCZ1MA2a1TBXLmWZUhN?yVMdsr zjUzg}6w-hP*@n7yr!^{_r4IT7L-T6(x9-~N8PJe7`g2RiFf)IcjDawX6cGYFd zE$o~gVvXu;96)uhFJ`Zwb5Pq>_$pIDSM!RFprKVacG1ca!f%PxiI{!7#e96o^vL-DpZPp96V-L;cZzMB9*uDP^?pGgPwq z6!o5Liev?WN$;AR#3RxXi@J(-tvBH@? z8{mE21_DF&yT81NC~geb6#vAceRbEvp`S*4&Dz9RVdDC9W|wl7Yddf;zX0>b@h+ zQ=`-7_`j7^NmB}!0Rx^a7fBRo0f&q=W9M#W_(nf?OBb*I)f>yV5EgoM}Zm7zgW^4Oic4_#fyhdL6=qH{1 z(eJiZ_v|>leq%S|%PVX~p^C)@R4cZB^UqEsy6Xr%1lS9rS=Z4jsWEJ4r4%5m)xt0e zq3l`=O4s(}T=q!PWsG7w<}PbAoag>kZ_vlzviUxz@Pel_k29s8H8Fv%uerp>_t8p*t`)X zF!)0@S!%O>R?c~)W-_L5luTMj&qk8p$Z+#>a^dDprdOa`z$zYZm%b+Rtgox^c(Z?K z)dq3QTWoXH!u1A*a#==#;P|wR0qQTGGq|dw#EMs{Y}tLOh*NZ!zr?!XicRRx{f{E0#dzH zr%z)G-E6V->sW|<>6G@3>INiAWlLh8x{=p_1di7)gCNmgrG){==g=H#Br$63&$EF$ z!lJSZtoQ`PMip-IZtcn<3XqB2bD^#ah|FI%Hyr368Mwz;X%0?z%KgT}YxoxQXxYl_ zU#8oalqk_Ozg>$@r{yYnoM`T)@s%q`dTP(cx9z~$_bBqI=k(u_S&-w#1@Sj(hPLJt zBGzvwZTLQ(Oy_bMcn@aU!W$=Q1czkWJ1QQx&}|jg+e%#6O!3O~hN2uq#A=kHinge0 z37D69)I#L!ejC%*5>SP90m#cO198wPbgbD`g|9--1SJy_Zgup99xX77l=n=G)<-&uZxI9r8wDG2vZyQ&L4PlERJ=W{*qn4A?W^~%9hi2zh7-zkw&>t7Ll z$;=3(CL@T7%Wy*`mZjn!OWTOW-}6{efWT~VBA@_Bl-a03Up8o{M1j|r@a5iFVZo*? zRpZ5L`;I|g%J?0Mx}^a7Zg@RjcwS}^o!+6gfP_?2Tx21qWvw6&MCBk;=szY+9}4v$ zrCR`4xe;UAxzv?9FX&CWR(j2au9;^&e#Aw}Mc%s99U_`1tq%hZ`h_7PhUO8!*A)J? zOAV{$;sxr?th~YSQl#Pe{**vzNK|V1VX4mFh4(JLaeGPIBvUoN_l-1K8C0|+jwI~t zO!YAUxd*zm4Uiq=1K7^;I*T$VcH~;bn8Gg9=66?8xw=bi89pOy__F$$y}4ouU*mMU z1u`yGVjJknUawj%N<%enV}R0ujr#zq1GHzQJ2r)s$_5ehU{Qm&8ZMrZf>?jAy3M-1 zE;8V$KQw1;aU??JeW!ozMS6B^kD2*zLiH8%6UEE?6-!sKodvC5EI=9JG&!qU5;Rcl zWW@c`R9oEk3Ray2*Qs?cDg59S&(l#`+5~$Xs%n`TBa@xVqE3$^v7mIIoNwd<_V(Mu z-GHF!&%%S>ZwIP)by)bd{(kj&ob`?->#SD>=JgTUFl{Mt$eD3HPf( zLS4qHQ~pKgx05izQuiZDgdqriPcB&GbgDK$-rVFl{c+lv(sw*%YoIkAyJS&Lnj$O0 z_FV8RS;}kl085P4bl@fLl!`7X$152R=NB@u?y061XUGK!s1mO)7k=Huq@+ zd7K#j+mAfg8ZvJ0UUUP!II z2ver&=bVK!>)( z6?V-(66_yYh+G8)X&=-|EkBwu`)f;6TSvoBZg=ta7O`tN`#cZ$)&26UZpI*BGS+?H z|7$jcgvy@{2@#E9VX9f<__`$5nKSGzv&;(M2kW3OKxH3Huq8V_lHAe!`M)! zG(&EWWl#VGr=83L$1hmQ^Rk@H_g(FL33R%}?D6+Q$@9wM{!Oy%w*$q90wvHD07I*E zk-TK*fUh@Lnd~B4{e%*ewjjov*~fS{5fj;E#k48g1diW*PtCCdyc@l_!lYNT&?VDy zpryh}^%{japxtw%M8+54=vXQVO0~}#8?5|Q+369(YjDwM{^$dkrCziaa%XnVTUf2= zh=7Jl2T{=)fD6_5&rVO57de%h31y&AXkKud1@yBSZ|pTtW8(EyUcUWLk@+7D_Ww7p zB5nxedg2}G5l4W8?~u-K{uz~eH@_X9AlZ+!XYCL(-N@RG+?uNC+z_jxH0G-+we7nO zD(o11_Qz(0m(Uo>2N{Pi zeBDXNKJ42HNLC*oMci7l)*|#WMBDo|hY{y13AE6is^t!j14}PwF1eE1-Z%RD7njX) z8A7iwk#NFdEq^WG$V#LhcSqB(-Y?7*oHn*}XInEuYcYN;E%h?Wv%OOD61=#%;tCPf z6g0?X6XXTDP(vQEt&8?u^f3d;ZsRPIcj-QSpI#K0#$9ZQJ_J30mT*%sGb!cwZIr%m z0^3s*uc9m>0^y8cg{#7ZUtNXG9_Jv?T})O4w-?b-=~@Vi`;&%Da_}z`)e6A^m_l(PI)fL zA}`fJRhnP8)ySH8t$v?1UJnu|r_sBXoL_9hndsajv?ROt?b1S_6S~1n`Ng&=P5xRc z1_6Bu_DmA9S;EmNVytb7_75rDi-Y8Z}SjOq7m%=STo10$i)kLjP z4j3(Wa5XLrM4ZQ$N_z>@wN4rWimD{nEtzCUJFFdF_U%=b-4bX^$TjS~D^TBDreAFoB0b6YZsqhA#nC^n2WXYWCO@O53DhO4KE!mx$ZSuwHMBS zwRba01bSycLL*u<_M2#j(UV1DrZ=n5kA<^{c5$YRi_(xDlHO7!tv`5*5A;3N@(yib zxjZsV_#Nnu$Xbdw#D&Ig_A1ZJzUg#JyiIv~y#&1H7AjHE^JEKX!zGvL@R0ap=6;2) zOkS|l*^Dq#g9)Mnq%DCQ?o0oxpaG}&~G?~5p zz;d&;b+%1y+uYWB?{1u16*wRP8hkJ(;h@h9)nfgKy7;5{`4WiB6tmUW*!t}p6Lz0e zn89x&kZYW20=V6b&TOc^L>TS-Mt=UWCQZgtiUZG^L9 zF57W6NyNSP$jfVMvASIrr@&`F=y%gyZULWx1qeH3yX}JCf5|6$c>~#9f*}U&XU(p7%W57|hRB z1Bgf&{)M9jLmwJ#Z`!k(rbO^kq$mqTjp+S;??&or?k=sVA`OEe;TGmmo3H1z!yeZM zT8EH_t0z@A{;h6I-Pbs4m{T7FNQzriZJRbCZL?0oI2lA==kx7PXz0v&h2hI?v3-3l z2^5A3=qYFRJG$1-5jFXjoS$*52du!fprL~Zt@ZoOh>iBysCpEkgxC1bg-80Tz75iY z1rdMSpf|i4<_6IKm!DcL_+CPZ(i{W20pLr>^xC(zl70NB87% zn%(JX7`)6(>b78Sj@Y6~62R1mudbOfH6KR~ZtAMI1IQ-Wp4iU`MC+(_x8<}#mm2)s zt-mktLw6RYb)KQY|Z{xe{T9On2Z>6v?ZshqQr{M}Ju&DC1pe->>g&IGkkxf$Ozy-k6~E%mZS(5n8! zy^fv8QDeA!ukr*Sr`Ff&0HzXMUOlsy9kNzTA(k6(Fer34_W!0f3HbsY0SJ#}{fGbQ zF!0FOBO>jlmlgac;iiyyZ#2qUzhOiRfA1GOcXGS^Wh>pb$Ja$~H`8^bCx{+AiFcmE zMG%V~<<#MTXzQpz;`bYyfLM+_(=5QdKakY44|*bcqm~6s51*R5b7hW_ z#?PM8ebwda7uo(~R;A73+eW6Yl*ZJ39xcOg1(S_hpt{m?@{7#lS80qEfzKQ27~LE5 z_T*grjD|M~c|GVEl!cg-i&#$+!?F^(2L@?Eyx?{&ZL?b^3MZndp*`+U#fwjpsG00y zW&N5)-kS(v_;K_w*gjw$L4*EI;KK6nX{^3WGpoMa!Ao8GFJrS_cEEedU3sHsnM{5Z zTzsS{3BHf_DM`Wt7dH3%C*}u{F+5~~=~{M7z6!ev+B#|=9PfCvFbsN>aAsf1m3{#l z$OgS`MUipf*ZA@!_>e~Ok78r__FhJdQ@@1eNmb+&%7;4fW85TXJPtO^aA`}VN)@Fi>?Mno##|i#CXMzZW*@ab{c3h z_ML=;$m*$(PDAtx;EUE2eWm+juidOVi%;iS!@Xgg4g&06y8CZ?k5?+8{JWh!34S}d zn2+HZM}rlfo>U630QTBKlK^_7G1?P0(UA?3dTmy2HO1Z?MwX4AU+T`QyK7C9-Ru)*P`R7!5YcXD+B94f+Q@>`x zJJkO3`kHb2Xe`L0RstK~*A(GS7pqJ9rHZF68yIYZ) z=mf6;ws-4|KCj{2;NTLa|YWObkc3aYas= zS1ZWt^~|43$tE^*f+2nVYGc?HINoqdhYyz&pIY$nv_%CS2GszTh$7AsM@|>fDA&5} zay)k}^m>B1O?KCBC~kJ9RGr!gnrh&vy*sU_$5>dzQjtVi{NH|N{-B>fkUcD038q&1 z$8jp-ne9^k>3-ZlaVd0R9C7W`;==7snvRT6=i*{LskiDM97EL(ix|M1NC(h<(g>Lr zq5qD?cH_1x@lLNXIwE15nlJjP;2J7EC;;;f{U28?;FVY(bGfb{4t<90@Ii1}0)55m zlW+nkzLptEf10ewWEk-d%fzCq(FlJ1 z+Z|Kl@rn@DaqS~SF}c;4t=rZPqUdf`>sxP%$Tcj2Cs6>$^_&Z>Z4)dp)z_bAP9boRZS?#J0<#)5m;K zfe<$ry`0pVgz=jxBM*4R*7PLXAAGu7t1lH^UyF!i?l_4o%Cu{H7fjO=9~XLO1eJ`YPet&_;t;_$-)oy)f|tXwo<-z#XUQD26{R!I!QS7YCt$x<(*&mlPxpU z;9)E@2B^d*Q+w&>G*Zf+}D5e2Vd8l>j@tp zl-G1ch2Akdbeb>1+I|m^2bTdx6pGL z+AYPRjSb5au{%|d&UMHBuO-iaE)WX>%sO4rh*YZJU*S`p^Tlp6vq7Kb=ncc!1ZbTDu13^aW{Z)c91fV?~dEZ`XyvJ~wA~24HcngO7A!PFi;=^g$2fzR{0_ zma9j_kRj)J>@)?Ti8S|$>&bRkicdf;oX@mnaOR1A`o2GaENo^zZqirbA!dW1um6Z) zrF?vIIYumG+8S1=^nGX zH?y$@pWW;xkqe&B)8Mz4#83lBCbs+EsxId!FoI2u6~O=b5^v1zLP;osWNnK$%Ia&& z6kTt)zCLe7^!nH-mBS%*)9~G&X1_Em2{HPV_xVSRvw3VjQ=ejQ>S6@8<-zz|xO3D~ zId%tM)l@y~ij|{2isF|_?|L2-(~Y7to$1sh{d2WSk4NO4wpoB4e7?{{j}67Sjegc% z-b zpfaqO9nN*Bw5BG1GuuU+5?Da&OLEMs`hhUm)H31i ze)YD^sN9`4_&>;;fE-NT7djYDJO;1S? z7MGH;s=$P7Wr%4|>gW?KK0gTJ-d)QOcz$lHm>%@fhq@26l?UeG+$r}+XERuF-KHxw z?}khNvhh}!jNEPjcFAguk-|Z~Th0JI;W7(#ud_?Ut^jWfPFrs>TVDk`Fm-3itw?Mo zqfuT8M`UY4Ul-mDt~eQftpW?Q2EXQN8e_TPOITpBXU({pdGG?E357gs4NG4o@*fl5 zS~5nBUGbbnoM5_FU@D>9!g?iwDrq{sgfdjo{F)c}{7V9HjXF=X zu619{c>mc}C!**_&>1XZL#-Aze^(LW|Bb8(-fPaq1)i5 zM(xwEJ(bk9V}Ej=hMVsp=Fu8ju=}tE3z&~oeEnHN{A9WGd~OchT;yscln`3)*d+T{ z?qR!$d2Gw;6O)eCFBR8Nd-;Fa$oD8>rhMNih~i~(sC?Lt1N>)vT579mRF^jg%kiMo zMv0Ao&86D&MF0ulbR48A=%O{hP?Zz!m-AfHB9ITiIY__#Npb;`B76tgrlJPFuY;>S zFXLqN8z#IiGiF$8Y7l=sUXE(9Yu7EKvHy9TNUux~)TT8p?T!9U$9;7nQqbNKg#7Gt zp#ZST`-K6U?=JfgCSBbo)hp(lhZuynch8`h*)n;g0!`Ly}%j+nl zYxT+$bJtyPQm#QPRfoG1n=~nb`*rtc7$6vYYKyvhkNw6?TEN&Pq9MdvDxP*VQTiJ* z%7~6Mp<`(`3015el`NP?eB3P77Dl7ovs_Npubgz(KTB2?dBQm7)-z55y^r z>0+y-2l{RGZxB&HQ*R4!u&vl)sFy+^C%2pIup+&eTuM36&3w&-glKK_chUhQ&C6i^ zgTq=sp%vTQJmlec%KYLVXWlPvm4p2FUiFnT^*1>|A6w%{EiB8ykWwrdY$g@s z4?NF(7YP|S+>{p8dqC~DnqCnKX#e6|*4ECl(j&G-A+$o*CqA9$xuV{@yPj9VX-mVu zhsPqZ7i_9N6B5Q7A&^wt>z7tX2>x}M+iVPOoDhTeZq+$UMoQlR9>|!yMXcu-YiuTR z28RC4Q0mocee9(xwS<&Zfwi{gIlNjXTu06!rMC!PgJa!Pr^v|CQ*YVsZ!l+0gl``I zb!X#LVN>}1)85_s`jM|l;F$0`O!w%yrFCZ}YR%6>~h z6D?Z$CoRBKwn?4lBK8_r6TX(Q$Xq7W;q8&u6?G5`*tw@ARU47BO!xZbH>l0gX|=Vn zckZO+d2HJ1bXA!H!xO&ESZlIbW1^F)rqnJG;VDt)Hq_H_0y0kw_cwGl`MfxmoGO6c zR&ows{e^qpq7=VxdK%pi5&cqEG|pT0?esZx5S+A7q$3IVAyg?VNOhmU7W!7w-E=#j zM*&T;QI1w65#l3Z3g^J`^hPxj1jorXYNVASYw=)70XT@ZRC!p7WS(1~Cz= zZo0m*TXhicKui1)AQiqSyRUC?>@$s`Uw}HaXW`I)pF;neDRlom^8Ppv99iqkl{W?c zUQwr$KXUU+cI(D!Z*cyT>{a{a(CI$uN!RK6xwW4Hm-15f^gJ^@f} zaHvn)9s$iMpzXB$=zAWeU@u}Gn3+JxeVs7(XHwKe&{6j;A7*p2Ryy1*X+ydMvS`@J z7$Of&^*qY;G~Zt|v15Fzz};97Nv4zv9TUB@xvDq`m<~<_)7P7q6!b&-<(I@Qb~29? zr|($rw)}UR!AQ*K-7M+TN+4}tlf3RP9AF<)wW*Jye7MiC_0ZSKQ&qb`LC-8`lf=n~ zyydR^GTfyZk8;`$r&LJe#Bt>B*oKOp8`l|IJ@A~55TZJq(bgA&*|=^k>GPRf5z^6I znN@8)V*`Ku+Z9Q+Fs-0oI&^9+fW2K&Lx1gE^zZv9pbLl&#Nz_a;$k`B`zazykkxMj z(6>2yRdoBh$>jt0xtT5g=OMRC)L(R3i@YTn--W$N#boSkQS*c$d{BcJI{onySQE+d zayh|YT*`lsl6dKZm?YF(UM2I28~!U_+w^#rYsJg$@ZN-HlzFYF%5lOqCE*iP8zkru zg|O766n-snDN{_7dAHG|yw$%nWz_7`xg=(bg_|+a`0uz(osxR$vwV|J;q@**sqivn zlrO2!V08pvletqier4?BPD@y>$iJ)>QlWO+Txms(EW#35^{1SINOWKIwi(WUIBpNq zNJ~mPeNtT9@%Wx4prwZg;<-9?{UfT9KTzd~)AQO$eMQfTl!iUwmxGW-Sw0`+BXiVK z4Hx@}wu-=EZ;`RDZCs)jN%bzdgGEN-{GcyOcYjnZ=CzmPv1943(8|?-D*4~v@ncm@ z96cMi4UfaPJ5-Tz9Skj9b}8uB@^7LfM{fgLlv#9_mCdaCNvN%=YT2>e6*h(CE@4kG zb>|Y+IxW2i2&S;S)gG~FFUD;|Q8B&zj^s|AbAcX}KqBHe%SdErW|blSBEDg&#+pd= zZkShi*(Di~%s!N}J;$o$Z69N77Tw7G<`jcKkP?=a&6Cu;0%iBPLy#Hms}D%x2$ zl#^>)CSN*G^iA8c&iz_l(=bqIxXvwbg|no9foGxr8ERU*(K;ETM|l9F(XmvzN2xs!cDlB;;>%tJSQ1?%T9QY@`apZ zZ3B>s0_^oO+lK3y0+bnNm>4zXb3b76#0fheq^D*KL`7x+>Sabm@2tS^$#^x5j3*Zt zoWFj(l7<`EX)-U5B`MD~x7ehtJP3Hu-t3a`eG~8U=hOKYx2)6JIuGqVBW5>@(Hng0 z=KI}uk@w?az(dyp@0=ZoEib1Z94R z#{ZpsR%`TA_p;?QarV;4a4T1SMh}nsI1LLAAg7D&mWKx_h4}yM%-hAFBx(w2p=r8j z@^W8mu)7olCkmVUluVlG)7?wy8%27^mK&y>EK*Iw2M+naLtrk#Jfw`3=R^4z+ZdmB zHM0jlPXRv$KAUqW#gjuB57^zhRPWLhvUXJ;cjWl4vB4)-wLbc)^|8JoV73c8K`imu zYG{ozYe-1i0$1FW+v6IVzR`e%!M}$@hD;(^g$SYkl#a;p4D;eFKh15u z1b~`S)9u=RMA^(l`NbW(T%CJno(;o8i5zkr(7zbO@P+00fvtpM?zSX$^{Ko?sFygY zU$fP<3b#+ija!5#C*2Y~!MyUgiuI-?n#^vbh~zm`qrF^C({5W|nD1e5j(H37+rab+ znu-T37A`Ryg3g_H>s1?dT2(2Rci-9?6}ipSjDOXs%f$oP<@He;B|*G{Y(_-+s2?os z_gb|Xm!b}NjXty(V>>o(QxTk6^~%fp%W~NijfwEu^11UaRhqEeDR7{*yfpKNDPv6Z z>X>Ufyt(e=qW87=^S$$txn3LLOx?262-KWO35`$`&XU0?|CP{_tF-(cbeI=m`` zoyn7=pkG*N?r#2^wy7L&D5br{JE{-T#v|%>Z!&#v!D%~jH<9E&K+l-7*M?)~GJ~6^ z*-Nym;FgVrRc;d_l5iS=RNx`i);6V4jq2rD!&B> zpPbEN-P{3yJD&>!eMQiHUu*+yR@4?-+YMdXeIAYdv#8d61&sAQ1?M{2=n0kW-ty{E z8yFAONQuy~^R5?BvrJExMK8QOd6TACQV@-9o7)ZRsyS+R@c-mN@$8FX+SPA7snU$3 zaK67+95?&PV>dwA!(UzY6$h47l!mPdPiw6MwY_#OP!yL^-`$9RGwr9_<^h;r;);7B zhv%#iyJ@>c&Y#10+rTmr>l2~{t-pZ5Pg6~63aXCyh#hO5fBR? z*w&xztm==l)mNnPN&24&Fzz>+XwOyxUVkga)f;V9P2|xMOa(r9ch-MfeifMHM%{&I z>r!xp$kj&4+<5}lTl)WXJl}LHb!25uTU`J)vQ$n;wHxIIBQdR;TV14rEcLC2^qv3q zP`P{e3S3SYF?^8Ccj$bnr4A(D%VaYZ^J!`V1J3k6{>Vq*IRx#=2U3>aDjb!7eG>Z8DSbOiNCfB8X z7_ozhg`(80ZVMnHy@P^)N>d{pLX#SbG%0~ZlqLw+l^OvhAiaeSN|hQS5FnILga9Fg zngD6v!}jd=yx+V3`kkYHuvjk0v$$vGnrp6^dmdeWn{&*XkTJT;Al9oQY}gj5;yzcO ziJbr^de?_o7S-jqpVD#6eb2Il8dr=dmdy#awh`j7=2?s?%&EZ%6M=d&zTl!<3jIq3 zf+xP@yKKz_^tLS2t^k%C3|~Qy>q6ansy`b_)5+c3+e=0h7A7yujxRWA)xCz>0!l~c)-5@LQxLCDI{)n8Yn(|zVEg2d2iO}o z5*?#H7Glm=i~?MPHi6Mmy^3<3@#}gtbu${+r@51vryHg@tl7BSu%8B8aQFT$$7-J0 zS<>Mi6`+&TtV@Ag{Li%3>D65{k}?gI4!kc(o09^y#eJ~mW$*u~Bh`>I3f>%UXbQ9m zBiAvQ?=N!LH2&IB^DqOVICHiIdT0wv9L9`#!6}U9sDbkDtHADRFT6@!-^veN<)ja` zB?!9tYOtCYAZHWZo^rK&&OPCV2evWEmDwrPpBF2Ns;Y(xLt1uj9fs^LFZZATibsK< zw^?q_#QG16JUaL7(PLu03dgh?Xy>!A<9GA1me1_VmMyFmXfF>`uax}pjGiK@q8^;v z_EV||QQFuTjJjH=8Pfa0=4$Aiz96Tu(v>6Zf(9Q?U+*{&U3z7_^6Xdp$;?;h3nj%r zj>KpT?f>!g(`!?|`jLM8yi&bdN8I`gmZ6bRwx5GT(=nD$Z;un}`&SBS)C`@F(1sq| z9G0*oIcA+!0G$*(-etMt@zzb&Pa|yzrP63hR!#F$2epCq>%AyKmF~j%kyEB={mz6( z5{Xp1?-()m8WmqPM0DM7LCv*7pSeY)SpmF|ncCyWXnio&oZIY?ZT9Q#<-E_6OiF#eP&mApX}Fne;Xu zZH;q5;~V8aMT+cM)A}>YrKtqj_yA7X+dLwr%csU%0Oq&Kybbtjl9?<5w}*j!`Y11qX##JFS9J6)9lz65#$#!_}AO6Rc8CNZLJpsx5HghMw-*^VDVj zV3CS?Nrha-0Y%2HMmKgZ?J!{(-c=U0l!-KPfB(``@^PwdQ`xszZ$py z`N%3w|E*|J0ocr`DPCjr_VeCyb5p+=-ybp54g}JqwBhKQW|c|a>r71>6N58M)qe4- z(|^tW{nzC{ss=Ewp&GX2`13EnIc+#OE2`!mFQm~tPNKY*5>U<}RP#Q8|Js>R5jXq7 zn$d_BC=F{y+GhW(mQkED|G5>IxKQz4qYnA~GgnbHJY zLZ!U2+=jnviz#(uWn}-vRz!I6ko>t157IO}(muO+Gk-+b_+2_#lHb1iYd&MMK+jB` zrX*eK!I~lqma+P{y&LSj9bm1nHOI}?i0^`U0Dicf%9GL>SN$9xO}02po%z>d$%>&H zQwi9I_OjC;uBU$M>Qdv*OO-zR!Yht_k1HK8ll9G@ZwX>tWc|isRu!$T_IC?e#us2u z6<>M9s+h2qK=yRkzpc3>Q|Y`j?mpyoAFSY*UV!y2P`_CX27`?_ZUkt33L&7@2rdpVK}dvg$h;}SE|6_wzBf~HgG+2qvFxB=X;&! z%;fZXHmXO46w1INN7lzZ8aCVVsEL0J)JeJ6uEX=i>3J@D{GSexkanUYoVT%V@(U?` zJXpi0qJ=pgSD=b2BP7dDemyb7FBxJd{OY9_nV{qQeeZhGGnYhHdTVy<3erj|iYIOp zr5z6MidFx$Z|w{3*9Qhwk?-Np|GKWBbndW$Wd3j?%{9#J_(|ZU3Z=W(0MXx-z21IA z@|!LpS)c^OHCLqU15tV1cbEwzh#^*GPKS9QiIU)l?8PNC`?C?H_s=TOy}9^55` zF&n4L8J98Uvzb6f`I?Y`#_&N%wWmNEr$`z8=y=uvXNd z>?ul{FvvJ`>>{(nuQGvN#2ai@@l4~_We%)6_qOPXuOaPDwW@$#RUoPIYdiLi%_mzK z<}*Di&Lq)X^m_KRjd6G;aB(fL;bDtzjbiVMHs*FGJNirGcE(+zB>%Sjtd{=AJ{`r0 zMmz7F&@i!CS7WHdhMn@S4Widd>R&0p*Y~7MyidT*InVt`LJNN=tF%!ckz=}~;hl=0 zq40icE7$cujP1_dkOq7X)9u+Zyse?AEn?;`QqX2v&e9zl$`jS>)umRS-iEYzW@+LN zMWqO@hs+%o(bw0{yC`$5L9Yvg!Pw9GDyKakU97Q`(nUh)ri2nqKw!47+gle|s;3;S zCn(U>xuK%aGh3Hlu}Mh@@p$H_PW@6LGrXi?4ylV@_P&6K-%&|J@b`2*b!tGq-pnPx zhQzcm9)3(OC-a;JdsT%rq3Fgj#7)0WZgF459qF|&EN#1O;SFmx@_En0wHRV=(Z^54 zNosN@T_iNE-2eP?WY44k)^AMUwB>K^fU2(k8wUN195=B)9HYPwB zyl1|;9s1I{zi~U#hemlBS>gY3nKABi1lOwC30Zc@QrTMN7M1u@DHK-?yg5T+2n-Uf za%-CiYxeJ1p#OA)lA^DOU${f-NHlqW$dpJw&rL3Yvp#@pT^u@Kn~3%=od*@4NNY9V z;d6Joo|Nq_Xg`CI@LdvJ_^V2JsjoPjOvNiXlIu$z6~^r=*(F&}=EJTNrM5JJG_4oY zTBEHVY8B1+Ml~TQlvOztZQXR7S_Jhjmn&+Npsp2vw_YFmqm#}JhmW-he&@7WpuMtj zqV*s>Q~#jMcb77{FD57+A)y=KRp-T=n-hrEE4fxKB)E;LNo8CC>K#_bc9{XJji4ZH zy^XS|*~p_fr&>koNUKybu({R^ijQxNKn7nShhW-nOAaMRR{@uf?&#kvB6(n1Vea&s)l=sXR$ zf~g6qyO;ZbL?Y?t?F{Fb$39d~bL4+kgq2UZ8|WgQQt?pIm+V|j^#N}B>YSQS&IDji zWoNu|T^+Rc!{8{SXTcy0#$SMNb}uza=dPd$93K3#_C0BI?QA6|!bf66Qx0TE!9Ks7 zVDyCoa#?X_NT77rs{6%QU6g_6q31Hj_p(l>y*_Qly=@yi`-aPfwC&N-z@)iv8VmY_ zfZ<{%e3M*9G9Mz$dQuFYn`aV@FhbVwv>hogZh}0G93r?Qsg@b~A+}sXe!^ z7ir&B`ZbyJ6Pxqy+B-o#$b``;;6>A6n!6kt5?^}tfwLxMDhp-qQdPhAJd{Y~R(wuC zos6TzAt0BVK4ne#rZ5oviI%x*g3*V@Cw4Y((H6(ox!Ntd_TN-uAtZLJ_=cn@qM=Sr z4KU*pB`E&@Q)2rSG6BxFv5}8Owm@R*6zLDTd>W z3UqviMgqDL6+$i*f9RAlNsG-(}eGP;^$5?7Wh@c+7Acgz)_J>CbR<3-LbdSx9RT85X6cvFG|ucMOWVcFeO zgJ*u{)~esqp4X)|EXkc`^Z0|=e|fE`ioO)viCYAwPtR8JeM?1XOEA8c&C{{xh}(z{ zJn_{@N<$I|q6~g4Lh>%hiR66xHBCr#Cu@JGvgRZ7bn)|s~VZhcNw&G9=b zd&K_cUfCPXAFiI-JJ))koBhD3lD%5j73X#ZN*q_vdz;bWxO%%{TaSrW1yyYkZtVo; ztM>N@sRQ>Ukgsl=;MSo7tm#J;pK44vBy$@B(SC)NHB-SFRMYrG^bT~$>>vrFzwiV! zI+0pT)%1qhS4Y?Upd7Rny&P1O*zl<}`IX;6JaanSp0=2~`ef+G9&%6!aBE0=(4;+G zv~&1igQiDx$OT86i_6zLVr8H+GZ51DXx>b^4AF?EBBaS7YUAqa}$F~9DDX1yT-F6_s9UR2Ev@a9&LMm^2&{mB`u%PLBJAU z?%`+5A5F2Gytb-is&3@1wQJv;#5S^_bKVH$SEY%}s)uSTZi0DBBxA^>AcWGv(8ZMx z493$epQCA1=^b(e6I#+|Wh)M8+!$$EZM3b+#ez|RZp}GG)ySc?3k2pz*3yIkblk9l zv&Yx4h#JV9FpT8pPdG>eR;bxJ1&*VxlKq)Yo0^=Ly0=&LbmKTq^d8u<_3|vf_^4Zo zC&W<6YwRIV!cmN0lw2wH+3|0=#0bW zZP!9M!Sh%{lSc$ev*=zPCWx)L?_K_-GB|w%8;su8))Xp{5|RjQq;J;|lj4ER&0;jz zb0v=rYaMacma-uV8L*7D%Ge~Z)V=7&2*WVl_wN1hJRX)rxnw(eQOobj2DJ*^QdIWh zy1otQn`UcVV~`p!OY#AA$`&8_j)%#Vg?=GzCfd{lUc!D-I`m&g-2XP~o_+5=v#mBd zZJU4K0fM}^ZvVtuB&=5Hm6%e;``!3cTfQhQq|>AO1Eipq5r);hknQ2>pfbVF4BPfr z>3yp9bjE@x)Qg+Efz9Sn#B%iJzS+x@?Os$+{C+t%LHMBrMFV+2hLRw~J-ns$HipB% zu3G1FL#Xci4B$2S$`%l@z}8_z>t68TrTOv(Q?rlPEUNnkW4s#4yGq=i(I?Fu(gR`1?F5|9+88 zJ?ox^DQgp%?BUYY)H)BFTAp}5(r!SFW5m7y|4b-Y&qeJ<$Fgk!zX(7}jL#j>U;x!w$0-(fqv+ZMBZj zSzwlBByGC8WnLPR|+MY{x>;itLv zWbT;#^V)?`dVh5LlBkG)vl~X=so~B`BcXzdes(}wzjIK1EDeQcS=Nzh1n(kL zrP}Jn<5Nn0H(j!qUh2Qjrm5l56c~!AH*Bt{(D9n;P8cDVOaFv@ysT)#^}oz$Cx7S? zogld9>)^@_i@pEKq<^K;|M1IC&SR{Zw7;VGlI7%E#F9n%3nR?y>H`<)@2v!vo(Y?9 z`VtQI8V@62E;$d3TX_j5O+zOrK;OwH!1Q&Mi~{P9IEm@AglHLxptM#<&ClBO5!XZ; zD~pyvxbM6kT+IPUVd6o{$v9tH3V%g+@K_+)P_tIcD)oh32E>}!$m%4}@}Y9D?}jDX zI&4LorUv(a5WWyLD+9~T9c5r@QoSmg8_maQO(jj_^y$pl=emkIW3Z|8iDATM_Jitr za$XpcjQB_ zyeK138=v6~3r+KM&52Rn+^@d;l%?Tdt9Ey5eT7h|80|Xck;0B}0nXx%8BS!uXmpkN zoGYQ7RhW#P}9sh-zb_e!M!|V8d(V_p)e!qUL7`H!$RY1Aey#sKl z@cn!-f*{Mu(JOgf>RvXV8Q)|D2=d6OXJRyOys%7b$L*0ZNnT37-P)or~11D2P;Ks=g1L@ z-GUhBBm6W1MFq+d)#U^ry=2&=S$AeaOcN zRcKMek!ZNw2}0kPbWyNDLA`_RD%mU}<x;KA;)7}=vK_jDURm4vTPOgPZvEDbWYFaS*yGG}P1ydnju@bO_pH+AK6JjDt?%2j{+F(u5CD_CRZC}7~F0izroCpzl!q!#EC;xTwe=-13u8TsWe-L z7T#gPVZ@epm?%M_dPzt!kw0HQc>rfk#u&DF=i1n3Dh(4wtz4en%<{HVg8-;(7jv#B z5cC}q2x*#Fsj?h}DGMmzA#KWFPVeU-ysCBtn6mA*7X9(~L4d*#7v~cG zfKhICYOKu!*;kJS-XATR2o2~f$kDNGeu`g7(&$&^3#3H~LP&4V?-Q5-YcBgB)RdfA zTMz55WfkVG%;G074skC}-=&iVsXRxmy}a@dU9YE;poEKBmp!XJNEQ84jma%zmbS|B zUZO#ERiE4{v2R9c>7x%kaKm)><)H*!ZG*QSqXXHPb+<+-fqQK3Z^|l z56a%ygl(^f zJ#ssJ<0sE7Ep9ZW03`$7L(0+=DM1U};_c#1F#EZ|osMx;e79xIY(_#oT{9n=FZ0se zm*f+3CEXHK+&;H3*Z7C;+S zBx2yMlXpny_$vmrxW&y^;5Q_+f8%IA8vw5olV_xLfXdQN`EV$)JEES2k9ybt=AHkQ zSR1i718*)ThT(S$@domdw?d5mcpi2r-Y><&&r#aTu56fWe&Z`t8$r29nwVg z!kmq#l-nC7Pl#;Q7*lZ%pFg(rzb#sw)ta0)12^JxRQ?-0i9Zy}&2Y7d`YQ5{Eet*k zP}{o6P#{EApdClZRBn}vOXK)D6uxmZ<>a?p*Sj20fpEV*SYOhWxi@1a9A_J5=R%+p zlm{3BKA81*0s65IRVist4mJX1G#75wI#k)J%6oA?`dk1#edBaoGJWDB5#AJZofs!_YY3l*_ZvQD?eP4DqG|z+xg-vc*}UCZ$ZlW`E54QiP9bLV%!+`q6BV4D6wJ^!vZS@jG6rFmM{RxjwJ79tI& za${x6u*qx3qTR*Lx>BC$>5)z*_DZ!NSMUh+#t|O3*xIlu^G=P8=9g8`=-{z-FwQ;T zZEIx1?dsJ$@`8Ni=V>pFa7O3J#e%bmI(NU2(~L*WjN?aJUmA>cMb3!SqJB(lc5m&e zde73Q+hwF@UEaJ>P+st63n4TcELjQQJ#1zICMdB2UXW!fN_mt}bdH9$GMLt8X7-|? z_Uq661H%IPMC$Kb-tcZfPXpPMG?aed`*U3rN9Y7l3Zw~KT9!SXR>mG*1obE&xf!;A zJ86xo5AZ6gcUQJBYANbiX|w6=GrF5rEZ8R&ItVmA?4p?UDfiw7?w?7V(R{LfDP79R*{1`nL7tL zPrgmT8_Wfww2TAZ&Yqb^e!$n%tQ{Ky`Rmi7zN{Uq%geniA?E+KBbhO!ctHD-n}(Ix zlx8EI3Q$Q}gRC6+PQR!|8OF=XZI!7=#^pRUzHfpVUs4* zok>osrrKkH&>r>r{ms4@NPUo51>?sIaT*L85i+|?H@+?%*;=^y$W0E?7+}jvweN88 zyoIwu+-v+3#OQ&OD*d!0_Xe?6ewgN^ovF$FI3)!y^ahiIOE?D!OJ?4MWdIewLK%=q zFdJJRS|(L8?>_<$xfNciq-simV>Z|IK$I!bR+=|91#!nkYV5RkeAa>(l!Oc+R3ywk zEETF$t^H$*7=|8=9PF1j6M$kOBR9JG_yS>^s_b$6k1|UwgH&6?Z`fL_tX%9gdu8 z3NNaj`xrnUA@!iLQ5zj13u)!G4GOLc!91(WH3b-;lB+hit|r3PyQ>&fxFC3g*8S+I zi^nb8XzY=%jBi5+Ng0LH?1?}Y(uLA%TB+7IzbnE}Me}x}woHG&t!~L^()3L%jMe}X zL`?a8EFRZeb}h+%0IULWzz)m5+9l*>S39?hr{bO6lu9$41a+u|qZURTi(o7Sn$B5rj&x0m zyH5SvAnR)}aF4*@(ZS?HQ;m@N z-9nY}wD(!Cp*C@A9mus|B2$`(#}1CHRegqnnzYG}R3?El`TgbC4N;A8i1zv3vQMox z8n>n@8-wqbndI&>X?gVMsw3d_@Z&BY#O`c2f(PmzlvHP4Q0)5lFY-knPCcc$x~3o@ zmEdh9owX_y`6rvH+yO}+sy zM4mZL>wbCv+swP>Jsj5JXKNb^e()%pgqD9mXP=FZ6)t>dNbWU+NRZyen^0OdP2vRh zf+d~v_x@HziqGir7QV^C_dF1UlQqY;h@#Jj!`k0p2zhf!k(h%>ppo44hXGpe>3N%n z_lo7>!K|FYq;Cctl_o;Vdn*-3aW#>gUhzPEUXK?k6}2_smzV09Jt+l|Tuj{-qHkx3 z(N+?5=lq^!qD|~bfc|^0lqwn;)+e$k-fL3wO|!ASL*O+h^?4jP==eng=N|BS?iL1} zGGsPlQz_5m6a86vW4D4KFc;W9K5;e1n>z(g_}tU5(9cE!IfA+)hi1%!{d-ApLr+`LzhRqa=I%Xz85@_GKKmzFZht;2z z>YpdlOU|DrHgQ{jDCTC;Vm6VtX}!-TZeSV_BOp&Hyp^NL7l$ir3QFl*(rptZ(*!0y zeTXNs*he8Ot!~>dtmOGdJ+;HEoVRWX5Gl=m)Nz89=Jq7J*f=E>Puf30PHQz05w=hE3Ps_2qBEuFL#DR_U6(b(MRUD@Ptq zJ>3ngcBPI!0GD+%t8!Sijsudxqki0NCa0rWj|NeI9>nFJ=UwXF z;hepp?oZXj`swv}M$dKHb(flXWr=H4J+!-Z`LLt($$M&ret!x+ao({AuY1=0M%_>U zhk~N1`2`3-viXnZEHDP4-KOS~43+}GpJZREK7@cG0X-pp9Hk82k_tI!L&)Mz;1T`= zy5VI7Y692g;nK?_-SpFL^;e>iN8?A@MNii+HeM_{+_0Q&NgJt`j|@MjmpYFqS0y8q zQK#x(H1IQ(b?L@<1bingBXjOf=eH1zTFVotMs@oXffHw9YD+T41YYa&b1~LBd}v){ z?)Neb9c4XqPT`j^?9kg$)LaC~BlnQ5Rq_eczMf7UiM$YA$xAQ??%&qS7wyJ<3 z=JtHvR0VYdH~dq`_C)KIhY2&An#KcWZ==aB`mP8Ly%s$O zS+vN*v>9(M$*mKL3ipSt(#>6fYVyif1$;OO#iFJ>J#QOmV-v}kd(H3Sl{|368F)tx z>}<=R$kJ`}XE|bVB)t}(-NqR&*VU%+{E&oLpJM7G| znWpgO=zzJsl>m~OCwZvu6L)ONZYlViP;a+K%nvZOCDG$2Z|o*Eb2s+a7G-AR<0n-M zvvUT$)vIBjCH*i2D1ou}Q6}7Xf_m3=F<0=*l8NE~4x1etZ0lXfN#6!K#TB5L^_pn* z$i|H=6@U_NALKxDIW6TO+eo#93kI6+Ye-&(!+@6M-@1U%NG*k{r!s%$5wHM^JxelE&`6@YVZSn(Sra7x*Irl@R7EBmuL}X+%VTasoMVRCF z>J9i$L7aple#0Shr)RMKo(1?{7U?zy;D}CF68wwEg`eKfBnCz=sk&U`SX8Ud>V#3T zQBT@!5)U3RG#J^XlFa+4PqPvm)PB`3r0UUErk_s?p72P@AF)~{mFZ`F?on20UA&Q` z{>bz5F7~Gn+&vB9;*XN0H20P>wj6LG%SI{F4xA-DJS?jiqAwR!27 z)#`9)Kn!!x%qB&t!!@K6Mr?o)g;U^cS8H4Y%3nR(9a)eKFca_UG6=sm?j3YikdhSX z#;|cRq)TCm?brW$Uf!KQG-0NTC!S@$6|W5GwR-*nEInkM6KG)^K@Hxld!N7CcmH>Z z|DS$mw*YhhAtAoqu+>q`$^o(;R=&)Dt``xajP$8W*}iJO4)a)g*0@a`Uw z-Rxm&fgXS$wl77expf&Bj!@OH*U0nH}AuJ!Wk(=MgXHd&_uq0BQ!s0BGaW0{$+oOS|FmKh38|4$Y0M>&2Kz!;Ja1s`cQB~6vT(T-3_fsQys09LlEwD%j<`| zCve>Cby?0MZI_Dt{mK7jg%AtOhL}C)Yd`xp&YwRdL`*3Mq|WaL+RDzShItz8PA1`N z3os}J6YG@256%AP$>ET*cP{ZMfwqhZ$M)4-NQ_JQpHKO9_WxFg|DV3gzq%_WFNI6n z{OeKXN$g2tWsUHMEgFGxP9RWMAXkc@#1ch#kdkRLXo?C&c69$c!rnc*CdYS^?O~D{ zPj>ik0A7IQdF+PXs9tl+-{zF(IFjH((%wzPX-VdMUHYbPETEp%ev8Jxm2zLhz{opJue;k=xjLs@loYJs_tM?dSF^Vk_-k*FQ` z_oWwlpIw|Jzi!kIDcV@d(ndGy)#X_RW!GdkB4Oz;@(?Ak2Tz!7062bV?nBxbm2Dl5A=Iyx$-s0dInY#*Ji z=ivp%;z_}Bh9%`SH7a=p1zfbw&dy_@{d^33M+*aw{)xU$nAPLRg8T?`=IEWp!%-LS zx%O~Mlj+d*df>%Vcv%9c$0Q zlzV%7R#$E<2V!DoW@Tt-c&Y}jl6Djtzq-0Q1}mfm|1_q}3k8JPYMYpt1ie4ZY7O%h zivIi0{eN=ZfBf**a<)~?iG6>^%%lLvrwhoXhM5^kq@x%xtYfi~lp|T5o6(dGw7|Va zQ_BHGlM}{$CFZZkf4zM?rfoa4mxC_$B0DE}+I8>Vy;wY6l){ni7Iy$U7dxd>R5Y1B z?Z%N^fkMqiO?zOj+5wAYGitP`ltzf8#)fLow`e>xo6VkS%@N7UY=5sp{u~lGeSj^_ zV%>m94b*|^{7Ku{X`;y~DYD!sB zLEyEoU$@DttW;bI&=loZuc)jXO_ql=!GeO6_z&N|eCN29sNZ;(_RA1BFC#BE_b^72 z#LK%8Fdw)c>>Qac2LDZCf_Cz|rBJ-_Tls&X$JZ~%PN5Af2oJG<-X(Ynjl1UFse7@^ z)XVA2{QFCazz`sJRzda{&b>?0;x0-Y+k5sGE}FZB(}ykOn8@#lw(s9#(lp^3iah(C zNA+d;btZNMuNbxmjTx`XMN-w6$ofog5oX+hEjljpY>0{xHCBW`r5qaT>wB@?hvyOU zaCJQdO*|2@?_nXWmzR~|mhO`o=_h1!>Gp}mceERW5XcAdx=D1 zS(ytu3Ntct`bnsOr>7@&c=*(l5Rt)kg36FkfYg2l&&m6r?$KQ7#^iT;7lw*#y<@q> zDahchJ92ll$rTfZq)nrrjibzjkgxaJ-CZX**xcvC?$upQGKe~U@sh>->6O6W<3FnGP)5znFQv%2mjyy~v{Mf})%#cTMZzE#6ps&x2%EtwT|Xc@ZSFcsi6^ zP_a{1<-hPMO)XgFIj}%sq$YsqLBdto4xf;btJv>)JpQlR;iYqN{-n%caMRx|$KN)d!uO>QW-Nh88G` z{ub8D7XJ3qHM;kC4f(m}f5-p-fUM5p8_n*R54f$Zs`!M2sElIV>P)+zcDtAd_Qwwg z0O-wb(C7k~;>q-z&cL!b< zbNG5kLc-C}Vtei3N&tr0LjWKxi(>gV&eL#sythkl>fxb&FnTC@0^>P~8Ub$aHiK8M z_gv4atyQI4XtXa!;nJrExufct z^^l<}qh&6Mq>1{`R~?_k*m9hj4LmmE;GY8yXrON^SKr)_2B;UT?6;U9er zp3?)vH35*VH0JVDS5mr52Gv=E%trVE6F_UXAOhJU^bc(xs9(!kSv)ELlXkST z`wDoKz%(2u;hcSEbgn0*AS8$g%ebszpKz;&dHMVv;CqKX3^^aahHykl_ zo{#JEf>0vOO_b}<4*ti2zml#WHA87^*VTqg9HYfF%x!FhDara9Dzf9oL;W#PY+*K- zzEOkg+6Ar65y{boQ!rfZQ)V_Ii%KOf2>9t74(M@ZGj6A;(}NfA0vL#H8e=?0sG-#p$cTx(S`vIodV4<||-+w%(xuk2fwLwSsxm=4AC zr7V9Cjas+3;TRPANh~V6K7m@Cd=|2k#?3vjAh3N#O_(?(Ps;MU`_M>Qfy>4!Lz!#; zG*2|>qv85cU$UG#1kxO){v28FEi%v_b}#q-shVh5QT?*84QNAP+efjdo$%R@S7;-n zx;he;P&hjCtO2dKO2}OgCEFD8M^HU+*Y9cw0KPG^9`t|(${0<#2zfO}aaB*1D3f?yL3KvdL2wFskgTYtCbXPK3>r2%s^MbO>woE-3f(e9xzXUj7d*U_U< z^N53NR+g4aQG#B@g5GtQN92h#PK?-9v+nHTRBoNcsHmu>J8Fhgid>PCJguN>=xD$( z%_58lt_e5^6=cTuG;B6eoFj)3$;t1l?W4q9Cf1w%(H?NMVPbyIaRIfkZb{Q(H^=@mU|+-~XEPYw)K+0Kr-&dES6t^hTczWiS{DSFawUs7%cG zBu5|I9}s3UR8;gZ4LkL%5rtA%&sbN-A?uf?e3TeV($`7qKeM61cE%+dkraR7)_v-i z<{O6^fbt~2GA~b}GeZoADj(ss1QO+3f~i*k*L*367{dzZ1Ld#?X-Jv4LoWZaqA8(K!8xeI1a| zqw3ScHJcq~;3CWvF(z^zpCGL29M5u#4{2*{VX3{!sb9`zn|yZiVpEyP5=Q%t<>)&} ztERr4avi)N{ECmJ;%}?i-FK&c1rXwp-KpRIU&;uV*cThm0%(ifL_-)&i|tN{Tk|-jyCsM{{_Q% z|8(|8ID+0Lwh%ZAo&wOZqU`JoOT0(w0Q@+`aYT0}sOfnWDf6ykPw;q6z*J3*v)rZO z-QvbaAQ=%oU2ZmfXG$~jK-q}L3U(XUHB8P3%1l_zsNO2;!vU#a9-Um28aiJiGS;7* zHS{J0TXZL5F4r$?9>!$47C~zw3r8@R<4<6gjdQbp{S`-rRd}Z1v9rNj#dA=|dCV)r)N{HEXWMM90&apq+u$rb< zv@j$wC)+&4{|`p_KfL|_DOmDM04>!$tTdnV|5#(sK413FEi4|;XlYwPZBHR>$BZ_c{y1Vw1 zL+6uI6unWBR;Y+WA8n1H`xki{`}=S}rT*m$hxR(?v2li$@}jk9O(G1rZERrhb2^G+ z-K}4?Pd>AKyQBe@MkWq|8!XG7JhLo1c#>CTV2lR^dT|$$sLQc=IlvQ<2y7ozwehA= zNK=d#*ZsppSRl5Qt<@PbHQ7X#m~PG#8Dp@k65=3*GJ|*SNCWrNAZsfKO00!A6M5!6 zT@o%*Q>okVx?QjW52X9$&??3EVytoTj!s;OdK=ksReZ-M0wv2M? z&pw6$c^J`L$Bct8L_O_bSOTjMfR!68IUeuzm^6x{q~w`n$g2p40L({3rZ-3acAqpX zb?y_9vat~%t)v80@i>V)W>!`Zd3kyJ#z!RFvEhC&=DKa-T<%I5`(q&TxH6Qug?9w} z^RXEa@nXeYjWM~B-8Xj8Z604xP+(tlbNt)WgNA$RIQg$cZ0__0sDY8tc>#@$N z>MvS=j?RR0%!y4R<=xf^8IBc?xODc)b91!l_?~twd6H=VkP@OV#2ik+SnE%n#IEcy)ep9b`V(nEr)jdB?Dx}O&d8QcjO`eYa2{a~nCyhN=fMaaCzGAtfqeFuIDS+!~0`c+j_RBK;VCiqJbSZ%o z1FNw=-CRjEiBH0D5DgLOfn**gpwnE6tX%Ki5E-A z381O+#mk5@E4}d=qm6J);QkydzLWj`=F|VI`F0z)d0_TMi0}__{cG&;OXq>wA^qSa zk3VRi;mc~*BRzZkJubXHTvGq#%a>!1L48KYtAI}YRi0x5(j3`hnc4Mv13VM!!yhw) zIZDMu2z##e;I%_zXT-orhG-dO-xLMbGvG;i-n?6z+oS zkb%B+(EYyX=_uUBk@B{a7mbDid8HW)e_a5ms0xcCL2`GgW?Of(rEkHH;vhXtp>j0$ zvW}*?Nb&%fxGSif3TX>z(D1h{046TqJY|1f`XVbmFDf%uHE_AS9!kZft3#jYFcn4# zI*>KOiFnYW%Q$GNQP8XEOr0X;+Fa~*rd{U$a{2!zW-gZkLWjVJfw0MuA zJkLxcgWbMcy;OGCU~2t63xG(O4iLFC0Dh^iLzGkSo)Aip50(2Fusl}%u&^o6@-=rW zi>@|qyhnWmGS669v6CClA$%n6Tqx5M4UkxI4ty=C5n&V0ke5pYLTqPH$W2M>Z?@%^7S} zwjZc4>qbW}7n!FYWaB0DlgrH>qMHFRg^8R511NZ0QwPX8BmHXrwp=wviNICdpCrWhc<)1;`I9|MXv6&Hebh{y>P{5M*mpDn? zBxFLw=|eyxfRYDb$3Zf=9{cn9?MP9l%i1IWzC@bfG%A@J9l3%(8~W1c)<2HsKf$nY zUMWCDZPnD``@7Hk?>LCuH{HhC&DYbxI024M5MS7BfORhPjS2>E($U5QY`{|6}dD~w}zdR1aXr36TsTkTWu zV1+k3-23wCoexHA#!8g^h=1rDMpltB zGI@0c%a*SbuVr>6fg5iew$sML8$lXfoNa#!3CUoe7eCdhdK%sYKPm0ZX z)wxMnJ*7y&i1O|EalWreiS3u^Kc7K?c3#^P`tWHO#b(Y56@xDrC~+|0@KWep7pEJv z*}&tC^ZQBS{H;#wv#n(VNzeSkLM3Qz(w?%-Q&XXV$DF?{b_w0-hU6-og z`t(E&u)16ncBHHuC^{>rlJH@8n}q98JWR@0VZnza#M|fc%MrPv<^B0#vjS(*Fg8_w zFl@18FohLz_q`K15xy~G68*03zyb7Xy{wx&pe@h{On+ol%{!sO71JlS;q1Z*g)#)l zFmHH%*1V_)Bw;c@p-+B1>OD}GoW74qKM>ccT;m&#;MM_7IeP3kfs!lp5*b}Wu5P~F zuBu$Jl$Dp9*Z_)Cd+_w_NneZq$~pqz`T?0>0%BG7|3mlR|5cO04hWE*o}SfksZ%Mp ztQ)9>o_Wm7%$UJ|s`Yz$ROHPw$IRyL)2^}}=;;{WG2|W|9Q?q^E@CLg3+$weR<1>X z;y?EK>`uVEK9DPay6o0#=0FO37PPBg`jMhNpjhl^u2!Wbw_klF$E%+? zn`Zi{rHIGKU=^ma&U$GhY0dsfc%h;ArD={dThqW#vHxz zi-!Pt6jV){K;R3sYqZVn?JuBTQffO$uEMAiAJ+12+{TNd6DBcVJ?M^b<$=z{!2{e= z%Fee}IzlUm3SK5~lOnxG%RT8jX7LKXGIE!BL9k33^>%m*DOZ_z#XOqmRLa&fQ&k~M zC!I2PiE|JsN9Cl5r6k(9ySs}J&P&NN4=%)degCiAqP(jhJucoIF<{u2C+_2R%VQ`f zr}M?j^*wSs>`YrKlX9`CH)11KU-2@%0MgfO9d!+5GYKY@VOHJnkZD#O9rgBPuksLP zP+{)GXd;~_VYCpoOr1Fmb3jaVK?_0EZ)bU~Gnvl=-G4VtyAwvs0ssYt@fuoN2J;;! ze{%sS--b*|12KeF+S2UTbJ4_TpV3l&RXICrsZ*r6`TQW8RTyP{3|gL}Zl*kVr~Eg0Is{kZNX$;51;pk$QJ}d^1%Lx+v)t7F zSqr*H?xep3h1mzCkf;5XMg0T`Sj4s8%z*As&c+BT4S=8k&2m06l5{JQ(u&t? zUI3}UY)&4R2gC%OrGfl9)~2_iCDzx(+N>;l;Yafo_qp1CL`g8lAUC>F7$CnMOU(lN zc?q(69h21ikWWv-ehPSj0(OGEHx9e$r+)6uMJC!=*2VDe6atZ`?B5pp^(nL*@z5(y z(xEBh(Ip!N7wjNVko(&bWR)wmlfh)s;RM-BmmW4m@wPM7ljME^cTMD3?OwJ>Gau>N ztUiYo5)xA0X=@lPv~uQC$_5O8Q_Au16Gi65RyK;xqqe%hA)yQGQe<`}D#14X1DZT}DO*NRG-!I#&=Tagd1xN{&VF|KYX2TJ;}d^F9oRP&@CgC1Lv8 z&Xa%AJOl2DYTNEOQOKm!r2Da@$D?V*vNcjsoN4 zr2ZHo;4pQ@omZk7Y!rZ;zT2SskX_9UowbN~KYOG`^+hyV1+^E;*P%f_I|(#n?mM0_dDU1|9_a}~0@H00+i z{mfixLiOB-V-z9$aKO}mM_I_B0~JtzosMP1uP14UrUtq%?nz%JRyXypkXpD>rQAH$Lh*ckbNN zsUNRSrw$l{rYw~*Kz3{7$GD$fJkH$`tlm03xg{liplpoi1X;VxLN)L)1KEa~foNkL zuDVpDfCwk&+(;yYeC)&aT<4M2X1L?75Sa%i7p@)lJzr4zhbj{v?g*0gb&zp?*YX2} zDr0~gcmfNzjLZ2&oVvVAnw=m`+LuouiDgcgUgvTOkh~PTuTAWaGUO6}~q#!Y|;zbS;$^T+^)@+0laApJzC)&`ihql z9&)1Cm|1(l7x(_-*n`%4{j2V32zE~!d9RrD{?D!eHkwcFv(|r7DouESTSMHdP*hpJ z&J6`%6n8tr&wP@>#u%|7(4}7l1jH=&RJ@WKl%W)C?xRn{UsfTN5*-<<9M)%%5 ziy@2xa1*{yeWRn?WUO+hcraSr1x8B+6afw%o+B}vA7gouEAgM~IyLg;X%~w0T5`dZ zMPIH7FJ7iE&|BvEMQPf#qW+fwc!sgvc^CJbJ}l-o&F)YG&*8h`+L)eraWIURwthbm z%n(g@f)4H-d$IDy>ytI01w@0ieyO>B=bKi$3hcU6FqSoa9TW@-px>^XrFl`gep6i3 zy~_(QDo!O2(COuSh*rk*#Yzc)_B+cU6-<(O z&h}K7l}Tr{hoY<)aE)S6r5@*w%deB#!X0D7bNHR*b(jy@E+g2g3d2kZXk~9t&--_l zYYqA8FcU(dXa7rlNDK@O!Hf^@V*!A0J^^fI3_$FY=^w9H2s%9)%K2ARD6HP=TPEU; zCn8D(PM{kWpkV}?^J}Rdv2-L>3mrv`2J2k zpd{7Kdikm>)-~%PoMF-Cygf1rcUMnu_}jN@^p!Tb_d%OrCDD&vUdwr7CF$#907HEX zm=35E&`d+9ox^(M33JJAK-prM)L{W47j``sbQgalhRnX>r2wQdxKFfS1tfPpw6Dq; ztvOt5rzh6otb6KqyS^F9dPQr!vtzkvetF(S=8Ivui-6RI!WQ&XyH>llYI$`Cmz+-; zu6g|Qi(RB%*;PEhB!YPi=2m6Q`q2o=yj82g@*BK93N`E{m2A+XZ->7!1LQt>MK2m1 zNv}3?+k)?!yLN+hxS#5$+Ln8i;JW{kV9oMgWtXxZ+=q;SvI6J=qh;S(S!XyoIDXVm zumtF^_zWV+{{wzI&cwueJO2u0DLABX-9e0^~;l(*zMkLQ_hTf0JL2(<@jkGaZ7!N^GZ zq}kJ%psLGRUWJjkMp>`l9nsuu8gF*rTpBey5A^`g%w&P|7`Km4xp?`-YJrl>(Ku{E z;AL7>B<@sVTjvwMtWPhhRk@QlZz!kk5|Z}*sjxq~|8ML78@Q6h#U|OK4H1JnZ=@xU zohBJC7cNTBsSrOIfS+Q#wzTc~SH0TG^~kcB?#6Cp=?6Fy!3!+^73hNm>`sQg^Zzce zFG=MU(6G*QgO29hu7YT(F(jlg=kS3++rQ2AgZ%eDZ*n zhkSj;BimEhU-8i@XCMLQjK#HF{-C~9wJaZo^!8S~-7Z5gWR1T2dHGx88@Hv!2L|Jv zX564>Yb8>+(gNkoPO4V8Pb7YzG=^2@hF!89Luw)NFI#DbZU;$QTYwiC#e2(S549s4cVP~BMwYQ-w?TioQqd_%usG)c=SfxX)=;{{#ok%#7~c9v;t(A z*XLuy@&#H_PbKV%eb=(VScjC4@epRWfe`v+@++;KNui}oiMuq%eG?PjrKGCJ)yHEs zHU265{K{W6m8mL=W7-{TZL7H`nMa7$XEf0^IcuHT{+gJznm7}%At{)Ft*~4Bi&(jF zYj0q1V4!lD;zVq*ozg~Ie$*6G|H>tO<8n z)*f0RU5zWP*=0pT&OcOj{_!X)r&?hI_9F5KhL?lBww_#FjrhhwBxL1+L6zol>@}78 zdx*Aq*2#^kVn?ZtN_jkTQnMgHaw8bf8EYdw)V5dScc zr={DNuh`nY98 zSP;%GIy5&oxACM^qi>|=kwkIvWHnUp8(ZsF5*>kxZ)p?tf`(!Z~AJJja?vrtAjm5Jl-u>3rpHT&intb&tZ)xquqPFw8#QgU8 zEq*!`%DL`&o8jlrS!fPl{obrh0b3WwbkuKe+S`L6e_Be>(r496(p5nMT1%~KO)>9+ z%LL}ow_)-z^AIyhLoKsfF@1x&*t!ZyIr}DyQu$Sc~nRa@3Y4-p-Q%~G3~9Qxz3=>QU0i@e6-ZiFz?fE-==Ia3rcd@w@a*x zM0etEJy=-tlNpip%~En~d~{F59(^#@K=57DsMZOE@y15=_Xs*BATXycdwZT&MB8;J zT-!OA3T|1R)@YcrRoW5_ni3SM`~5pxvEwM*-lavgLDfM{!cm6+kC_TA4_!i2Rn3`q z$RU+O8P+0(x3;e3b!*RQCn+%{U^*4BY)y<8Ym$=0(2sMQU%|E)xatqM8aoOJG4}VC zC7(WR*2Zv5Fe|M}lKE&8O8@AO=6q$N%Spxj$IN(3%6`IQ#`}wApHg(;-^ChAE-MxK2qL`>C4w-vZU@vJ0<)J$Kq^S9o zI7u@O!MJfob;LdOQI>Fs{ znR2gpw0pEL3U36(Ry-OsZ7g|l@nU;%M6GAup)-#~*&Qby{yY;?<62`Hh=l|Mojt_< zS}`%Ge)3TGA<0_P7nP>wryGaPsuj2ybhX^L+)?Me$#Rg;Q!HGYpRDUy-+vJ$DZ3o8#7a&q3aE;caVco4!O4 zrBUFJ#z@&^8>iC~Id^Zxzft&*pphkiMw`kzB~$7 zv+*!kyDk4no63u!L?>@#>x&YW)eEU{)kCeZ{U*!E+S6mRvu5Bfh$)+`vHNESbNe(- z;9g48whCW3BWZKvp+tI2P9c=*3!#vXt6U4Jm?o3?A|oOq>a44_cEYpi*V^Bn4tAX? z>(zsbzwdhx;AgXFLRiCR#NSHsG`hKP-za~f>6XqHL{oSE%>@2)_CEGe z-JLV@z4_9-D)(;x=cw0t-<}s8D<~Czp+%ZBKgXmJe@kg=<0*0G9UG@w7REso>7X}f zCf$vH`9sfXbwP2ZT;%Lzh#C%p>QSB9=FsLv^1b01nlt}x6!VUq93$!A+H!8n+($0G zv{Eg@*>`0A3&*|pVJrJaIsYXG;J6h)9rf(Q6_@h@Y2y36BCnG=h~iKrHnVr7+3?+o zDXo@aguH@Nd)xUh&Gu-I#vFO`5x)sGSqI7^=Z=2Kv!4bJoO&5l*{ckZhlnwi8QlE3 z@-s7T1qK^CaL}CoBm6{e`eO-)nWp>4%1a2RS{91CxnyV$3bqI{WvLnCrrzY0c)c_( z%VI%g^u9Kg4W<`7oEsZ7nQnA*zpw=%E zQ$;(kknLPPKf)0-Cut)a%S&~qs{3)Z_?e4CnJ>-+i5&W5XRIbyBX;pd%bf$*ds5N@ zk8Y#+C;bG=cy{I-ddC`q@Q$;-Yz^1$^{16;xoXt0cKgHJH{7O6jD({EqPS;+=&pNN zo84(IDzLFO=~5W{Jf(w9deZX!lw$9AgP|`((v60~RP#`kZ67HA zlLGx`rxf5v@A6@>f5wcIcGTieqVqJrB4~ZP3NYv_Z*7vEx!p|aLm4cjD=&Yk8ku*Q_6M-!w4%+^Q(x*@Sgx`4i)SIw7TK8i4+FKWoDm5_V>#zk48!nN6`0h?u*TnS9yB~$ zHXv)&ZbeNu~}DF-b8m+!{OZF|ARpJAsl+1agsT@7C|nGz|n zv+M*jJL*Js$iu`(-EER$9iKP!_7 zLG>?M7K$)h83o?qRsSMMBc8tY44o7EKSJynh|7p;QeE_WY-ValYkZ~nA`o<)LKPkRmFEyh1~T!w0+fOJE~CpdW*$xt55HchI-4M zc%(~z7WITp6SDpEZ}Ap18X~1R^2P?{Udi5Bu*l{~+N9 zU-hwmx2(hP@JOG>q&+-mwbc;ikW!%`e&Oe9*&{NY7DL1FQv<9591W((JKJ1;=IsCH z|Ct`3l_9;eHY%Uz8`6c3RVZ^0e)hiNyD3ZT(yn3D^}1MoF$~RYj2%9p zy=|cGdNiZnL`5#5q|`+E-je6~Q3_dz4`*Y)Gw zKdQtph;W_MYPwqTA-3i+wps1?3(3}Zd3Q2P zEM<%D!KRC)Hb+u#_4r=W%&#iG<}M+T|HP}7+@eNNE0?(p9b%50JuwsU{Ds}=-Y2d3 zV^L2=isP;(RdEyCGz8?bdz-tyN$q19 z^3tC-_Wme zSK9wD)R7(&6fHdoMy-e?*a)N-{^3$__o=E8!*cuk4@*!#Jtqh3<@?>7{D`_0mr$@c zY%=_dQlxLiMec_7*r{?Jx&%v&lB(=sguyXB{{;Vk-<9SNitI`7!Egx}Mjc{GkqLag zAC&{zfCITo{@fc_kN`<)*p}6GY=?cZRdRP~6Mn3JDL)*q>m?F+{HQg5hrE!jRSS!|&X(Juo zrB|&p(+9a{6Z+3tUtG0i6N?#sLbjZ(Of|1?ShlsOSaaBTpcy>8;d=b$(i2Z6tRI(4 zp{+|lD(Q?8rQB?WWA91+pP}@J3z(|WuJLYXQJy&VX0={UvQrRzx)@y&8@@Ni|KWOp zr$LgL*r1iz+cXW)oT#l+!@ohqJ^;OprIAD4?G6?FMn`B*b>O3PXAjPoJ;;8Q&d%KY zs4n}^m>uQ%rQy%r5}N8!Hst0H+obpG;{8`HiFS*K+K5%@*J6roEoMc1X2so$+xUEP zhw&L14kDutZH8|QDq3nv%mUL5!V?cUNo`tT9@mVaZC|N>n>7z7-|X+jyIofBb~Hzo*&;T)@WemMfNKkr%wR`(7XJmvA6YKa_ghXBs_yc@to51 z#>~7i=w;23{f&Awgpw^S^0}z^Fy+ndVMs0Y?<&p%46Ln5|85-bB3WK5O*yG~=w!4+ zE!?3=;3(uv>p`~GeM|lVB95KfaY}w9z`p0?2M$t`4pWAujSCQ-B%X>#;{%c}lb>_) z<&90C7PsyEoSXxS&7N^~DmDGFXKC-{?T?B*pS}BLFP0xcgzQ74aFr0|{zp{NxB3Km z^X5rze9z?XB6RkDK)~(Yuld&NP03y^V{mGvowD{`-q$BZRF7Ql%)cmKid|BBz*_OA!wu5c5;Fc<)934j%rNooW1$3CCT9cW2qi{m;S%&`Md6a{(PQfe6A6d9m3l& zeq4krkNjXvrz_>odWA#c=aTo(0sNO|2e~5mmOs0&dpHg<39suj@7Zf;4zU1~mu)S% zE`gt@o4H)+HZ=AGcXzxg`D5pa`^OXe9ac=TEeC6A+>+oIPm3LO;EESO){fRsoiEmI zv35V6{hy4k1u(JPNYu}PmG6u^XCtnuHRF4a3%R*eLrY>K^jeOnM*|K%ctri(;zoLb zk?YWVBlzsKWL*s{#ZZ%<5?^Ox+Zp-%M-M_~-xX6`V}1T7ml_W)^+5mGyL%tx#Gw$t z?C17<$cox#JqSVR3L70?WbfK3sP$VNkU$iFG4&x%KWw!; zk>U2V)7~=Q_A;Y{Z2tY?nT4UD;zmKPQO#tF|0FOvqynrWa|h}~g$L79eroYY#i+Tp zo-}u>J2~Zs@jUKknlW-W_vO6Rsuee@GWpgA<_adr;!mxd&DQ77{=_k_9tDo6?e)xS zuVX4-SXp^>miS!OlF;`I0R5tG?yx!WTpTag+%WBV>oYWca#aBM?L|8#iYUX%T`!>qxG#^<5n^=}i(GUH9(F4(|RMmkXUa$oam!!sT7OtxM^ z6u)!rqrCR~;7Uq#oVxceFEb6KRoEIlSwRd=sWzG+Iu6-5mDv|K&D`#=re{4w4zKk~ z{ej|`WtWZ}YnRTkbVD<$;aB-*u}PSTPZvX{5QEdpHdwk(U)h^UPkC= z3@*dOtq?w5R385cmMfYLGmLo5eyJk9lXbAIjHlT?tF_r52$3JmUHyhaA->Z}zNS2- zz^Lg#58ON~bCVAHvyRuO_M>cLk&x zwLY)zTN=$BvKW8d8Ce~D)2xoaLu1S2^bb)kE@g6;PfaYE=3Fh@ON;m+IF?jVp*eI~ zRG69fh=l4NZ1LayLsENUqWU87`LNqy&wYAG4{pK6A9q14y`^Ag|gf%SxJ6nN4xfnqC1``K}{K=rJP;$pQoH_ z@PCI1yz$S*?{H6e$57FRuqt=^ir;+^F$mP6H3vFW?+6)4=nN?0jz-obZ(l_qVopJ4u9xUj&pV$!+=2vUf=8{zKYt+RzstH2rvrylplibii9ydEWM)!Sb8 z#tw*Y;*pkK@L6!ZB)8gAZ1*Tj0(Opq{!gH zM%H7-ulu7^J4}x4AT4`e`@UvQ^Ch88Du{1=C+)&H$Cen~^cEKrEQV2ayf!w~)3%&( z`W2J7pZ^h@iK$cDoH3T*ofWv|C>4gam;hK{Z~*t zlUIJZ(dMIf0L;^+jg0Xv97S}@yh3ehcw;B3^F^X&^UTV!6p>ToR>_Z(68^@pju*&K);rUraw^ zffitsGRz!{4d@Y6j zx4K(-zMvZ_#SDt!&S(PpBSEM=F%Q1hlwh-B0>o6QuMo9H;!XKW8uyPwZfuSFM@VM` z{sU0|Cpb9yK)J4(cIVt)+tfe<&{{bILC>z2ghB_~8zWXHW~QX7Wlekj5Z7b`js zm^kam?KlAyXAQS73BS0N%R{p@zI$Uf{Gs2dh-`xzzUgwq;_T(`_62wJnNDTJjHG#2 z9f@GHgq!Fbr-t;8O0)hI@cQK&*ZiO_@-tT0{#J)uz((+SueNY}MWade-QjNNq#4(n zq%|pQG3kwkkQFt=m5Fx8^StSuUXGQRn+NRgExX=}e6v3d1W+9Z1g%CUXyJdX@&_6v zUsy@EIQbHGXhf;uf{WQI8KbmEtR+Ok3S>s5djQF!tUQ(Xw$F z6l~s`COigG-L<0@6*h2!|1Gc8UKw#eV^Z3-7k3?{&q$u(7oh!1F9RG-b^PdRJ_xZK z&M{Sh=aLJ*;%D_*$ml{0BZU~+uQ?niGT2GKy`F~>Kb-g0xIcINZWO@^=T(2Ijvz4n zk~0AQ(h-8}EU5({Nhyx;%2Rod+@gQh%o@4nIiiEy#ICGT1RkQD+Vz z!VPSH@Aj6-F;dK-Ewyo}%-$~3n%?nl1Z-`t(sw$)ERpl`?~UQ^J@!r(#nC)+bHO?3 zaASJYv8(d^VaN9$z&}9ge>a4w0fYE4lF}EG*y2(=+W~52$i&pTg=}@?4yqX+HR$a$ zoUmi&%1ly^>j9x1VJZcF_W)tg~g^;Qz?I=J`Rf2WDgkRX0& zkI$oH7InCh4PWBi9AcyUF8O)!w!p@#m}qPpnjO1h-y7L>4~u7XAHZ2f4nL4(vn$2r z>ej+D9_+N#QHrz_)LxV2wnUi@W=WD9OYTSz(Qwxm%M)H&V{yj!i^3S14xM@7Rb^s^ zA^Jo2dfi`0gy1bTk3nX7tH(^%VPtD6y^}tW{(RL{Vr$%I%2?L7JboJQ2{DBpd>=yD zE{4MT$A8B;WXMwaV-}b-_i@yKS~lSRkBK3%d-LLHp_Q+VY{XSr%f$g7SLLBC9Z+jY zB@|cFF-v}aXka+Uh}2y~jY-*=Y%M%=)To|WyXig5rsFo$(-#7f44$<(wh>!LvAsEA z@_L({8qz(k2UDYlU>Zi(f4(L)CGdMMJwK3=!F@irh#>o3W9AsS|MF%2e_xjbwveF- zI|x}AMDxcqsB7KYBoI)JBVcI+{I}yfrBE_D$zKq55F*IcVG=~aWwd+lvoOfHK@=t( zr`vn;q-hWh&y~tYFkJcvCcDn4YOm5!FY>pLcQAX}cG3RD1BitD(5MTg+2!P}pQT0@ zEu5ez&Nu+dZc{|_s&J1~}VIo2izMk?1A1Q?FT0y;jP*yGaAbqtkYiE5L zEff7{n)r`d0Aojp>Fg_q#0j3JSE?!d!tvDgUGhr17cFX{5BV&7pe4Q&O&rO?z|qCs z_{dD8WY&(E;1+7cyNL|iAUT;nQxB$Y(tpIqk#6y@A8LQ`h?h6LSBI=yNA9ZYK2@Xd zIA;Q-zF)|@J^g~)L|fc+wLh+nw^RtPXWwwYQPAtye`*VL>dA|9Th}&j6} zEz{9`BR+j!NGn2a`ej8>d`t)=8X5pEPo;3@7-=#7YVqhqGan5J8 zqe99=@JYXilhR2ek{^8p6D8c#!;Tr~Fq$RUc*&;hRhS?K*g$W-?mUIri6@$6QJkvR*a-^9vm(rzJY$FJLWPo5Usub_R^ zKWo|m6&HeVv`Sktzh?c|9Yoi zOO-!M92~X*Gq$ak>~?rHxO-RPm9gPyhTaGpgZ zI^X;UFW`ytFVf`p`aUqWivZw>G1^?ev^(<3gfYW%4o;{`XV+&KGQ ziZGG~rq=Osn;TNvtKSR1zhP0tO0D&3fPG-3w<|F`!(0S3l|KlUP^vy#JbzUCFhW7W z~Ona2W{i!jp?ax)~3pT?N`4_1?K)>f~LvPmv ziVzi1^^D@9rjXmaI6m&%v?N`0cVK2X=f?2xR^}TVO76jzQ<{HvuK)V~4r+h2l*rU= z<&iD7hi#{iCC($bga&h~pEyAYdV*lOraKQ+kXV}o3r$$kQ-(~9TZjgm6u@(R%@d7z zYuY~E+MgiScFBSdd-vWOTur~w@W@s6tk+SGmTa^nSa@SBbnrkWd_zaCip5nhw+!(r zTq;d!Ew^^62Q7}GQ8)6n@!W0!@Tg3<*GvdSV59wl=gMb+opxJ%H{AJaqHO-1FV(1x zP5e->cf)Hsb&tIndEfW?U-)yf;76+>O6_&yk`y+$ZN+^x_*C3xYAqZGso~hsbEF}^ z>pE4#LOrL)x0Cx`>iVx5-!8deAxp)RRI~Rahk#cnwst!wmk}TG%Vx%SIOa~E3&1n+ z`hYG9n2?KilQ!3;oqIx&mRwn$fGcS>@D0ArF4dm$nFs8g%gkIs#ePt|4kp!`J3T?0SihB6*G}I+yzRNs0drCKuil6CDmI;xfLA*@%n*Ku-mcET*mz7 zB{__^Y|TJfXU0fOo?QBB^mXre-Y({fecWXV)(zUids$-zqtDD-0clt?z6g{}_E+kK zt+WOVL=Z)BZP4)*JBRPI8+rBe@^bc_cB5^hM)~f>^1Y=j^*MEv*EOi#6<9sNlV!pd zVoE48hHbDnINaoA^&lle=LbHxoD!Pyt=T03qk|d$39DFtsP;{LkA&vpMO7 zy^`7gVD}!cQ4|V*k=No$Z3ha212p3Vhf(Fer2;itW^kJc6GGuv5>V%PfZ{>2<0PYt zwND1yecNvZ{~FU>2YF9Y%KpXrEsOJ0LU^HFz?CVf6yS0CVjWtwU2wC)g))$8=#(t7 zX0Vx95TziAx?Nv-VKah#InUkrvV)a+Ous8OauSZSKg6xaGzs@I81!o-4rLTS%8gM1 z%P#aba88slg$h&m`n&3DG3V+`-l;eJwv&5apy8jrx8`^H^DnCA+5CC~iIu;6wLgDp zdWEJS>Kg3nqrov^`pU;kkCpldBpYDmfjI6|3+d;s#PILAaL3fyV7zM zKYxJdI!EN{HQ|5u%s)!_kGslpLd2C{wy&8o(rRAlTioP%UmOL?_cU|Mt#}+NK);e& z_`Ni5rpkn%qa}JK>AAtGjI-5XHD<`Bg7lSJ-lY28mI=X6_|Rr|)O5s#{(6>t2amE4zcg8dI+K~Np!dK;Ro<2g|~ktY#`KC z^xx1auH;=ogqceOFGq+I88dT2J-l>?pfh0t-G~7*rXdFw1Ogvr!W<_8{K?;Akt_@F zmZ}*1lOua#_7Bh&yRi!-cbv4@_W%hbpoCg9-Y|dM`*E8Baq(;FEu(UHi?L^05GXx@ zjJ_ZX()*Wi53gHh;tLV7W?DQ3OU1vtBHSO3zxPGE>bNxz9_Mbh4GaJA=(@NPWg#UC zj6J7J`t<6TXCH+0K~b36DEQ8*IXC7gv$n_NIO=J}eEc-kF<__ZAGQAs6k_Gnb_E@F z(gE|y!#?`I(4Id{J2l#jMMTayUcbkEG2P8a(b${; z3u40}$VP560WPq#+-<;nCQmyzmb}Y#LU}cTn3^xSHz&EHRqN+v6B7jKv&H$!?}!FL zbTe(X5GcQord#85)n-vf&O;tgg{$bv!%erp74M7hzo6579H$=SUz5MPyu=>87>Gy^ za4AE0wmR-sitD{tCO!%0JmT#g7!VXZwMyA0jJnOX6;rV!e5LF-Tig^U#RqAepA#M3 zc(CRy!9+H=8KTl!y|we2)JG~blG5f2reF=2&(IQZ1?m|3YkNf3$i6zq%;$OF|4Wp4d|A1$I(8M!oej zz4j2*i##umt7iT&i~A0QP+*_MnXUFsFD(>CLQ>LlYHF$xBrxnHL2LfK2mEKHa7`XV5d+10 zZ6J*FgFlkZwoj|lD<+{L^EF_V2Pn5@R-ENGM44!^oPsE9muB}5QFAZ>XHre(wfRK_ z@&AyZpK+GTAHF^P%oqD@mUbwcjxsXJR^c-K-l!b)FlLE&ctf#f7kqHtjQB?6iA6B8 z(BJ(wr#%6bAgD@*QZQKLF%@t8m69PrOG=sa1EPR3*p5N}(HP~mDd4@8vNY^InHWY| zi^Hc_^xJ*-Cf-NMc+?*;^GoURjfQotw5aH8~TZbXm&C({OodnZuxA~ z5dz)3`RJdJtAM0k8PU2TGVGu60h~_JG3}3|{E3EnT5=Lj?OeQ(EHwks*$gy0J(n5r z{aV5UzM;{Q7LQ2-*%jDcw8oi!TRHJ#i98Q{bNjX`kABy?oVifx1=LI$>r~(J-U}fV z(j50?x2FO`>suI0W8b0ozH?08hbY5uU)89va2Qh2z987O9(WXJ;9%Pq2=N^~07ah4 zYovaX_+70~!RhJs$7R*@(-G)aX!D?*d%5G-Dgz-dA;H0(k&$s%yJ8@Mg=Gk^xd}F? zC;0A6QG(}iOp-F+KPSii5%ss9ZW!%`XpFI80ke-~Y6q^GLa?crZ+X%^&Y&JhmZRy& zx_boHuMAPsy;VnExal_p5&)@-b?y3=NfLSDPu$RO7t>0e(oXJm`5GD8=uAt5$5Vwv z)IL84)@0O%hq~G5Jvzt25=tvP7;fe=)G~1&4#_rNE}tK6T|nI~<5EZFPQXnfTlJR6|VcoL3|caBW(#Sb?6fn9Ce zo2nO*W0ubcij&hA_gd`FKmtOYe5ONiXL;SksK{h%=ed#wX~`~i5!3!5jgIm_o<@v` zWN@6!{)|#E2`{%^dn-2y58jd9pL#-oUW%HW7qSi8`&&R0Q5IoR1V&0OVNGH)bCN&T z%HV45dTN~dNKM`TUL?2T6NARi+>|rEW%ZqFOmB`qg%o1rJv^fmpCDHC#iDg(yptjab}%Z3CW>ylPRRh%*N2>;QG(y2X(hEW8E$= zHCGjzn5X|RWDDUq5Qc~7YqkU&4`%)BVmPB`p1S;{{7Ibm-u2H~n$3X-6!Q2mo% z0{b*y-j_RtvKT&pcX^aG|B1pVp%>RKJAAsskOXwH0Q6GMK;$jb{O=;MGN1^H0=IMV z`r400irO1m|D-&r(Fv1Re7SNBre&%OlW%Ot4FpH1%@n5( z%dq3-N=D{7uo7W!M;$(pxDceJ;eVW4>C-Bqp;DFa8b=Sn+%hkO$)r`Q2!|OaTMt&< zXYP`&YH6RYNUEN&Fy^_|hw-(0X{g_u^5g#6rINb$P|KKXp5}zbupo-?g$o0db)lSb zUn!dLS6;etN1bSd7XRuQ~HRwo|fOLl2j2+Q{yS;k;H_%NSey49rBE?=F!MYu zG4OhLFVR3Gu4uW+1`XOXjiowb*GqjnM6}H>=X5&A7rLGdKK0UZxG2$YZn`M3z&;Dp$V)3_R4uZY)A2w|_2m5tjoSH8wv@5R4vFdvDTNy;RH;=bF&U^(N4< z6BFN9k{P!;iYYD^sL(@Sy^{MzO$xhw{bZnNrv|r7adF(G@{8Ei_^UFhydt*kx&y4~ zMAiOW>#dB)O%2y|g0P4PSQQGkwN?U3oPHNt3kNkp*M(0azxeH+zMN(RG>Eu$d>zl} zju8BY$LgsMVL;d7<{ZLl<_@878PooFPM#T}PA;nQ13gR857m0NHpvVq@hX$FXs*3A z`yYameHAzygK@f^Un(I}MViP~J&c)uGak$UdoRw+P^Ayf-Zj6T@(8_z7pq}m`BL$a zpe`DGmJIGf!w2VzINnH6*doZg_0CUeSTy)E1QTt#g$@x5XFE`pD=65`S|zFXGGp)p zu`Lt1$ZIHDWdZP4aJa6vvuN1=m0Qx;(dbU)d)EnXjR{vzjVAU-LByB+D-}ebJX039 zJGo{Y^rLt+`*1{Z6{kCA$pE$zlVV&qp}{T7xN%JUzKgnBPJJJ5XLD?KGoi295Hk~> z%`U7P7-4H}C>7poofoy_dIaw^W@?w!mlGrXfz;NsB-^Sg;}UxyP$9a4i4ynW!$k-1 z%rUXA6v?O8ZcFa*)&GSckl3x+$eUI`>xWk(0fiqHjVp4hwEo(Z;x%D%m}JLBMhAf7 z#i5)9{-h$)&=+PXZxXHVZeCI-4Ma@%x zyf@V1mg@c*nE6wGpp|rCxl+^4k^jx&cHG4!nO>XIFej;l-bS12->vPHb7vesE59lr zK5@dHQ!lxZaByf6Z_xa6w|-W;d4goQpGv=p31d~FF{-{^MHW0q#(aGO+&lfQ z*+SN1#nhaWm)B`0P9(ap(}6oEUv2*`mS5a@_a(={0NqJAoG?&Hit!E)rc`0OZW2?& z*~nHAkeaYia_|Z)_&_u(K89&06UARTee)?d`_Vd4jb-Ko;;#Q(*!&&oo`DEDOt-hB zr=pReZxQZ?^7%6ym@XE~OTJT3Z!3JL5rbA^_VF3lkj2I^YvvqOnWL5nraD*QVv_mc z;YplxG5G_Yfn<;l$}0(Cy{y*I@rJNqR=;JYZoIIbz2lb2VTtDf$|{ED9ybpAb+(he z5AXaQzzK>_U0q#uqP?_?CLd3J{QX?xXR;4D&C`B2G3Z~nJ8^NK$$SPKw=iiu7MW#c zY%(XmOovmsz;BQU@5^W%{Ky50;eG>vqCeY8hp}ZLDQ<-BlyHHBa&XQTEt0t`>+-p&aF}-N&2a1eW+=Hy|8&xxha~GFMOW#8 zI+lYVGo}54j%Ef2H??p%sPar7i#@N-l5qw8sPS9IFS`wjHHxagtl<}VdTCi@`6st-D~r9x*4g(tE5V@jC*vA6&Hv>E*t~Iqr9vpb7(zT&8*PBY$`Wg*v9=&UARj%9sN8#%E@^5iF!zLH^kH-(ECuct5qcG;}OFjZ*SyE}Ipn{rLL0L7(@?cxzk7;)s+5_k17n3dqy0 zaClP2FF(}I4BzSq985z@!Up|IGpcv9gyjm%_f}R@ZN!6bnmiE2yA8>TF2TFI-=eb3 zSj@bcqwr5t#APLEoeR-$=67(##}rN_q^@prrtO~*RY#^j8Wpmjn*YX;)Z#PfX1WG&$olt_2b}N3lHLU`(lF#4+DY1VZ~T zK0dxTZ%;v7{3Cal;c`oynG7CgnzraeAI)y2o3^Uuy#-j`2iBJVM}v+L|Dxe|p?7SM z1eNk#WUYmgolBkNkAvDTKk>oH$CGMg11j=~1YjkFZRx=Nvv9GjnE}7Yxouh2fA8ch z(-`}2k8RDdP!msjS6R}W21ih<$b~PL0liRQJ9IYT0Usjt=v%chsWDLcH1n(p=#w9h z6&sgK!T^q4f<}!iXm>T^Fi%59=0ub`^_vtI)Nd1J>tK`gZrBJhx)fCdLhxbg(cMvl$rTn<+S9Z(hoWJJIC3h8Z0ahxxq~B_ns|}m=Y9i^X zH!EcWp`QgK+Fzlv43=&+4t9KRa_+WOj)_`=a(m}{`xQK8lHT`dLR2T!6RHRI>M;&q zHd1S8JG^8Mivq`uc7p{5Hf>jNLz_vrkkd+zHWT(w`h*ntkL6@==}XQ_^F+=1^Svk+}w6U+93(aXA+Ekj*Zje@*2MFRf;T z0JKMSO3Eb#x5axE(%oX6g@r;R(u#^1ZwkCpi_(7W;LY2+?Yzsxi)xOO@sz{A8Mso6J^N{av(Wd?)WtFJ#gJ-#W9Hy&cVE}zZ$H(EJOK1Ljl|?n{ztof z%Ha!mRb~l`pJsh*dm(VYdNRkv>Wv*v;mK)0#dSft_Z`IbrtT_(L3(QzPQdbnSp?Mi z=VMxtT@qlup8Vq%|4Xt!cZBZY#xdn9FId436}E_Fil^dYJ!+?)ZjK_Hdn1C0SaIK< zVV~KSvsgR?_-#jIW@#e&-rHl4c<6R>U2scry-wdQSBGyIzV5ZG^iZCU_Wf~`%` zgFahkrIHl2P6>SCy}jqvTlR7Gr;UWgHxKX2IKM1^Tg|-zOjghiyyWb##Xxz}7wdok zPadJ2%?^k3L`%R^YzMgx#Jscp);rrv@|REFQ=|PF17`7qK%7msI7#`>zD}ppOUnu|-k^vJv&Q;%n(b_dR(!*cYf@f%s;1*MpiuffD zbJ3L(zbDDv^le{VsOeT?p8s&lVW7p^xxk>e*0&;P?YBpG6uq~MD(pno1IRHsct0S< zt~bwZG-CWHp4bkClcR5ws%pN;?-iYN9*QsAi z9d}R&fE2h$y~fYnh+nZCv5! z`@T;X^p-Bo4#6xEl`Y8da$ob+&RnwsJ^imas?L2%=u~)upyTG(B=g78G>DGFLdP)} zu!Fo;s>12uJf5XtqOIhI>3iyYw)#v9lRVKDqE*G8P1E%7C}(lK`)!iqo_)uv{9JV6 zaW@)nf*zbfR4^|*r-Tm#YG%XOs|`~iJzwBVtMM94?H_AhZpuVl>}ur*7whB`0ONj} zqx9+_eEIu|%UVuvFG8GiNH8PQt)jqioaYExJop)W_XIa2BR#hLL1OUxh_CChQ>V>Ua;(l&9AEw5{06#%6+hN9Z*^df7y)K``{SdZXG9tt=-czlkRRIG+}!V z@)Co3hfhsQGpRfy%JxPa5dHR_p@DnI5hq@FXuGvM@HOzjZjSR;`fvZm&4R5{(uOiV zZJgbSG^Yi7IVc1hQ~ zL7lBgHEA&0ZJysgtt)ouYm?tajC&Dq5e1ZOy^is3h9(cAl(zBkC+H+enbBICOuyIG zBSbtlE|s-phjFsyZTBneESHIRt~_^_-{GW9noD3A4m;_GLfL$)U9F@8T{x@F)c@@9 zKhcFJjQ_w!pvNyh-Ae*sBOIl#d${2N2o+P1m5u+ne*k9@(;u-{P4ubyzu#KG7_+Rr zg%blM^p45pjYFo#D}QX3L3om4{X&rNnDGT#Ur*=GvlSaHw$gnhk!0pQO_Rn75D8oi zRewIhc0DFjLmIp^q7*^y+%0SZafCM!^Z3#kMVr2t7xK_p2|ZD#;A4={T}wD@h*8p& zeXA6!%F6)R(-95V%kUb19<50s{7u)KXaQ-WOIT|dg5sW32z~#l=BVdGla&tJsqf9> zD=<)%Dv__z=X~5qda1nWIp9c4?$7k!DD2|@$vKE=b7P3r-Th!Dj#0( zD%!J&@)^9?ZhIJ})KZmMh>+}tewS9b*x>dSW9tZ;Pb8^2d={xfadH2|!SXk9_O=M} zW5TpS@Pi; z(8o7U`+{^|F{(>sTnii0Ooz{3MnQV zuMLYv6EFXs<|zAkI+wwG_7nJZW1Y^-IF!0)bz zZr@~S?__)9fgdxRmO`1vJBqt%QQXvq?%liKm~#vhA*<-SuZn{^EKy{J=C| zud_EVm?gZ=x6UM=?<(08W_J8h9a0|28#vK&SY3AGh0#q77)`OC<=eX^`;;ADRHfgR znYn73%W8~Q_TW!U!+8`BhSeUys)ShXn?pwP#e~7t&$!tab8d0p$#><(0-NGE@L`7x z#ZUP@&>i$XN(;5}%vdXOhYG4^KVnGT(~%sTY_B=+U<%Py)X%K(7?jHwvJ{IT+c^f} zi!3yy@d!Fb`?7hC%bO;}NtEwyH>`bmd16~O&ku$_JFaLG>#Y(laT)j8&=y6j`Hvh-R#uWMG99_nQdwIHn;~2L)&sW)hP>T}Fusq68|u86{kZ5KfmJBz zVT&PIP^3o&pMN(jAc{g>iNmX#V}VJbIW0WQk8yI*6OEmHuNYh*f_qB>2~8*#mlm@1H2Z66Ih98 zc_*mauqQ3%SDyIBX>rRQsL4CT+;8E@hD7a{|E8+@lmm}o?ij`+e=pUn?8{ejah4PU z2l>8{{XDf1s01GR9^w&F>ptMDKE_;!+SOVHzh`i^Z$}f>u{T~gyOj*ry>mcLO*sz? zmY(_%9?(4Vo5&i!38ox1Cp6b--RFq5Lk<_vJ9wR5-(rGw5ox(vN&(#bA+YY{W@2rg9gp1^DX&_sEiX{{ zK%66N{h)HZWbF%n9O+@(Qk`bwhgLSO2EMp_Sn&|mZ2ZCLF!Fc<78*1(e&n`2MvR7Y zVvNj9tS!3Sc)Odrw3$xF(??Bzn`@!Wd@{^n+tt114yH9Fd*(Y{d?#ft0(;*KQuk|>dpVK6^ivy0&cw;V5n@GDl%yc zSO(>7FLX@o5Ug!ccKYLMRXQLk0)#T>8DXZs0>0~_eX(U2W>Gn9?luUt(1oE4=v_V~ znpt15bl|}EGCmveBlWj?c^3buW4oE#{6h_xAQuB~v)krn-3k9GDA`H%v&yh1l8$b} zh9`NX@XY(Ey$C~Khw z8Wf&>06Pb|b~&Eo?dZ4Lvu^~dxn(Ea$G{N#T+3@$&T8Mga7fAmu`^)|zX8l+XwMl+t;pqkM>>Q%QG6yHAU zgLbkY{Z9I%dBw@l-0!QI*O>mJ;PgP$1j-z~wxD;%O?zAaH52+{bfm(~Cr_4CyQW~f z35TgjNKaw;a+~2)5!hNz96B&PkARW=I1>pahk+kTbMW0^!t76Sle*FfIkCdc9PMPu zL}=VI7`}w02g3u0{At-~%C`DxX#=t0i#TUk!a)lm{zoRX=_w3M)}3ZQ%MVsZkCw@= zz85ZiINsp6X2O$`;d~o%@2k`xoZTN|tqzM0D)9q6HLDJ`Mc+$&2V_MTjk9$M^E3T# zUXqz|a3;a;zMex&H6lc+N;~ARsyQ1bB}0>jq6XqIJRMqf>}lNR`R) ztk%UAA)Y6+KwR3G)h-|x;tlX|Aue1J-@>G$Z7lMu4AyxpP%k%ex~N}MkTB1slMe({ zd>EaQec3)d(Ec=<@Ox{*#fRLNnS?hUGESA5n+{*Kb6z*kX1RwCi_EkuJM`zw??QGZ zm)!vHzC$^kjlHNv^ecRhdCMxXVTq7ru#h9pbGt@>dv4r5v-&t@WWXn`$?r1KmZE5_;2OpQD^jXQUD_Xw{XR z!)_MSXd$1EYE9~$u(Up$U$N`d+9Ix4AK{&Y6>!`S2WIQ#5!77TgVATa0J&Kx}u=3Gf=+)4&!9!6?G zO6q?n)3Bs8W}lCl zCZ(fQ8tnDUoI(&X_gA~pLFL{1Y)Y$XkqvfbEIc%cW^nx2Biod_N^GqjYs1+ub>}0c zPm6<#fV^e?v_X8Z=r%j&7ZzS82I_j^Vib28Ws7I&k4J1uBR9`IT=Bic0DWpSl@qI! z`i8VdVtDcN`4kM8W!i?Gl<5@?TV3Qe_ap`212=_TyBZ3h(wgr{p7|H&JKz(b z!i2}pgDLwX$m>4QtkH%};-=rtET6WmCvXdXyTjI$=XIZ-VPHQ_tmKw|q1HFV-Od0J z2lVX|?d`Gj-+5Akv|@g(cKIZm_J_D1LM|Lj+u*D&T&*QZq#Ci2g;+Kh3^ffqi* z&~Yhfy7p#CWWK#9qeZ@C#z^F@LS zF9I@Egl;|!RQ_eYq!c0Opt+~>w7vryw@s=sduRzQ<6SD_<^bOsJMBlJY|F5cd&i}b zSAnF*)R?3wAP&B`c{Oopg;Q+GuE}S5ZfmNn1rJgmQ*TRxgceG7h%L*~RqYUszj}9- zr=NJ-)2_o%U$dbCEg>)IflDGGpf#YJ=1=Y-n_y;h@JFXhZ{ClGj^6M`h zAcht+BG!6CSC+3VAf!l(yOPl4Pe|QGcE{eH7CkfD+eL!sTDnW+JUo7oo9;8L=$uQZ zEUq$aji#&d15zD;2#%XC)w1Ow{1^)DDOg<$-E+m`t;$El80qjmUm)TI$gOH)lmFsy zjAx(2&3FvmK>dP2(!aP~3f~dj)yTfVkh#~I$!SyoVxh@u0_;AIK-q^38va}5-JjyP ztDmJnG~9hlf9jwM@MtRF5ZWUX8-INLg@2j z)j){KTASOH1CyE0tQWvJ#C&Vq$O$|EiB_vQ>BYrJCu}b1EJ)onS<*}^{aT)_G;9tp zv3%@wXM;^=RESuli>^JM+01Re+s=04gURsWUkI}A{ud5nD0(&vA31Sl3^(?(C_=I; zVXio&(%l)}WAOcy6Df6bxHs&(Udu_7MHPWhX4`9)lYhM}pdn_XWS@N}r#Y5>jC?Sn ztsOcB?SGoBxS^eqL-GYO$KF*z3NAC`;M^9shh=wNb?}h{FDB^Eh>)5z-3*e5sA+$9 z2xX?EsASixB76smeaz5lJnyihrj~&`60ovr;Dz-WVj zO-cS#t%Hk#IyfQz9m&GFQ>~*a&(_&KLx;tP$7cfuwjF71ZSSf=E`QSawD(24(Eqi? zDKN4IQmM9ZK+fpfG=K3hpVzP9m6Wyytx60*N6tMP;rti|vkmlbOabGD)u_-N^|`yO4G*x0Al7CU=m8k{fl{i^4ND!uyB zz@)kP+Z&NU&3n?;={M79J~ZB{xTOEhX7`FL%6A@t$U zf?rhj)hMIG!2RPELRpuaslt@mHH9RT*)-_M5EV$u`2fSEtraXd^u9;0Lw=ZK)vp~v6f}YxGlurQ_Hd0l{_d%N;VGdRX z^ziWBa`CDM3z7Doi<1g1 zuiRTlVB@-_0 zpA|b*xC9l{$-W1v%JUZD(XqCG4VZ;n*^lrXv;b+o-0_=!z8^_9BFF$?k462$LN4SU z70P`b2%}WY)ya-!@EG-*f_?vVH>V2X%RP$V3WLPumb0q8wQBv`v}hwPI~cY<&TX2w zjpSVWI)61<2&{~+alkIu-B4hDKwyoFC^)16Iy`P2&$KW*9E+%#!I2tiR{l4?z*Nau(|pqkZ|^qHPDS(>=HfBBmk(dHyD@v`tFZbo5{ z?o@=sC7y1SQo3^d<7(0 zYY)3nHB}@EH+!04#p_jB>VInN_ZmF}ELKregUzxYnEjR+_-cSoI8n;l$14g3MqUT- zMPD!G|EU-MJc$b^-b|%`MlaBC%ki__kM(!igU}_7TdjKM$WhsV4UoNJ`5#6Coc;ia z#vr@P{{bN0G6di|9$%m)?tXj)6pc@pGfQG@H5whp|IFEh%M>${VSlzU9E-Ap$;b2C zu&q=Hz*wQZa93QXpQ^DcRy5qr*E0E#0r@_}is9bVOK=0YM}VQ66E_JSM3vv;c3 zAa(h{A~fqAmMR(fd|VktI+tkd>~yo9Fahy?E*96mtK&aVL@-=_suTUg2x#+PGZ^D` zA>rae>G$zEt=hnX=dj4pp8G2>*~~+Rl6^q1<~;yj!pu)L^RAYarLi-ZCScJBM|%^d+4$_cU2JFhd_Y^0M#$hT5-X79XESAQAAt5h zfo1RlvevQG%bYX(K#9IgwQ8FrgX1rAr`;@(OE=GTsjV-63DjfB?#iGux8rRmvu)&p zAWjvBz*}V5=_){FHQ>drSDhGkpU!^(JV?*uU5c`@WtC4j!6i?*M}1m4>vNReiy})) zOWbnBv1y@G&=c*sJNhY8MYmGJ7|wKAk|U#pk{_Nfu6%Q5xW2C$A_sY#>&|v|jc%L$ zVO|B)v|p{aDyfLBqA*CW#f3rU{boq4$+ncTbzLmr7~%A~9Nt-otu{mg?M0V$(Y#=_ zkU2VRu!$Ew=w17%R=E#jZki^UXa!1)G_o&~D-$pEc2Lwq3e`P5b_9S}O~8nalA&Z` zr9(Jf`nMYKIxWl*SB}ztwYoPK0TP-wuYlm4623l)a@>}>4c{J>@@(Pm4%V&~ojOhj z#Lf7q$gAIG8NQ6<+|w#M&-w?+wdZI8+f@pI7THDGq9zwWEX*%Cf}J0cXAUaHC;5)hPVp z$>@xaTKGK4_zJQv511WBY0CNblpg*O;+CM{pe|VSc!_TrPW%h}Bqqyazbki~)$dh1 zXgYlC)v+`T(Z2}*8T^4J0XA7sGC7ap>?u5sMAIn-OrRc=#EaV+Z10MHseWYrfS~1M zpC4&#FS2%)(wwxZ=x2ipL?#{k&>(X5%-N7(JzB3i54);+sou^)2mQ$B7nh`I7yF)| zs}E*XwgK0T*dsXl{gP6Q2ZG!}Yd=Ri?Pw>z{Bp#K4ck+AaHybViuTGZ-L`(@w0nHF z3Hxn!(rrHEi(69{_Vvx5nCvG_U@2Nj8@AM>)3cmFlz*OF-X1HJCjPPp&fHq-0qOi= z+PEi;uYJ6O>4;>K6|oS#RBbp5-$u=$L^8@q{`2J@7TxUCnnXLXZSv!&S2R$6*V@CI z5`ow~SJ&;aptH+H*0;y+2a2B)pF>I-z!g77Z>+Szxws*B3Xj9howFV82>`7eN@H&7 zq%oHc^bKI=3!67SF|+m4DjzO2QvOpq%HIo?ip!sc=*N?&otpJ`pAy(f`8G`Se#BaF{Ri#&{W3KK<~O;cT=fzRd0qjtFs-mZ6#DR&dYvOVxtUNiYfzL@ zGTQ%b%-G6xC@Si3lQ4ME`IOEXZH;nv2mGs-dm}z{&OSq@d7Jo!|M=NVWnGWaJqcl3uP0srf zkPBa%f9Ne|$=`Gorri@f=&-m+Cfi`=tO7NM8e5bdqPt<*Ts;xjS-R(aB{KIWBr>l% zUj20t{Fq%ai|$;)H=RPZY8@{6r#0bud@NsY*xwk1d^R}8e&z9feIP{x^jAbz+N{AA~03W(Z#7QoteYi_2&=WR2+x zNO($N!*)ZdAR%nkcx;tPfW=U%t}=GWm<=6}fWUEG%i zPT|f~2Akb1bHD5edTvP zx98mXkxEG@UFX}8Xg)oT5Q-zm(+JLs7RPD0knPVLt5kQXe^_@?z3Gih3i0i~kr|Q% za^+W1A54fmyc6P>nH@E=Vxe-?07|70@}V$|rRt3yi+ajMDkoCSiUyP@G=z)ZVM zZ^_xABeERj+gI*aZO&KNs>b>ZrI^`xsdZvR$^kDEl5VHQEA?0LBdwp!8@eZ;XfByh z@)-BFa2+S#G_~AV&_--!Y(Y^lqEqoQv9MrfKEjvn%J zWn0CdswMuu0d%_2TrxYvlktKec-aH4ubCtyL87HWM-( zca>A4oJi%Y6_1e55hn#*qwCcE?-&aNR0MqIp<$a6ot_&PIwVUl^|uj&FUnZi44MaI z70sA4k+J=zSuVzOj*JHy?So(gr*-lm%qHTLu5GM#&Aw-7a^yYyI2C(s@si2_oMg_@ zb~N|$)UV#~ywoa;RS_d64pVWJcl)Iw;5ZDoE1Bz`tRL+5XAPc$<}A-rhtr~8;=i`~ zI_lK(m51m|W;G15k_>PG6+tc9PLwQ%F|1ABA=fZA=V;vL;~XpLCmZRX5bLd4 z=W3;b|FW}7M%EnxBX|2or_Z|~&If}ch>K@F60@FIGa7>seM@$u|HVUQ5Y6pS@+krw zus}pYP8K!gh#WyUr|z+$)*wc5qtL?P>VNl&zP&=Q~XI?A|Vu5B$k(e)OS@9Joy9qOe0eG||T4>24Vo2do?*ZB`gk zIZSi!J5vu*?l=VdJb5^gHg}M>>3U6v$1;Ytj(t9z;wq&PWz`SLUFs~V1J{)ib*24} zOMtSe%)A+zE*Y!=QezT}%d~FO4ebYIQp2q&)l_;=u95pTI3jJv}ANZhRklY5jx+NQZxn|^UE@b*-(;g zxkNglAzh&F!I@lEtIs_uB}BKJe%NkhQu)MZ!{IW6HSsz0vRUJcrB_otWl58&#qu3q33sVKSuR81^pHp$Do@uT4 ztvHNLN(9i7Q^ZAT0#a4DntO-nDQ8@-4$5Ox;5(uAbdgC{zId(8LlA?U@H zg7)-eB(Og(iwV}VL+wc7K|XWs&KGHAm;BFTmL-x?uMFbj=o>yoSrs9uqdwWFyxI?Y z?Vmm)L*#h#b0WG=^mc%k6EK9&@f9g{D5kgs+PVf)#jm?f-y-1vl7+xvT zg+;7~R+OTqIre7YzQv@f8I z2~om2CbKA?ew`ZxW8O4@jf9dY`}~#DuxhNSP<-~t8|@MK0)7+hLL?MC`m--`kiL9p z=8HDwm7G|cA1T@Aj@k(&FzMkTz40_4O-0rJFw-`ORqQ5qD#>Q#O@gST)F>uEtE`MJ z*_u)xe|1VP33sXw4O>kKx|?=1OY-k4robCPdK&3N4{-;#vV=6~aGufwl@Bzj!(U?> zOU?hJPd_iTHTJA^=6y+6m@?Fwzw~bDl zYKD$@dySQ->5z`0cG}{t`cVC#Jy3vCVEF0;ldbkHA1IsNQGrgL0@^}8#VRZ9hJe#o zSs5)Kj<<}UNUSO=nO3inI0n$A9(72ckXj23nd)uIE^^gZo`}E-JaM{PsgnGS!_P`P zzN5|AySbBP#rW?f>;~B@qC+1Z(RyQ3DW?aH_OA&2FNc1vwt8u$Wr8y-hNP2n7DB;Wv=-OWzX_j7X2>vJg>mWB7Mshq|r{Nh5 zhE0QuM(+UkW`QQB57Qb{d<iRWyb1y=U@@~aOQ>KbFoIk6O z==}~uT|ij~C${q{F)L{k5fk28!$mx~infMghNJ=ixW*8L*uy0`ID3Sb7v?&PN?{{U0&BoX3f6a4<3P+Ln-15 zK?l^W62T*@3g?xlB8m9(SCIn?rZUUJ9~ha#wtm6PS>+3NY`_GM;YX%33r2MEQ5=UjBMRYOOE3v+YY%R^gs zAxL`0TwyG*|jdyNdlxs!A!xL&-A@7E1T1wODa{4o8_yi*arXLHg&)Q%NbDTVX?*U`?Trzv=469&~XEKlCEg^BlC!r z7;8pEYmz-YdH*;4$^U6+GIjs7DMyW-L5RS~%`|hj6{0ROa3LsZ%mHiiPqpFCdnDCR zGLm6--9bLcf2vly%vr5Dv6s8i0c(uiVaVMz3U9)*#z3-SncU84OcHOb;`u&r9t-%1 zECjZ-H!=i1`EjkAd~#Msbw)^YR+u|0a#kEb@m85$*-988XI5`s6*OQeQsLvf${Z&U z%&fwBaUR3$;@fTq@_W+5@K%fELhBcLFvb44FCOP3Dfjk@DT41%7=pyw?zeXGQ&U^= zi}8XO__VIEupz?wXzo)yrGQYa%SVz!GO1rlV5sL(=$>?Db-%GVy3EPa16CH8p}Q{R zM4?T&7F-fcy^?f9>GqXC{X)|MWE3i7%sAgk5~*V<0D~@Sh835qsIf}(PxeJE&YV#O z`9+Xf4LB^{SKACDgJlAgzv2=EY$_2UrAov$FoXaw(7oDx+-iZ;(G^Mv{whYME~QS! zm#@v;C4<3lV`c#Ka-NHG<{VF+l!_J6BPX>78EY1s#v%#)<~Q0 z(DOgUoOlP&`+4M1ai-1$o0^CD-WvUjyGP{8)MhajJhkm!h{+ zS2#{RbzU_<3_0L<&M+`|0&Rpj_XpPR8cNm5QY^AD)#LzNrP5sTX*W)l>G`q9>#EFF z<4vz$-~6q|xOkOGzhX1_ECC?Y7y|>K(95HX54F$;yin~Z|5~PmW#*+r`2`a^Z-cCc zDsq&67USoBo|7rCJClI3`Y9ht{HT1euW$xCEVS~SG%kthcyM-5+G?CLQ-iW}ESBv> zZ`=@sT;7r~N)1S{Y4HoK+K^kk#5Q6~;bv!CcJ9t|rvY4xs*`T$_v|=j*5g`qR5-?+_lJ@TT+nI8B|)+hLC?HpX4^hHIoc ziU@b`0z-L-mE5LhGKw5L&Rvg+L-BGhMf8CKT)FqXT<6pxYe+xZ$tE%7mq=(+5z?grLA-}CVqEHiUn87lz@EFaD+Nm-S0Wio*Li4LbYaWclI zjHyTPTU}8BMgH_2Yj*ix;h}%k8{a^-x^rg)82}IMp&)h86YR7F98YXV6QB&7S!~YhMC4Kr+ zn}qJe2>;kg?K0=LYM*Qw#vqS|IbtJ&)_=LwHJf>>DIbswd61ifhG<~t$*`!B`^nm| zGvIv@2pVzfHXFKjg5|Nl2x2%n7rc}m36XDm!PI&_D+Ux0GG#RX@irgWwX9VQ#K&qy zwRYhR-NRR*I2F@SH5v$&CdExkIf~9bCccMpH(*3!w{jq*z@>S`kkr;lO0F7#`9zQ? z-wluhu=S;iF78I$d!ReFD&b`TgL(qmOagd2XzN~45}+Jp|8N?jhCECEFjFM}PdzkdCHL{&IgF3BZ-h zs25LVzLnGQ2J8}tBb7Y44zGyzm4IBvF&OHLN9Q{p++L0&_sS6O)pvAI)#vutWD|}D%c4N}+U%S3!9_|V?LKY&#kbDm`Hb~6Ryk@_ zymtUt)EXo*OXc`mT9%bt>{hUT@I%loe?4JO#)#fj)-BfIs9Q**A$IMdF@ZkbdRCDA z-NYHjf?RM>hJQBvXNgMUmYm?XfR_Tf8bcxiE0)9Jiv4rnHlte8IXYoVbgKp@0hv0& zaEV*X^M1~Th?Df@k6oT(>2Q*bTKdl`cu3qw<~w~Zsa_8%NV?5xLoJw0SSb*`*RoqE z(O$2LJ`yxr@+=rmkC2lP0e5~I4!?=mncraL@U2tIi6oP=t2XDs_MDVwNy z&T(FTbK_C~gME_r#sF?RM4#L?Gm56pHg=(O$0)=E7JI{#U0uZ4Fe=&poo{++Wtf^) z=jN9Bl{cYlsZvmxp?G@FM5Mnh)&|e(SHvHxGgZV%8&^!G&h|nFAh)(g8;R3O{lAVA zr_!q*hpk%U*Q*L2z+@7pGo*G3^CvNH!28yjz5BVD>pbb$h!Z+~gCY0V8<%!t$m1mf zl>rQ?Ybk@zceT;_KUcwP-M{Og)|lttV*4@tGsAN*)GGiW9rL%2nZpnlF_Y>bd8&6& zYViAx7+6^|K5&q>saggFl{hg)e$~&i+A}4hQ8xA%GYk z%R`=x4LbB)F2^a?`>wC=!{+u#iwm@2T49-HM+j`7h?_U%sF3c zbI=23mQPu{mI9gd{L%{|d{?vhLaYM_iM35DzMCd!{|d{iZPB1dAPq`aiiu#mOCTu~ zL1u!ctGUDXHgao2a9F4j4I8zxBqhlR_1$L}0%Fffp?_W;ILvAsS~u&5$q;bRUT5UP zeT)rQCV>>i5fp=WC?Yabb~Cax0_l1#*gi4kPk(38w$_pD8=_MG{^POfN_aKZjq0I5 z@1ZyMHG9%IDYm4x3jKb@x-;wShtNS}nNu~BpSp56gwnsOJh2YD`uoECir(!rTv0W* zaKwLk0lM8NDQTWix~Ixhuu`z@4`4v@tVEB>71|TlH*fh_W$&x0NKjGR-aT1kCcPRK zwdEB)TlvrgZn>@Y$zwPhn`lGO(DGo~7h=?)Ga7 z-DAF(%#S%^*BpW{z!rUWcvSwxhI)x=eDKE;`g@Pn9M%+6OZhGJn5f7U{$sI5)gvd4 zVrt_ai}Yt*v@-{=WBBy+ocHC&&&^N{{1Z-@ylv(De7ZxQ;twTpu0LLGZ>H28D7oIk zoLAX;qi;+_92|C`%%Bx5iYzKr2|m0ww29gOP8_JbrudE9?`8}B<`G|JfE7Muak;nw zAi>Ji^j6?3cjV|&r(7cjrkE~r$0ZIPs**S2zaGD#)tb+rO2_=54wqNvhn8Z3&mGBP znO}Bqgb#J-XRLc2N}rCynuKQpkGJ#9vuVFk9XSnUykNDpA0#Eq>e&>x{D#GFolEX& z@Gl!4+H@(#zZQKWVUTXp-cYIiEyU6}u#AHa!y51YWOg9%b}c`#n(OsSrANnZri91u z^tOx=iJ>)6Yo<)!jic+x{0RR??H*$a3NI24QbCLBQGpT*^#SYs)bESP-w)YkwOh1| zc#s=z`&rn~vBRB3n=JX%@9vEr4=)dRNV`h%D#{Sl!EU*+N^Vb~N9;`>we8W3u|BQy z2`f|7Uh+QtFLih5#Ft~!MX?!4D|ezprS_kit{z=*Oj${){PGKSTlf%X<{Ekt-$lx; z?pWP+2|BhL9wFF$Rj2;?bz}?7deT2c+I}OP)*Xm}EmDu}vwG@*8A27aOEUCXZ<3xI zH@10m44CO5{rIk3^51eaPyTQ;9h5E8 z|JdsgyL9F?VwD0<+K`0xB^zbJn9B#|(>p$HLuCzk;who&5DhGM8_kC(o}Ivyd`2qC zduYHxy@R=QCi8-`RWq2I*)o|=eut`(;W|~#Um`TuZ3d}WC`H;sD?ve$tqLkyOj)t? z$EhqF9(2+#!pMGI#yd{I`I5?L^lpTVDeo$HP^L`?bdyhfUX(UjEAGa4dfQR!Ptb?0 z6q+2050ql%2#?*EV<5chLNd+gb9%3Tkn?UI(imIq3a@c*rnEZq39q^^pHY`n9ZWEk8uhXhj^)WKbQ1ucOfA1x^yqJTJ&%;!cjIsn(O88!r)a$nz zQ;;EPuXnzh?7a-zWapOC(;W7o{8+3H?X;zQzmc?f^ET<7x!v#<&l!iFK3WhlEAZg3 z`#mi9v4=so2rn+S74AE9ji&q{iXkxs;PvB?LGyfe78PD^Z6jWJB@CVE!s1# zHio+x$!0&&RB^eTroSWAmdqgQf8DNQjV}qt1Nv9w-awNIwP-&JV=9U8VA!o0T|vG{ z`kSrzLY2!;&4oVxw;N5SH&pZByXP~Htc}$S3feUti}R>sU+xORuN_z6_9bs1T3Qm2 zNi+KJS3YC45io7URknEK7HM&5jX0_Bt-K`sk@Hsd;Jid?<&Dte*w}WXOJm-sOUFnm z>PcTL9hq;Ff@2~*f*;pbeUL#p+l9nIo(2gZwM*)GxrTW)Bj66jU_cUDYz4XoiI2h4oy5XZV))=Kt z*nI4P=Psi@|A_2>usYEy9iiurbjz9So7EY8pI-YnG4Ur6@}J=o!^nZLmqQn9ET%jbY{sStl1k{TciJW~ z{oi-;*D^$I$+`Z?w2`a$Js6#f_s9RM=NtC+@SKSI9tN zn_I35fX1~o?~7Ea;!_X9+U_m0Hv>Nipri}+#Q)J zOQLu)jz-Pw!pN^huyfJtxpuRl1qdB@3|LcY9s^nO28L6zhh^Ni*&j~CTxb$oJcQgj z8%Yb>InZ`d={jrxDtoY6>I)N^En&7J@6qE%Ob1xk+d|*{22Fv-)xH;Ox@P&|Msepw z)Q=WBgNN>eQ|-cU-YOxYoCEzn7B(G~O|KX8JMSMTft)Du!$X{La8C`Q8^yc;Mjvi<6` zVx3JPNKhIjRsNUQhaGaR5UH{xXy9v)*|c9&6K0*+)Xpo=0AJkmYe}Utn|X~bfo(CM zV-H_N4n1?f{O*0jtA2=CcMhK-U^o@l15-q<3VVc?gN`F+ErE^K+^DYlKKisFOSr+t z!IVP(`G|D=$?%ikf>$27k0j@=hexLuH{0W_k(s=FkLk5}dWJ=JPh7Of``2&HE*8H} z5*oY7TDSA#FGcpLZRYFVmP&tX=BwOli@?6_HRv<~W>|n+8amu}@sfHiaZNKKz?1L1 z{b72MG0NGy6eV@9kItFT*em(w`U}+zLPYubGa1uG%F<66H0MgQmD>xyAUn20VQ5UR}Sdr&!I;&nz3`$og zZjLlWkfr}R7|)BD*;~XqQe%pZ>=PGlO&QsA)_hun2jFVxD_2+5fkx##mzw8 zi#aEBVN}WtW?Uk&xbs*xYl$yKQrY+cIiGH_n2+c{i*=+|jX*BU)b|3T)uPJXH6E|G z7dCE+&Hs4{`tzV%u&;!J_lBlQ-|xn6ZU z;SkLJ@j{(n$u=zdKlaS+`2s(KD4SxnO+Gi}Hsa4tG$Uubu!hj4t-_%A-<6NHqzIq= z$-|hxY{N_0PP8fZV7*swDXy7+`K!|DGJ?0%l1pA4Sb9KfjPW}9lxLU)lro+Rh~gcWlrasZ|eMAYz1 zz|J7i6yUy6HPTMmt$fHHuf_n}3(WSkeXYvQr%_6nT|u~n5(@zZi}A%x3`h)!CqD*8 z?T~#ieo1FL2F6H5K7gyUp~V*kIVsac4sBS$+erwrWEMRZ$xwxKxX}k|!wVbFiuE!U zEsoq8kXbx0n-5KRb~gAXtz1fWf1I1cS)!WkY2}Cdf@Waysas-s&+VE=CXG@;!0WA8 zX!ochhq!e$@MH6GqAJqdg5Ol&+||Ux4vbwlE@>n=cj@vW%^IN=7|yX3+54xY%V2c} zJ#beZ{J1-SJuD*o=1E53XJ@0+Dlvn((IWF@L_!5IZC{|-sdS~ZS4Y*p=pvd;WfTzsEQi(q<#; zMR^;1t^oMSIH_HqW%M(+%xNiCHzB6XY4Pum8fqO+9Ge>(daq_x-~jEc_4)y^!#lEE zf-EMC6UNfaw(@9oEbO3v4|4PV8=D$LWUc8dz$ZZUyhOF;K>o<~){`0IpAwj+9zDMrM*^nK8)R<+> zQ(E&rBR0;rj4qA5Tq&fBWdHi|D4{~lI`E^QFt<{%tZ2NYV?*!jzBw2}Q{~!^_BZU7 z{Qf{q&UHj6J1EFDZv>+IeKuqtrpR1D;a_+ zDZ+|K!pg5rkCML`e|WK64G2A%+YrnvW|Dd6(@SYX3!&(5nk}>bk%PD2(}TY({iv#+ z<5c!vuK&5JveBQUEaVZgS}cWm*AnBmf&5WUIWlLs`lqSzY}y>2BuDVt(7eG{?rTZB z_5R&!G5srG!LPZyi=17k8#r%}74RP5ZwTKzGKShM*su%O7NAIx4#*H~6`K;57pjDk zBxrRRfeFl4=#ydpih(~74X~C?gU$ip0HGjRZj+kXZQMMh8gv1u4&{plf;2#fKc*{6 zifq591*3wrnnS_+efZ=^7qRk|l7+kT8K0hUwhgpJPy5kU(o0Sb)iI-)CL;p=8c|de zM+T4}v0n2ABnh}oUSi05i++r+>P@lKgBY3J0B?oSfEa(-0tOm@iQ9A!{;`EW$z`|# zuTz4r71^fBT$j$XuDMEIq-#lzYtKj}XHc%u=|&fTs$b<@T$H(lb){urR}L(;(0ru=UJ6H=e; z6QJ40gUQMzZ*jKj6bbVFa)5i4KE7X8hTH?!;!V?);zT3O2ythJ5wX0u$)PweTcH3reAfITq1 z|1-r)0|DOIt*(=MnRPkExj)4@T&_HUEZinxE)|8=&+1)f1yq=5uZYQCrA1nIoj|D3fQ-`pI!v``L{sJe=ZU5+y~+rCaCWnN6fdx#*0~F$S6UU{`#V z>-P`La|QivbJN(*86u4#)9q@rz;`%0i18vRPY^Ko zgQqhp#EytF6rSH+w}~Is){bcLY|e~&VK+^sMy%oqgW(E#Ngh#)3!i*mHblKU_QR6K z$eQ;05w2z3`s%Xuo!0hPQtk2$T-v;T9#LQIdBjIS#uF@RzD3F6G3!GhF2wAiBC*gQ zGJOb|@rA~XK@mpZI<@ZFOy9wr=n=vIFO4SjXFo-($F0~SSX9rL4XEm%i+l9fG2!!@ zS$Hu&%ofecA|l@t13Ncadm~z}9+{gH#^08c$<8TjfplfnM;B;*0$$xecK#Y3>?WM# zMovf(!MX3*W?1yfBUcXnp4CuH>A$7l|8HLJzlyFp_&-OMliBLMU;dkA+U`Rw%#Djx z%H&^~#l}VMh{yk3XyFAb>~DsIYI-lzmRP(wdRkyL{VRTQUf{g>_2FFNfBnt>-4nn3 z+p|7q@`rI!E)!(FROVsCAFlORz7ab3F6K)?v2w8>Cj}2c7n6ljtF}10VQ$6X19Q?` zFt9{XQ!z5*wOQG+Cy5sF4FBj~>PZ4dXR)zx!`ZN5opaliJ@Bi=J4vO*r-3)H6wm+Z zb8Becd=*@562ovZuUaGrE!v`tjyYe3CqrSN8{&6AeNdgcPKDi+$!^cEu*ICjgyB{( z?baoy7ZE5Gon)wN`r=WV^`dO*DWt_>3bY372ZTcmfx}Q8uq*@zz6|aHT|NplfNNR= zn~EI;9c^+|Kj^yn2FG+K!G0-GM@QhEsM8n&igsMRs2ecn!xUK)r~^JOVtgfRC#KyU zfWpDwM?v`@dp*z7dg9MTMs&r%bU3l9t7uK?+*fs0vB}^1ZnM-6F>z4DZEf zITSM(-^$-CXKOu3=HbQP6f|3wAz`md!KW^nV3G$O*nG+$86nWpa%UZM_Jn^;oW#FE^r4zPjj=F!gO`r&?hzKWmC z^|$&nFK#qMdHOoqw0D=I)TW;73@(#C`wWn^{@hiUg1>BWF4|xNi&#v&GdFi+vi@?C zZ);Xi>Qz8p5QqNg$?(o+UJm#gtD8Cg!BbINg~{2?MH9zP$XJHXDKf0sCDv;{AtH^& zy5#5&s5**^wn5xC0{hA^HwuTv4clnpguKr6?FiNTT~VyVK^Ir3XN+_$ zZsV`cw;+C~^eoqzjdwz7+G1zEx#$L|Q@jwNpId7bThH?{ZujQWQ}*FcjNdovDPBKH zUD29S^3!H1A&L>*mAWpb^ADK4GP|4TCGj(#Y9ED1q%a(q+t#a%B|ruiNth&F-pghm z-0}I^1}W>lp_w1iz3+`a%v~aGx#=T=b#hq~)%oS^Q=*$89X(k`{18lScTuo4#1msz zjli=Zg~mPV6x&2nc;TjX*rl2dpBj^5DlGcmOK#`PZHs83sASbtT~4hzkb)cNK2gNd zxA{q~;R{OSy3^Pl5Z!{t<*u|I7umZNd`mia_%7?KCqX}k>J%=Ae9@Z#P?RD@ zYCkuz0FTE;f}a?#jnhsr`}=0u213!Yu?7=Ep;wa>FT>~e&?@3@soyhziaLfus%J}T}^4<8wC;C9>3us*#8|o zWblu>t<4nhmO{}pXH(d-ZuFShC6$H{AwjhBrvK$XnNca_zs;!A@o0m8Cy2Z*GJ-t9 zc^q#6<6DcG*-EbmduM~NXDX>GdiGt*s~C|1I*dYgqOZ>hs$^qm)NyD(3tIve47)n~G;f&o9Lmqnmvj_@wa%f{3vS!NV~d z8mC8Qc*{Y#C>dF3s+j%wCl&r;8|WdV4%i9o-y-?-jD^5drAh4Oet@L?8ItKvYY0R% zN;G-D^O?m)jH}T5MUu?oSg~)hILx{{T!(QKWz?xS7bmj2OuH)u>IT}P>jBsUCkZzn zkZ0i~b}`G(`mz3)eI!GRC#w5D`z-G80~HL!ZbbZg z9=@MSjMZ>a6TWZA^FX9#HQbLmtuBrI>g4pXB*6!EFOV&G%%+544|t|&8rq15ds5~9gp?^+G*lYO#ha;|hM%6bkEHNjWq zeCC5RzaB4&nX*(dmP%HjFX1aHGq%CP4S&fa6UHOqR<}jm#LtN4Y&U!$(%ojTBGLs` z8x2h^$DD5*#oYi&*W_|7M5nht*RYvB<1@P>@(zyD^fB53tk!d0W88V)fTz7#{p+o( z=Z3jQwfPW9@8)`7Pq1)#o*vT7kwQ4{k$Ix)uZHK^h2gj)!l!1?-tmpQGa`a-S&g-n3}Hr4^ep6UNXh=S2bnd_ z+*E(yNgD0$x{FcUkOyL7nD9w}`#e~Xp&_TXg*KV?Z2775_?uMDkHk3BR|qXxxOxh% zu{1C<=#;4%$uUV}{Ji%J>*QviF^_m8t629O`U#A6Xr+6AFpdrD&2^r%KxwVDk|SP} z6}TTbiCrH%ECr~Ptgnpl{?VV7CpvSHS9x;n&dwK>Jjaby$1J`1If?w4T}O=TA9HAz zJZh(U(lpl!z#iX9`2#gAA3U^<@rLUrBvSL#AbGQ!sLxK(qM%qSJJOOJ^itDB zRp*8Y^%^AKXll)%Y)eEs!Y*L9xiR_OMa_Jx`l}Nig%%$!wA`pu{G!$@ zhd%Ld8yP|!wz&5qLuE5TTgalph_${<@@z-GYE?^;m{ipvJJb3mN=^4yXA&W`@mcws<3>eT20f%8@+I zPnC_1rSW2dz@^=Rwz07gKEGt?A9jm@ma`z>^sxn)Ajk`E3zQ^kd-9%=4ETv4pT={b zR^SJqDs~H~3o-)COd}bWC^|J@9f6iJ51RP6VW^Lac#eeqFlq1Qyu1lw?bKDB5aE*@xV_5sIdXmS@V<`$Gik z4X`|KcfK7*Wh&7YSK{`+S%5t+EmSup@4&9K7%w-;_6I(m&`T~J&#&7ubaE1N+Dm3X z?1DtKXxFLBh5DWb4t8)Q?q1u%y6Z9%qh<6j4oi|JPm{HRoV2}s5haJfr^J&F%F4=L z30E4vfrI)i;K2_m_uBPk%Q4pM-FitoTD-QD!{5Bw_H^OK-w({)T~(?^<}1Y%iwV^1#^wOHp(81afHJ6RWuPLoFuw@wce z*UbBrBMlrjR_6K?KR6Y22Ay)Jx+3EDwts=gtBb_kI7+mkjT%z3?Q&>avKR>pdMF`RVh^=NKXL#wCU{nY&g+>Xr5Smvh zb`{+x>e}D z^Ef__?ai z?(qFUWQW{#u~TBJ2M;PB#ahoKCkG+7*h7Wx`8Ka2%3pxX*q0at&I9Yak^fXl9S14L3q{d*1EtBz*~^1c+?d^qtZLeV$!ib35~I}AQ^&0-8D4K%hE*74cOBG zyYSf*yDSuBK1|r0sI0+HbhH}1*`dDG29O=TWTB)D7O1Cu7fu*MqN!fFeG@4jQcF-n zv2;lFH^3TnXNxWJMcs!@_V=W!2G+aW+c#{S2vgQDzFG{)efd!c4Ppa2E>a`R*jO1v z+8#%q6It}#tP~sYbUWoyA6BV(`~B08Xg|^HJJN!J+|p(7DVvd5^9UhdH#fhhH8taH zao20J8#3cXgsUSYH+F=y033Z3(=l^oPUyg?NCHxcQE$RAhx#r?cX_epmn8Fj{pDEl zkyA$^hRXzw@;FN6lX0m|a*x>F|Wr!{CjlfpxbKV=Hs^lMEE z;&^b6{X|oY~ty#Nf1Grh~NefP0#V&vn$6GE`BDFza%bL!+-+`k`@rkSpEXA9N{=Fn!RgRLnk1@SaWL*xR z!y$&k{UA?;`kUv|S_(apbq+=UJ>aSk!LjdGc3VmbT}~gB1rs#!m5`1$S!2gg&@5q2 zoETF1@aNoMpkH6wT6Ns;)Ud;+h}(vTcw=vfbsYD3$?do074E0oeX3Cn` zUh~+n_q>{O{sabN=4yjVYT4TU25aQ6yMhNni{`S9Az$l-bQ(2N5vZ_29@#ZzoHqe6 ztPDk@#We~BpaR|gM=I}Ttf(ARTp<=n3@{ zcLCKud~k_4yfpKP82A4~xTk;la%9HcH^B2TKHX~kI~(oK1a~5Nd)9O}4SuAIOpR3k z`4MzULJNxk+#34u-xp;W+zX5r^QG}$O!VBBmQ8a(Yly+E$mZT(8|0PomU?>l?CqAS zk%+qGtJ-ts{zB2`W7(+5^K8?OUMl_N;1M;)>2sn7)=l)O3!N(=mc5GD!DCwS+kBGH zEN-#I-&(K&`X<{+h0L-kj_CV|?q9oOgMsD)q2l?Vp=~_zfGu4jryhtn*3MUcFG{L0 zr{Eb*sDnsv3;T}U$Yc>Gpz9H8<2e-pr=|E4f)dFQHT;Cr2VU95*aN$!2Q-L+zP-4LUKK0~~ka{F}U;MC%c z5b{FyjxKwnaB0g-eRzt6&#Z`B<~-!1i$rQGAVlc?Q{{NtVI7Zz$}w<{&c%Se_Stda zvD<;o)*KH~6-o>Ecj46qfH$vPZ2cPeuk?NQpJk8~<#z>~gBQWgMo-5A%rMBt7plCZ zYhGQ+`+!fH{{HBrn(?@@lh*!Z6tK8sHSurlg=gTEr*csYaz(E=V>^N)IVJKzO>Wqr zF)&cI`uSx!u{*~fOby34H@vC%AnM|G-$kfR-n)RKWU@uIj{S@ZbkU&1Im1=;+gt_z zkmIw~1DQsu$wg7)0wWFEj8|x3u=PnLT+%%!4%}24b=E+E#`nQPhhK+=?2?NNX2lC@0|A3EVTJDU|3 zmaYcRi|(9%Eu;-h1B zX5aijer^h5Z`D`QJ4fjBBeb!%{q^k4vvo;jquFPDfn~yEvfcIU5Bb1c-3KA&XfD;3 z;iqq(RALq4+jSKeEwuD+zb5}2VW4^V`bMVjlLkEr!xkbkj_QrL4H5-srCC0%78`gp zuC0^7Bh`l)jQRScWq<{K)*Kb*tT4qHmuS{waC=_mtRHMtuA@|Wdykfm>!gH04PYPD zd&koDLM+>Dfu?ZKUsW&7gMEF#WHb{IZ=VcuxOTEj$ISVziLoMaBoxMWP!ejT<9af^Og%X{s4feU@@ z)-gY`fm~CGLIZjT!l0tK|NZFp5Q#iSC z?3hCAyiD#bKKxIzt>MXUrorI6Ua0oJI`Z1SB4(^NEz-X(xreX-g2*FoDfa~Ug-G5i z*{_S78!y;gV{(U=O6}0BoG}hQv~HdAw|_XFfzFVEd=e##kBNNl0ek_zhrZUeup}J@ zGI0WtJg^89{uwt3Sp!~5OJQMKF$l_W5W19c25>HJ5_1Oh5)ci^1Me0Bl_k#sI5Rtd zq%#!t1n7L!0tGmR%|gbzzN+ZEo85`PoUpOK7;vmF)Z#&h(a%dLPll16c*j-Y|DKtc z93)V}^osdBu>2~nrf{jtQU;H?SuW217@?{&U=%FHlgZHLcmyCe2mz|MC6l*ct`LNu zHfUoJpBpr!D>l%K_0|pn%@EtR{nY18U~-dEK#tM8{$kPw_p{SJ9TzxhC2B_kB_;6K z{UmNJbq{9T#m^e>oaU8hJK%NxR;Q7g&Z#1ZE6hp3DTs6iRwA|lKh>yR%a61a2qd(boL zfpNFIU;s_oY_4kcv(Gnv0CdDPJ`+AET}pU7%3{RIDC;@FkY}*6^ZVer8@Pl)bs|pc zy_|*TrhCzI-9FK|udyz4G3~OExvC)A%#x{(vD%c$M(QScT+`o3N`F6X)~>N5|69#S z27ME+Ss3IC2x^=_Q|5gM5kt^xmUTDDpYppEX7J)bD1_q=#DIhSXUL#O`W zN>AfyT^rN-*oW(7p$g1n3&3cJx=RR`P|$^D)JLmMBa3t|m4Kefn$VapKKe!M$!7mg zWvfy2jgipyJuhh%o1E*~^QYaGTT0xMk&e`k7`<7`&90P6uR)_vGy3)Y_oJCD8g3a| zM`%4}9tzl^S+^Cp{+Le109IWK#OoTkU$j`W3UqR&u}?VMa@{h9R_4g-#8CCjb4h0B@BO31)O_{lok|Ml#f&0qi4sWDZh;HJ<7mUmP zq=mGdMSj0Ts?+!nimfhqdzesNqo*5aDk}VYR{m!?==?sX-Q>-`i_L(d8?i6duT=-G z({=CIv<^f3oKc_eO zxdD>(65gjL^sJ3FWK0hPyqLryaac?--eVep3iY1xl`qmU=a& zUcaeX3U0VD;DJ26C?~W(-Z?2-V|pP1;ciljNc=1Pr#mMZCswREJDhDx?X>|GQtK0P z=94=*I!2KBs+S*qUY2}5o9G|)3tGNk&ZSgL|Gga-5|*u1_^T%T?SoZEH6a-hnY30n ziBRJVL!^Kk7tYSzbzV};hL%h#Y3*Fp4S3GEu)=oIDjU6I6J{GVZrk-P0Y$<-MLwMK zkXFPvlg=Pd#$^~)bgDbqX35Kai&-E9#+so{OZfpLx-FP#LcVzit+Qd9yn4VGJTgXl_ z=I<(|KfA@t9pt8IST&2GI$VGQpYy{vPY^R33)RVA>MmA*Dw2i{Lb0EC6T%^ z{7Tr;cx=yu$x<`sCVL0x@8f^Xi<#Q_ucZ}H9kYTgI_ma8bg#swI;z!w!+z>EA-#_5 z#jnXYcBn4e+DTD-%?K$v_U4_xhTOW*)beZ44u}^)$J3C*f+Yv$$r>M zA8l<>c+HKc74d^fGiilEhmh|G$2Q(=vWwKoJt0W*2*@?IyZO&li}SPK`pNg~k;nz3 z{X+`)*_$84pqf?97ghWq4;FX}bl@a&LC@Z2=8Nna04J<(D$R^lc&Fm}OKHkDlKKo+ zZT{aw{r{g|1r z8&-1f<^D7ei=3A!-@u2+ws58FU1ej#^}Kw5ziuj{TU~vCI61`;_eRn{^{6b z%sMd!?+P}1lcJWw)@bg0vazomd0vPP#7yF8nmpPE819It4n_fUTGU<)b7`Nm66%Z5 z@1^}NSqXW$LMb>TV_&sm$?0T7$y2Bsm=4-odaM;^m(j#q^cL-5JhHYnvaY-4r2 zRL505#SoZqefz;=f)I0*V?d)HLE}M!L(-IU*l1_j-6cCWbT}iVLnRKF-Nu8uishOT z_7JUu69k#OW*o&j^~t7zX^Q_Sh~dqIn=S8UNE%9;gZ8+{A6nh=p^p12fKXLIx6G4ek{?jCe*^LCLcCpIOEl}Qt zFKo|NI^5zWMz2qK;s6?!HHfLoyQr{CG;siA?yCa})im|UxT z5BTAsQkI;f4EUj+FpJw1p2Vy|w2=4X<5BAb+%qOhO{h zC9ZE8F-pr4-y}t4--~fCY`#P*R9PRcdAV~s$AI|B3T!=X?lmNv67eD(OqBo}&anWo zN>w)l3q(V1dbs>Ei4%licxuAdM8BA-Zo=qor;k80lDAzYnF+$Ylg1Zsu%q&W7jA~+!boWWz6e+(r ziCuAy6n{OP292!bL(28ZL00C3O>9nYP56B6nkn-tOH3he{v^XVT-Wm<;dWoJR=Q%C zHbdQ<<-Jr@wR)vDgsHmFZ-Qeyuvt9Q(6M%wo9q#I=-_L2b!Ng@?v4RR$yj9jF0;^) z0jHrJ^$4{R52jx5xCb*+uZqm*iz$`{PvjH1MMs00bR)=_2HCZ#gZ^tnc8lhYb70*_ zaw@YA7QEXzg&r65HuNt?Gm)J+o=nxiy?Mp*_vbq_k2t9D?%1SOhJzEng|OwBssym< z*rcf#>lGATB=`RJhME9hXZOb%q5qGA;od*nwqxe&9ZQ)+vXIE`j}fwyO!TDC(n(02 zSNPXC>357a>V*1<*5=DENdMtTk*f#MRC!DRcPiyEd8JgF{`@4Fl~`;Vvu5HRv{+z% zJ$#g`96nFp8}3l%7PSH)W#p`RoX9p%Q8Q?_$ZfAlku9L* zuc?dXq=U(?Me`ofGy-fGraMW{{Y?OFtJBWa19{u46T%e<^;4jD=zcJDTi!j93Gp$= zBBKpJa}pR!u?0BcMpI!K_g=7lo(PJONoqBVOts@2o11874`uLX>h`X zkA)jYOH_j-k-7t!#=&-ryjdx$;dseVw1Fs)`vXNCDT*zr1bX&W_`=#Wlmph&-Qy`Y zPEBN-*Hs($yA7y-KK#_qq=es4bCy-)2|a*FZ-iU`EXwC&@t6ZZV`*Ece&bg|)a!N0 z!)@(l7>ng?Vex$$nxI=VU|>>*ol}s3u>DMk5UPwHk>CZj><-O{3*kL>ywR?#Dyh!N zSxN^Vr;C0_ zTZ_y*Qk_r@0R?N;H8ZE>I^;dWi7QqhU7%iFYx7WMCmJ_8(E?zsfJd(<=(v8H|+k3Dc`5+8vh>)B(sSp#-3j_zdz(&I5NwW>S zQd)^-X=OuZoEqsI^};KlB)IZHEoXZHHSDF#%vZqfSMVwZy3u{Qhv5|MLfmq+jeS|i zZm1kx@5}C72~7^tTz8M|VGi_Niw=!>lgnhiic{`SJE6N--LOv;c%xT)%ESBP%xYk| zUdEH&R8t4@@#8+veIoo1Yq7Vk+C*LB8#=@eeU;=Dd-`)b*N}JFS@dH`ys6DtDIJBV z_iI~@$rI{Z`$x>3BZYt3);VK?buZ#C)RhW9nR!}9Z^n>;ooxNO-6oa}o4Q%gcri)q zHc5V?ull@n?JPfm4$)nyHD<`nKg``Q-YE2QukcA#-;&K&%6rU`^k{xT(DO~L4OO7M zszNLY?}s|zH?79HMw_;@Sh|MB!`-+Jv@7^s%_(*)Db{rDfAlZP&A$lwEGqwI781j5 zkh}1K4=cPqkqb5o zDy&27xYRZB$MNS+XGMv)Df%Bb`j=o)x5dgVT)4EdHY{A~7h5ok6}bIoDT|bxiaD1G z{rV=q(XBVWPpk2deP1hz30;qxm+No%aOYEhaKu+8(VTl{{Lju%gVyiyE2OPb=8wy| zx^F3EP{Bs2UDP;7Q81TaFuvf=ttd9-m(>zIguQ|~JL?YSZO?<9It9M`YcEPcr}Hic z*oXPJi=qlwZL|g)r`$yzr?}uIO5iHk5r7@;Kq=!S_A&AjuD4XqVhG~>@jT}8L=X>f z8Ti^LUJ+zCD5kRw0PQ+O(kj`?pimA14r3o&m+yJ_azZ-aGM+ulXPky>u0lt}8bAcl z{f{8Rb~#Fl*JrCB(Vit!vI3z-z~8X@IWp4Ih^Qol8B*1%2LTAOFT-(Q@%M@)_V7EO|RXt2%=TJ)S#l=) z6(K4&jfpEjTgi8%2z!9i3dQv5K z2w%pv5QTEjTaAUcOvCs2eN*5hRl?CB|CDU9_>6QB|!WWMk9l_>w-j+^;ec_sz7qona zNc`BM)mOLR$1IHc+kD}WR$-I`Z@h+q);PE=u^Aa7x>w}Lj8S;IU2-1kTHhC+4d~eC zWfS-T!?dsz^NEb5a#h*gvo$EE<*Oe>5o(0QaAKHaOQCaLQcpvMZy0s6u`xY<#}~{K zwmI?Dk=k?CzV3K7LXoE(Faq&ierNX8<$@hdF8!@{gGkGj@uAhA(gE{sd(6Qs`2fd7)=EdR4~qe-aqzz`8mg->)V)hOKFA>ouxh^tm5mm)>|r z|D{b-xbt!aVSgQC%R0KzZu*V;L~DeoS{I0mjV^L;YONKfr^f47Ke^&*%TWzkUy^PN z^M8AN0?IvF=9_U>=7VyH$A&+D;DgwEsl7-*KPG0Gf{;Q~2c1SE=>D=NxjLEHk+Z7S zk;3)`Ct~)C^lY~kEgmz6f9S)XcGCWQ0u_R2HUCJRCUaiL62*ohXxI>fKju+1jKHlv za5zwERY*TinikeF{-l=bD8IQP2~*_k$iKg2@x1--8LFN+scgirl8OGAbA^}LPafq& zuQjk4PUx=XCNyGs2z_EX=UOdiLRd+byggVHo(-Hix+j2gfU#F2@8RrQWMufF(5rM8 z>&FNeb%CB~CE6I-WI1{nMyN&|%tI6a@W#IZw^Ze4mA0`j9Cb0r$m^VC9O=tI9mO1g z>!5U_u5e|;T2^2Ta2#mH8}qWO(6b((3pj7_5>rC~gQW(w4M*e6Rx`f=xZ)4DD1dK_ z#>5s|l&J!!F#vneipY`GI?rgN8tF)68p#wT7r9er7S}9KvhCYa4H^M2d_wo>B9BUS zk!6G%5V?&lC<~H)V~De#-6?r`;Z>VRK&>C7SE5i4c*#rj^A#H41Z-rd#*`A@Dk!4e zV58ChLa}OslwQiz{{%o`j)x<~xu{*QrJBQ46biwZ12i*-y^t=wCaoMP20eExg_~xODQiww?#bhm~3d;%5WhK ze!ehFH2X^DE{dHgw?_vpTx9SSBqD8>2b9X>z-}*fXAX4zDx@hkm${O+^6X;TjV$d5 zC1)aMth?1`r8G$WcmK9YP2=R*7;}aPKYp!t-I+DxQ2g40qFHg2HU{nORa1+SF;!nY zbcs6nO8HWB`);G`V=grpHHA@P-$m`!K97v(#!KTl1f}~4c%=Cziako*(l?@tDjrdj zo10XsT))|y6aCO4(yRA=hWM&z(avngn2=jx(yPq340N+<_3MnMv@*H;0(b=0V^$Ej zzockVb?m&^!-St@y0wi}fpIkUHyx{rZ0&;P^_-|KImyzU@#_zj{9_Q7^Nz4p;R+^o z=$s4;1XXoHlk5O4HD7DrDD-&a_I_N^*92z3G5x}Y0-hVYg51eA zaUz>UH?R}@Jltx3`Yo7Fucfd9|IA@uWZcXAU2Y_Q+gbY$4=6;nf6rS)^i20JE4r}m z_3N4#x>NnJ3ZFkI%{RbHG{E-9iR1u1p+iTo>L}$DpulC{sU7ewC9RNDpg8tDAOqkm z;t!Sq9s3yzY=y~+1VfzjfPm#-rw#Bs;CJZX%0$VxgJ-n7s<3Pu@!l`#cQ# z2skZZ733hOZEIP%SYM`Eo|L-(m?foXA{D7g4iYhXZKTq!f)tkhI$-mzQ8$Dg;a-|M z8yi(qB@A;fS1IQ?Sw`D1Jm3dP-)LhV1N*kuof@7}ykcYf?PG~SxNKdkF^n=ch`PZ` zU)pXJDGRG~^?Zl0xI*|l)dBC?vc*)@fOEmEGy=J}w^T~0#g>vfOe{^^q(lH~JIo_v zdG+V~BUAz2K_e62ECG4fn;sHBY<21{43Fq~BA5}s{5G?4Ml2ZbnsAn~*Q`g*h1U8O z69OA*mSGiDk0mgZyR3IMG((aCW=G5 z^A)aOD{!g#--4kEPGs|>X$Rq3KutECZ@woc(8g@nvf7qiU#f1Le>9V(2NH-csm&P^P_fZQiIg zdpLY5Grma15C4h0IvJ4@{)IIy_qTDGa~C-R!@5@JAAH9gRxwn!yvyOu5Bpok~Z>yUlN`c++z-o zUl%3>Y=aF|&4}WkEpx8|A0HDHn0#@W^kmwu=PHd`+Bc4Xn^6SX=KN6{ijk1==7`A%K zojIY0!s1v8bw%|!WZgtKLB(!zFBRSsxVg60-~Lnn?Y>Q1gU39ub>hd^zdyv{N5_9$ z;9)ss6aM#5G&={v;WMMSOCAE@o<46z#+AS+wsPMnJ~h&FQ;r_B72XCTsaRY?B0(2R zv1gG8z(uO3f`RK+dq|zg_J<;_&}7kL;J6(ZM5p#GN}B*97r=H$kn2b%KMO=5v8vS~9ZkxTLDo7i5u%tLEMr?oyvRXl^=J$#gW`fY zh?V6Y0a_EFO8OQULgop$-XeShAno--g5ZN0v&PuLUwB!hkhJ^69DymGz}KKh*a|=a zB>lA?T-g}NUrfN0c1EV^q;T;@lL{0=iv`ZDBGFUUI){U-2N;i3Hq`^W` z#=aKueqZ$A!(50<>wv>JkeshxbJ!qu?~>)Pg7D4#ih!|0riljhJaPk^aQsoz?z{30 zD`K9m(*pmB)D?7ewBlxzP0JY>ld3X#1!B}Q6hlOD;n=%0Yv(hfi109>PK;)~COoavvbC&6w>2_*v z%-7(8kgM)@;y1)n;$Qnj9NiE)`#>;%6+Fbi8zc+dDQv~~8C(m0kOX!D>mpoB%~=7t zy|foT^s6K6cz!&`^tTCcraAjTZzJI{+zr0Na`~@V!m|ZCrZ5@OBYqTJg4}F)-yQ<&eWrqW&#yLXqPNwK+%m9U z$+vKlSp}cKO{OUzw7pldzSyy%5Rhw&A}kjVl`Pi39E8^HHSl{ zj17p~$jq8tJ4!FDZy)sfblQgfpmP&fJGBrzE65>ih2{4P9txfd4`g=SP^A)Bc4VG3 znVAU}35)09-y>;Q9{-@Q(S68^Jl{&)Lu zWUFJXK^a=uhh)nz;bHffkr6L-dR3WDj4C#eC`t_n3=AR;WowKotuVjf_;xCp z=3*HCU109!Ew+$7iySyMn*5u@@JUheh^u#s7I#VQGU37U?CN}S6^mNZaiZ3Wp16buCiZn-`ZTltTVHE@44{DBRI2NntRGPS7jcF%E4py zV+wW~xK7oFG9P8Tmxe3LbSzX4eObT98>5)h%dY>$+Z)rnPQP z=F32{2w6!N5=lH-FuAk9KDa65g#C=LSlp!f1>N9Jp#q}s2WaDh+(DUdF$pYJ!3+C5 zc*hmX1<|y9v5RzLW!Sr3^0r-|*>bsrOCg8o zZRgwTpF=XOXS1tVa*3D-ua1KZew!ouZ7!i%el{-y4T)wqZw0%>!pc#HdtQ&I88&TB z{idzuCc4(qx$;$g^<$?G(PZZ_c%FVmy}JVV0g}qW^w46$%JhTOaC6YYsy|yh)_@-I zkL0YB#GGPOz*UcrC60g9ey4fr3k@UMN}K^jpmOsm`LM1ZIIXgxR!05{CH${tjrgsy z24YoOFOc>Uxio34y>=8`6GAH?fv2Qiv*Q2qj*Zbng= zZw^eDhau*c8YN%R7URRGD6N)VfXd`JhQK&lDS_Nn*T*jE+dq=pZy;G@e~`bPz0SuP zv&BIJ^muezhnWrZYYH*$vv^t+Hz6oPuT413pUtg3Y zn0^{$S^DID$Aa0RWZK5$%>xYsXSguN;W-8qDu% zy0^=>87}gj!oY0tahq~No6UYq^p$-Z4FC)S0>%D zTnV1fBIh;F{?0gH=PNsqys>HxJ9!eDv8cbmyo;cF+TE(u3-EOP-1~JnSgY$u`9-sj zT-3B`j}e@gp1IV}Z|=`RnaA4G)YeZX!``f^Q2Ug3tbK0o>Rm1{)LYWF%GAt2Go6(- zxT>>}PCzMs4d%%)Mp(G4AvOE7@f)H1^Buj~?{os!;(ecFkYs7R!e6aNs7%+}MZ^53 zejymR_8uBo3nQ%9l1f!xI=Va+JH|V#tPq+;s^K4%&n!i+N;GJ(_sCOxNZ`x}Y)awG z>NhtMf96|qOXw)}!^S?}uSjj3UaZ`D?SCyO{AfXzhj_NL_r2i$IM{iNOvvT!JMrmq(bR62o+1@ltZ5XZ6^59j-Of zU}}E6^}J6`3x)1{w8L)CsZZf6%s3_)j$pnbT�_!&)MA+CiiHUueaMa(omeP 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�XZHUS=j=<`IpE8FP22nf`v) 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¬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

l`2r^3_6SOca>AV!4wHqzBoQoX2c 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

L$ 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 A?f?J) literal 0 HcmV?d00001 diff --git a/docs.overmind.tech/docs/sources/aws/aws_source_settings.png b/docs.overmind.tech/docs/sources/aws/aws_source_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..f8f38128e493bc53e18a7d9cebea1858682b3199 GIT binary patch literal 116537 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�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 literal 0 HcmV?d00001 diff --git a/docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png b/docs.overmind.tech/docs/sources/aws/cloudformation-update-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ee8568659629e17ccedff58182523623b3fae5 GIT binary patch literal 412841 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

H#vZDbvWyCJW>3%o^E?L)fF=`fL)V| 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�lNIWO|ZGpgC-PTncD6;bo7> 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 literal 0 HcmV?d00001 diff --git a/docs.overmind.tech/docs/sources/aws/configuration.md b/docs.overmind.tech/docs/sources/aws/configuration.md new file mode 100644 index 00000000..9d2c115a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/configuration.md @@ -0,0 +1,118 @@ +--- +title: AWS Configuration +sidebar_position: 1 +--- + +To be able to analyse and discover your infrastructure, Overmind requires read-only access to your AWS account. There are two ways to configure this: + +- **Temporarily:** When you run the `overmind terraform` commands locally, the CLI uses the same AWS access that Terraform does to create a temporary local source. This gives Overmind access to AWS while the command is running, but not afterwards. +- **Permanently** (Recommended): This is known as a "Managed Source". Managed sources are always running and assume an IAM role that you create in your AWS account that gives them read-only AWS access. + +## Configure a Managed Source + +To create an AWS source, open [settings](https://app.overmind.tech/settings) by clicking your profile picture in the top right of the screen, then clicking Account Settings, then [Sources](https://app.overmind.tech/settings/sources) + +![Screenshot of the "User settings" menu, showing the first steps to take: Click "Account Settings"](./account_settings.png) + +Then click Add Source > AWS. + +![Screenshot of the sources subsection of the Overmind settings with the Add Source > AWS button highlighted](./aws_source_settings.png) + +Then, use "Deploy with AWS CloudFormation" to be taken to the AWS console. You may need to sign in and reload the page. With the results from the CloudFormation deployment, choose a name for your source (e.g. "prod") and fill in "Region" and "AWSTargetRoleARN". + +![Screenshot of the "Add AWS Source" dialogue, showing tabs for automatic and manual setup. The automatic setup pane is selected. There is explanation text and input fields for Source name, Region and AWSTargetRoleARN.](./configure-aws.png) + +Press "Create source" to finish the configuration. + +## Manual Setup + +To allow Overmind to access your infrastructure safely, you need to first configure a role and trust relationship that the Overmind AWS account can assume. + +This role will be protected by an [external ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html#external-id-purpose). + +To create the role, open the AWS console for the account you wish to link to Overmind, then: + +1. Open IAM +1. Click Roles +1. Click "Create role" +1. In "Trusted entity type" select "AWS account" +1. In "An AWS account" select "Another AWS account" and enter `942836531449` +1. (Optional, you can do this later) Tick "Require external ID". **Note:** Each source within Overmind has its own unique external ID. In order to find the external ID for a source go to Settings > Sources > Add Source > AWS > Manual Setup and copy the external ID from Step 3. Do not close this window after you have done this, you'll need it later +1. On the "Add permissions", don't select anything, just click "Next" +1. In "Role name" enter a descriptive name like `overmind-read-only` +1. Click "Create Role" + +The next step is to assign permissions to this role. To do this open your newly created role, then: + +1. Click "Add Permissions" > "Create inline policy" +1. Select JSON +1. Paste the following policy: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "apigateway:Get*", + "autoscaling:Describe*", + "cloudfront:Get*", + "cloudfront:List*", + "cloudwatch:Describe*", + "cloudwatch:GetMetricData", + "cloudwatch:ListTagsForResource", + "directconnect:Describe*", + "dynamodb:Describe*", + "dynamodb:List*", + "ec2:Describe*", + "ecs:Describe*", + "ecs:List*", + "eks:Describe*", + "eks:List*", + "elasticfilesystem:Describe*", + "elasticloadbalancing:Describe*", + "iam:Get*", + "iam:List*", + "kms:Describe*", + "kms:Get*", + "kms:List*", + "lambda:Get*", + "lambda:List*", + "network-firewall:Describe*", + "network-firewall:List*", + "networkmanager:Describe*", + "networkmanager:Get*", + "networkmanager:List*", + "rds:Describe*", + "rds:ListTagsForResource", + "route53:Get*", + "route53:List*", + "s3:GetBucket*", + "s3:ListAllMyBuckets", + "sns:Get*", + "sns:List*", + "sqs:Get*", + "sqs:List*", + "ssm:Describe*", + "ssm:Get*", + "ssm:ListTagsForResource" + ], + "Resource": "*" + } + ] + } + ``` + +1. Name the policy `overmind-read-only` +1. Click "Create policy" + +At this point the permissions are complete, the last step is to copy the ARN of the role from the IAM console, and paste it back into Overmind, and create the source. The source will get a green tick once it's started and connected, which should take less than a minute. + +## Check your sources + +After you have configured a source, it'll show up in the [Source Settings](https://app.overmind.tech/changes?settings=1&activeTab=sources). There you can check that the source is healthy. + +## Explore your new data + +Once your new source is healthy, jump over to the [Explore page](https://app.overmind.tech/explore?type=*&method=LIST&linkDepth=1) to show all your resources. diff --git a/docs.overmind.tech/docs/sources/aws/configure-aws.png b/docs.overmind.tech/docs/sources/aws/configure-aws.png new file mode 100644 index 0000000000000000000000000000000000000000..e2548560782e044e39fd8df866da3c10cc2154a5 GIT binary patch literal 56460 zcmbrmWn9&5&@PIK3Mhzxbc=u>ARr;#-Q7q@cS)<1bcuul(hbtmD&5^B-5@P}X8FAP z{Lb0m_Bnib9$D)jcg#I=%{A8$q#!4bjz)-vgoK1HDIubSgmlXi3F&4V%1!tr#ang- z{&&k!NKyp_1!Zbp{x=d5Ig+G^pvs%ctr=Gx?A67)e^yz>J)R2xP_=tY-tvxf6SrzK z&v{I7vrlz1M?-rvCp}!vdb4t;zJ$|W?ct-J48gZ1JOZew8u^QQIAnz!;)4FTT@R=` zoJ>3R>6)FbA50(kMN4IhFofNX?=Qg`BDML|c>@VaKpaO@b)r+&<2z%l!p5yg)b!8x z;i6Z!V0cIIrq6UKlXAmHcK%#bNnEx*-L_y|Q&gp3QuAkr?>)cYpUlpgV>g?qH*HQY zFA9s6pmOISlMRV9{vaG0GZKjc&(C97q>}52k`D3aOD9g#O>&ouv|9W)+hv)wiiwPP zfAXlg%&z%h|8hl+S_*2kD$0P z%4z#;_I)})1O*96A(ntkdOh>oJL*mRB37jbsDqf;DI*_W9A*$u3Bmn|-FCiXgdqf! z;)HY}8`0$G1NSsEnQz?|UtATXQh`b6;``uC2}Dq+sdZyw*B^NgmCHxE@462^;ZC%> zA@T1Sc{11YlEs%kVqH=0dOPB^$hmXPS<$OW+ZIQlB4M%xd-Zj!G}PSIe~I zjZ;fphU*Y9a;r_uTV=r3!&|Izw0C{S0=lAF$p&7>k%wWDIe+o7zL>ZApI7JyDM#pf zX?1rMSx`iazJGCu7G7g4e%}O2>)p!Z`J!mUhMkq~l0%1iQ7EOr zc-Mp;lnESO9*h!peM>U#siUjEZ(->uJWM4qBaDAgB(COYd4E7Mw=Mq;j4n?yqKkf` zDkN&8jzE})xZ`XceM zB+^^Ng{`Qz98)R0XSDB0e1Ub?zt6F%by7`zXWL}hJ)K!N!|?quFQ3KCky-i2)A_4p zbhzf5;g`8s=3cQG?#D;?logVZT&>~(L`V3{vJHn3-t;irocY6pTF*NgU&R}53}7Zj zow-x*^P>(@?J4SO1xFx!-IwfyEfRlc3iIj~z3adv5q zzwJOZWl?c2O3PK)LM~F3oo{3LR65O?j4C+-F$J{HIvIHrq6*T_MQ0RBpOMW!{B6G| z?A*#Ze@3iEM3Ah)jS>fslfgGbwHLmxS0?35=a?aQ_xsT!N=+JxrWWzai2*a+X!rFX zZt8D?v8;>%Tpjv1-K8MsCh8(0~Fi|MVpgiV>awrn=J7MmhqcyDnJ zzwe~mm!@yAHT^cb!0dP-Xt6iMRI?=NKSs#GQmk*Jj6S^n;RPxZ(kq!e?&M1P_x7H< zVJOA*N^FbuY0mt(9{$QAnE0HJW!>HQJW(}f*mn54blWc&@DH^7ceW&Cp2o@4&OW-g zg);(>&A6Qb|Y3 z7{-K4A7EDODTWYtTrf*j+7irS9z9_b%7P7gCA7&Wu@V!0#Mh)1Vwd~6*jqTG)kY$0 zN|Q>0w^h7SUV!CktIJtsYgKpG^XV?MUr&&bhF`er+>chho3XR$SGLV(97~cAN2BF0 zv0X!PEg9(^8X*$MnX`XeRMD_Rg9CgP)-WyN8l(xkI#lYE|nR`lZ#4i*+ z3`deX6E}`ZdxEK9qWORN^a&RiSBFu3wGUyCKMv{W=#Xy;YHB7VBoM@PvB3@Y&$NZX z+5N2;W%3iEi0i5-D;w$(zYSL_jNstns;Q{NCnT)nQ-<+;>KU+qD6+Dy95O53EDjgs z%x-OMeb*`Sj7Sb=!dd%nXJ9h*l2DpeA1$h*#jMUDS4D~+lN<-_AsJ%VCdkdI5vuH?iq;oQ$Ipep5DG)jLnYmJy^M%%!W=QJxyciBXwJxCW=LiQhFDE& zEq~aWDm(1Ti=yTi=B;dz7r?I~4QovLOZ?&tHRoY#+D|+uXH!$3s1fP*=@6U8^pk)K za^4L$Q>?s{l4V{F(+Xq^cX4L6#XO)5`wO8JiS0|GS%#=jhC`1`ub!scDMM(ouL@_V zb==dbzaKEcu94UxTg@`|Pz4y0qlYm9b9z1(eBxGjyNtCK@dRr`Qux6xk~fj_P5qKU zwL#U9e27otC{?vbFKjI6-|Ye#cc{Yp_)2@M3RG%HGg|XTiiew?bLP!qAZ|`Kd-&;v ziv|`+4Fx@Yhry9XW~)n%WNoZ2{(tk9coG$~ePI+v5vIyvY+}M08~X}>@rB%1MIZkU z5%2&cmJP7}XfzMhbCI?`w78BXBjrH`f0f<1F)3l)e&5>PnW=T%on@Zug^4!PJvHa} z)Dygcoa+%Va;B*8pN%cB+f3(R;?C83RM{_z%~*~uxQR13;2fw@(OcdTzhv(2>#4lM zu#&b}^zGX>o0%F?Qc``NOSYJO$Cd8AKYuvA&YVnc%YEUTpPy&;x$-U6QN8vDh7cQy!{YR^~B?p^ALSr(=uA)}E`*2$J5(X|*g;!-M`<;*9fYf|(`HV9TU?7bo4^i{L#~c)w>T!>rDGd({&#L z+>oUvqda;<_UN0G!Ul~Yy}Go9i!j~dH@d==!)~Tz)f<;-w{!#q1QZnqsvTGFyHdJk zW@ei9CBvydI6iLhzH}E8yHlUkwpd|4Bt{oITj!>%_II+v5{(IkVsCE`E*2CN#K6Gt zi`;FzZpg_on=Cg!*`4F}{QDbhU;NscXIujffz7 z>5$Rb=;M0_y~b%h&2AemCgrd}tQ36Yez<1;=eOWQk;lo7+nYC`#4nW^>H-7BW2j}< z#x}=_PyYNFulH~@Hby*RceZ|gJA#y#klV#-C`)#8B!4Vj{DD8~%FOD>@c$i3%&tR{L_4z3|(qvKA`%`b7a*$&QCG-0vM@0=Zc%3uq)_`@+RXYaG z%8AR=Jg3Oet9SRq^jCgB#BSC5<)w{{O(*&rSnBJm%VNFy$z0{^cen1s%w*vFz-NcE zQ&qO&A|e+@WBRZMJJZz%usqrFDGhJ-4AoAKPv(61y)WKm%O(B#^$Uffq@)A}79Jjc zd~mQioI8}SUYsSDbauYmIOKn@zwdsuQQ>v&^58+ljazph2o-9S7%sI(VFfi~rf`G9 zrf9!Y{3tnS*k4GAa%f8>3uxVi5#dp2-7Ft$%K9dj=r=_KNdU>HznD%}l;PqomuE*Y(a|id ztTWToYf+nElQp%q*T++~V@1069z4Jc60x_pKR!N&gXEMHg3Cb2Zlz&ty!6xeE*xw^ z@I_cyzm^?Fbl$kRoepM7i${>kqMV*>7Q?C~e*P>y_a}kX6b<=3gLdWN)vM%l-XZ z-~`OVeKW3evWYK(Fe!0EXk-&{9{u}y_|V|1JQesZ~auDv4Q){uoXN7rWY*!eKN0-JmJJ53`0_!P|_D zi%W|xHk|lnba;ecB-j<3<*1ArIUgS%FYo1I1b^1zV~XanG1tF;<~RC0w#G|>gM<0J z&iWts!se-SGV!usg85(z73tQBDXM_YYgJmATU*QC?SxYq8yj0;JE!;Qp|G&9k)fej z1nDJsZPk3Wqs=iUa`IP48zZqj=%p1EPyD7EylTIGeT%uUvQp`Debo^|GlUoWNa?@7)J_4W0!5z88`JL&qTH`EJtn3C0&3j8?Of z%2|OBAqypZZp()3SDzTtS}{d__e70 zQCR)gaqR+4pVPyObX=y^$=n%au`oI2!LvZuvO-f2iTqeCQ@$nM7QgU+Ce(iTnCZ^Pj z^_Uy$+1c4k$C&gRREk)iKmTUb7Ct&Us;Q}Y*pyhPRbg?;RO9%9msdB1>?NGUpR}$V zgA__kv8)7L)E3xyF+2Qf+lobj*>#_ve<LBls8M9_-9X_uCEQG1N2Cx4Za#) zTwF9+toDoa^FyAntoZ>Oykz-_>8lL87K6_$K!N_+HDq%Gg8?{);QI#sP40ybF)`}Z z;p5?r}ANF@Tz{txDm(8)OzLW$Lne#z!*7CMp-yVS?P`^qT=ACMA?%Yj%}R z>w`TGMGymEoF6~pxQ`@5@KC_1fCm9~84`zcBE>!_je?4X=HTd9>v3YEqJmSu4CnFu zcsqY9qf{IfjO&@tEUXP<9MPjks#;o7aT(`$*B1x!+}yR`n1_h!>+2zYR>3jd-*-60 zXy! z$N$rUr*BuSLmDwwX4+R!P{4onSJh(ww@37WL%GlYxN&9#Z*AmDJD+PeLD`YcOs+92}>eX}9U`_QC=86WFb_ z!1zr3t95qLL_=tU6Q3LWilh*R05t{xt-rs&v)AKbrDu3}7(O~bJ?*e{wYNuo@E|*m zAiJvn@fcQ6hV@h>&+N6kKex-41V7_bqDgAM@u?{$o$ue$(hLEgL7)M1v6-zq{VM1y zB_%~I6EDz*t<+!Rya{Iv0(fscvk_ni*fKhLdLj-R;)u3%T8o}UHps8w;KN~Ci_`a> z0MGmJ<42~(i)f5=2-*NJn3$LV?wtSKTdZ~7#KpriwXrE$2~ftVuo$6pbVLMN@Re`6 z$IjH$)L-{7r)Vo11CL3s?$yut;F)y4 z+fnfHdO@%)buKeUg!~dC@mH^2Sy^qtDu7vWSPbWM_DVC6QhgSFEN?*lJtZZjwwC+U zR*C8w!&qBWv%+dZ$@~1n!^aq8eA5v0b5#-}BNg=YW&xQZ#ytPapH6$Q(dQZ#{`CAj zh1cV_KUG*xP7dMGzP-9pUj_aaK+n!-p|+vn0&HVleLakah=AZJw2{0*E0eFVF9?rSbap3XBW@ z%SV3le@SZ(U0^|$ZvJ$>QB6#$pHS)iHg|cBg@BXm7=v2w!f?tJ|AJx zS@he}C-$qtV{yqO~r#o3f=w==d@G%rQ%n|0*kgnF>Z`*o^NF|ZXHtrTn zE++(!2voM#%itenFTLQy4&M5gMC+JbTB_z1%Zk1q9?#5e$&2kKLn%b#Ku{b%sN!j! zFgSc{CC|36wlX|xlSLq z8Y&qbgRhVjlAddl)|Ey#e~-9Hc=1vqfIlp37%@rmraxonp)p@S_We@yu~7>-dhR)^ ze7Q^fk1!Sp%Vfb|4pP$MLCfuOvI_5XIP*eLTC^Tq5P)0XT8Np@8djvG?Tayx`Wril z`SRS&y&{x&Lh=nI9KJ`&h&EHvl+Wm73CLPS&V6mAcloV;OOIR}W+@Q%q)SNVeN3xv zfKg<^i~GgF9a#K`d)SbhMjjCCu+Z{SNmLs3Ye*2ohi@si-^ZkqJsf5cEKRIyT579% z_yjRy<9KAM0Q)XXO=-vX!(+c|S}`>K?Kx7KIW_u#GU-qz=_6DneQ7m|dPc}`;jNjA zT+>=#-j8tcy$Hu`%P|kz@hXfDM{F5*WSr)WQ-ONFnohlbeBQgcB_xwQd16WkI;3Bs zRv^>a+em1vdfduK@s7LzVyB+}G&Yck4Sdd2?3@p^2QB`|4;9UbaSgxvUc|{fqk`>Q zOk3n|Zxjnq9g;$jfeed~YdYa1mBjYS`716&A?B@n!G3hO@;B$yDn5xy>Feh@VTcKT z4d{PDLU1}(LN6OhCtu*5?lKlY^N%w6$m~O?bSl91B*9k2x9QIt1|iaz<8^yBDa6Ap zd`Fn-*66dDi>0y^tyT^Sj>Zjh5&>f@&(D{DpA38Q1goQu|5`^+&*sBxS{2fV4@R%k#jVk;GLdth2eOcM+;>Mq%1I0k2}w^+U#KDAOG)wIGJoRPxZHN3iDWS2Oj{8uV24HAlRL*j+u>5O1gwF|M&dd=VGs|@nSzj zgi?{O<;S~Q@?SUw;~G6r9l%MOZ1V$;1fWAmSQs(1q$KMNdDO430e5mbEPc561T(#h zjNCq(%;jtW84R!cAwJ~)Iy%JlCE(Qo1w0lDU7M*rS!f9nrF=-S1T#tTxvJj!0eli% z5QZ6@oO}g&n5c-z;m(Z5a%ZfQLG0kr5G3<6#rlm9q)GX`zs$zR#o-YU0Qb=+=z9k^ zA!*7eOrb29gy{HqPM@m_$j7mSAcc0{TM(_efOUhh(p#F!$aJ?yJqFar1J~5m@hH#P z_CH{_866%aV|k|kKuN#p;LnX}r*$QW5*HU2cQA;*`&@goJxNMRBI9`@fFda&5zk>u zN@2V{mAaP+nSpJc)kL*NK#S~k`FF`@vtRev7#swHO~1} zRVQGGMn*>Zb#F>sclDM#O}r7wk{|NfGsrUz)ywL@D!pL?5L}YW);LULt#}c@()rmL zH8nNFt0W#bhptZy7ILXp06AbmfjnqvXo#VaXFtkDRwygCnbCTN+m$vCpRlm782e)F z&NcFbv4BbS0yKn&wuInyHMk}7dDTF|0pn=|f&svXg`E9T`$JdPlmCG_2!w!keK;f! zBn>az21&I0>AuOCkKt<1=hfw9=Y{4V_)53I^AvIyzes?zu=US0OP2s7Df+yF$Uh5! z9US8nB?!t0EzmfigG1eI>Ix>mx-)c0$*4X~}{nIZJwvmL_{o^T0u?ii63x%}k z%a<>IJ+B_{U;H^c+Jq%?-vxB>G#F4zRC>BmUotN$1tjTk^=+V0FFXAL9gyjRBSAp`5+YxYH8^OO#2=ls6k7b2Br#sokAGOBU%&6l!yLc$@>#Ashdkf{l%hfg#KWl|qCv zYJ6g%q=pqtFe<6UWqWdceLXojIqUBSB#Rv2l&ofI;L@Jy=?id0%Gq-D4$Elu`Q_z@ zWBNW}At8$BS_eXcGpy}i9^2W%*0 zFQ7&PoQ`~@I6;wW0d~fyTV=4!=#6@Ar!@tDtgc(*f{il(pdoonB6s6SR-nzO!o$Nm zIQ8^5vbM6avbK(iiTV7(TnIr`gQHRlckOI8+ygKRw+>{<5(%|K%2m9AN@ZzjsjWSg zkwN9-<0JGzf~pfnz$pzr3|8G#sC_6~9^C6BWGJxMZfss1a>3N7cBG9(C+O z-jdeXFD)gdq^!KRjPc4?O-&8FEGJMRnVBXKC4j~=t_8B~Q$j*Du;Hw%WrKr*5P8^+ z+I?al3c>v-6hLeN;RcJ7^4`}Ml>$ft^#g{bjSVi-USc5$2Z#Oc1XeFkPapy;#)`av z@;!?CvAAbE0%f$$UH6lnq>zw%6d45t&sbTNWxf3$XqgBqEW1$WD#jT<^(iRo6Wn-m zO(Ejle+*s>Xz7V5u(_EAug`kHzxw)Y;pk!oQL(eD9pCO*cIfHp0T0E56+|0M2niNs zmHYeqYfssL5G%FeKU$lxfQuO!<=Epy8yqwxgVO@;6YTeJ%t7W3>>p&lPkDaP#!5jv zglzJq(1(f&HVVI{d1Pc{Nd6FKV0n2NxB{ReG#kC^_n%%|UIHtvrKOdam{?;zM15)j z#w(d(4`O^?!V5Z9gsC!j^wGDo3BG~ zh?QrC;}0|;KpZ%nN83~C4U^!=PSyr|`Cv(iWG^*u42ytUxN+mg?CdN$5&OqEKCm2s zMi3do2w24o;#k1DtPf=)wiGa2Pe(^;mt*xbna_nijI7@MXtLVT6j2i5{*sn+ecpII zRpV?4oN&X{*(RWjDzkz2X*nqnZXx1^4VW%}5CmNO_Fb8ue@I3KG3E6}0FnUPFznJ|TLkyZmkBi#U*!o12!O>S z#t7VzOr^_qDE=ijq3w@HILG2?Fw7_MKK36#9zG-g6@82vJzwl@$staJ0MmC^`WM*U z`_!MU7!ozwBatyqlpP}>xw{^1s48>Z$a@inUF594Deg>`Ay`KKo*IFqar5MaShpM_ zHP{hw{D*)He)&}ld4=UWD?+vd!pNrxl&@j%xpWCTaaKMzLvA+JQb?yXDr zKc$As9B4=uX-7uVGBe0XIjKu==EGhcV!nBFs7e#5ZtPh$H<5;A{_fvE!ftPGf9{N} z(SPj-*~f0s9_ZANkkW)LShNxcp9)eQAcm=8AAn*-{W-+C9HC`F`uo-s(>}-I38IFK zlyerENY4L329W?M#H#uWWHJiURTa~f<0HN;&l3Hi%=jvjPXbvM@%v-e*GNr{k%)i( zz`#Px7U^3$g&@l=hKUB&t!c#XT0I61T+v`&quoh{`n57Wx0DubWt%Zygqwm#dIteh{Hl=0Hr=0+TPr7Q(NI ziOEI)zHo(9B=|;t{*=B|ZZ57wh;%i;LL({oqWJ#;Uo}0gbNB9DjO@e*2Wt6UU44C3 zrKJuI4nqGb;*f!`LTm-{w6m{|!&&=&(HT|y;Ghz`03sBaE2Nh_CYF|6s|QeDl%2tB z%Hu;-BF7vbACIqjgm}x)#P~Q0@=fKcU*GcN-{S;{;MXA%`xtm|{xPe2zWjT{kdYPz z{`6jz_)MdkWr&Cg`}jZh{iJbeJB;ilVccW_r%pa zbvo99Y}7{a_YkQqIdY%iVC8jGE-{(O!tSS?zmH?AlVZea7BJGex}TJ+dT9wOhs7&l#mt202C^nz-x3-qtyWdbe2T*^TiLo2!WAq1Boy zDRkY9q^5Pa83)ccqZr;uKiuG5frnYsxTMq9IhX?8k4t=nU7m>Qy zx3@!GTo7}?Z=v@4!V{hSt!1h$6f>IsM1YE7X*i0f@_`5!*S_aQ0=5w0nfBjNR{gSP z+w18p*VLa3OAeQ!>ZUk?rGx1^T>{ zHP*2l_E}4}UPS^*sS}KbZy$?Bd2yG{H9Q=3S)FT8z!2qEUQY0G*1s#$7*9z@h1qfN^UtOvTRnzO3A~X)Q@$N~4jNNZilr9T zpYyygpW6Dn98ZVqZr2<-wRhr(_Iiq=XjW0yD`IyM%+<8D#n!!{UK<~gGqFUaICWd7 z=XOZuc~(}QG3oRzWNnw+2eI(|LbAKQ-5pr;RH8x>E}uBB8tbF|BEMMK%YBMBua_cZ zt8j8z=9PQ)7%Fj!7bhcW%-FhkS4%A%>Fk$@f+(lnZX}jOa&PWVrT82a6@F(vgRM|{lYo=^x;*V%kLou;j^Nq(o6&xpRuUxNb z7N?%(ktyHXr^lIm`Zy}mxohaap`~o4Rz6XWe%^aMoWIoABZUvEJNOWv?dQ?xQ5uoD z%Hhf^Z1(HuO^zs1&4je{b4OGP_`fr}?f7|!&ccLg4l;yfM-waU4i zJBTry5~@^wvQgE{HSQIPDABLct=D+7Z%2x7_A|qW$O(lwXbgp%a|{Pc5;U=oUboD? zQ&&+DL@^(lby>cXa>pB2>8ZWlLRNM#3WeGzv9@+m7{l0PfVL*VZCu>p-}AkdF4Ga( zns)=WW@@hFSsd+_j!Bl*G}}U(C^(97k2ZG4GZJFsswUQgimG2OH2+m{ycZ5nX#Hel z)U|Av+gSW)f_pz8)XpWp$9AlvNb>9Q+!(w{oe}5G9c|}z^Igq0t9F!CRAozh&fx8j zZhHmK*hK%Ha^j`9uXu+&JAYtbgC5%Z6qh3x$06=9|%Whc9SonwO~`AJp9 zZZmgQdbG#;Z`y>$&)>;j(Q~+%klqKffZSj|X>3J-K_;je0h&wabRL6vS^~EQsQEkmq?_ z^kvvp$W$HDRY@v3G}fn!Yt89bBoWSqE%I`7+4@~sVR({9Ih z32Rbtf1selcCG3?OHftu%X(hU!<6$M%S!Kls#z2?nq7KC3*E1XO>25;}KZ&OB< z7pPF?Mmju?gxLMnH!+!9SEZ2PO3^69RK61*_w|~+p{88rcge3$D2z{ztxQd4$L7xd z{2lf$yKSq#N*8CUM9r=I3`_FVGZ=IKs<8iORKCKG(eSQC<@MaOx8y=79UZv${FTIM zTJr7emiGP*yGqv^Yk%hn#$5Q_EGvDP-Kg;F6QA6(eC@gE(Qs)Md#42~p+RMTr!yIO zY$Y7{)x^YN!hUnDIV6HUHz#s1f&mPU_{;E&Pi$f4ZbycB#k9rc?o*|IVl9%9Lq#i7 z^%nCnKTLZ1BK^nG{$X-kK55h(>KAmm7whSztZn<=+A$9Y2NR3b=qQ$#qYGsALbBh- zA6$RNv^MCFbZ2~Xkf$R{;D#0Cw3<~=RovAl{)nnIuF@u-b0DxUTPtwt%gf}GH9_-E z?fLMzGU6mRS>FN_icg=ubUO`{wV6<+mOFn(7j4fqauXGOl)LMn|0x4P3x!|#rg7Yx z#zRjPLAdjZ-;wmUY-=I-}tw9%OQi3wLJQC_iym+f4o-kP)N#lowk7&xMDcse_S9hVIUHr1C4rzSg# z)J-g(Vmun3KATuX*q6~YWA64(vggemgQZ#55!gsb_*w=Asko9^b#-3`{V zp)50SKD(9KF}9!Rui?d3R_PK;)ZRcc!IX^Jpq;h+1fba4`HYMVvnXRUG_-Q*NMek( zc2uk+WN!A+NQ(vAj*YBi<&~N6lw!UyG56S%u4?$v68IhI9TF|NR zMQBdncFe#0Kg60ZK0xWa=gZ5of&!V!Wk?%=j$vYCWN%()w;HztHks`<PcSY+!`4?QKFRmX?EQ?)#`R zM#k4rApLnjCE;r}k{1hPGNjP!RlsimVM@wwg+E26ITi1x%Ht)Km1lw1cHQ~m_A>b7KfD6j+}xb6>1kR zUO0_^GXO<<*9Fapb~;{KEaZNQpjBl z;y1vVfDq{RJ=}helkF)QOqAA8f~Gnxj6g>qa=?b9GY4PyB%%|rh@1wf9NQz`y*mfm z1tzAxkrC%#eyEI$jQfE(w)hlOFJHRBF6HLtLgD|`o%<*l7{I9_q(x%|n!;7{v-KV@ zchIP58W_yO*#U*1At-%7R}6-vh;sym2OzuNczDd!y4qg+-Fx@$U1{R-waQ9wQobkU z+2T0x>sD6JBHD;R@v^trhR_qNuUC7X?!$_fTTMh`Le1w5l$yxA{|cH|s(NRkqM|y* z9l&$jLJ5WnG+8zCLG>I3$#G1~0f^zCVg{^jtgGHBQ!r+9Qc^t>L4b`F7jJ_B<(@_Z ze=8nNdkM(wES~r+u5ZW7t%!-h!^T~>kpnX)v zT(%rBMzvk#*PL%@!g#eGuEPB=OElcK1a=BHTMSEvi%XXOFf$%w==ftFku7|@D82ZR zGGV?eD#uvY;7j5wRG_K*SYX7DPeRz<$q8ZjP+bJp+jh3@rC~FQkc6JzERcSn8!iQo z@2PSYG0&Ule0+Ldyw%m!P$5wH5CKN`5?mkDEy11sUSB``{qvT)2NYu5-Or#tHCn8{ z3w0$pze4SxX#zngr%C5W(44ENiDd#S2?`SzP~U>=>EPf1*wIgkE}%8Dg6a&Me4qWL z4zMb)n_7qEPVh{hIcx`SgEET{7q=431za=?=nouWCnGSO7w}}zz`3jsnt<~FqP?`N z5BwCcz?GGiAl`c7R{)N_;Q=0_j-9i!F7S&`=77QxFp6}w4B!>CsvS&H_`KL{XP?DN zK}`{8)&Y=ygYYXa6zVRqCRo_maPxPIk-X{YX)v;fLZF2$)~RN3+g}2L^Kf&_7Ie{_ zM;z9ZhX;N9G=3uD;;Tzb&va{?pzH~hHAs0P`Hk%C{s3Fs)z!6*4RwqGb51Bbl9Ru^ zituuAVW%(#84WzU9%N{66}ZAhgb1xxg$_z5mQY#Aw}>Eqxeo<2@T~A|CAwH>4N#p| z%Q@vlsFHz4F97WtlyT;5#f+LDR*x3xPJwrW0fs_x1|(Tf%y6-{&jm3gRB?4zz?&Ri zt6V?@3q;VcPP*S-{Y>I^?M>mA$9r>rYz=I#&*?JW>EE0`*=hSCevu&d%gV}P)Nk+v zK^~MvPft%RuA=GlMNV1KF))(ZEY(171#hJ%BQrUnucE-K^Yec>N<6A_MlouOiT=;qKTDS*F9YS1VnChH03#-AEuC^3RFs= z+PYo^{?WSs>$_jq!g&|L2nxIGwd~2x3@CFsVK<-(IaK9;RR>Qw0R=8K6;=OrX-=J9 z>>N!&gRzR%wPTGX!&Tm#=L*w)XsxQ}Uhz$#cHi1e0`s)pPi}=XOPiW)TYqMWHBalN zgH0^!A?2g=tMk&`Q4DM}mQB(;;FTlk@JOLlDdOjszqs7zVr_JuE92$nRW1KRQ*7&V z$U~+Y8AVO$N6a004*5agBR&4>mBqDo-A200Ir-l{G<4m?8jW=I(oBKQ+MeQ`^EHbR z1suhl6MEDFkNl9$1%P=6NDaDhM8Tefgaj3#DfjmFhKfc8ZD$ukRjEfvmbNjN8B%a9 zumS<);lqbexyBI%xo`s%-1)Sc}>gv?^I8?Lg)!Q@fhxM(7h@=e)LG-?jjEpIyqpORk$y}Z}3$=sK zg$@M(Dx!)iAl-+9jWCrOPiQ|e@x2219|5=uij1#cfAsdsxuAv-vN^cA()fXUFNK0- zPL9N8UlBkCD0M-V9E$U4DMwC@j_1%R0caHT7*IokwkO~zABC5G`}URJj4wrBNhvxu z*2Kn!&wfD!N^PfxP;7-F1Z;PZ0j&M7M74O0S+At3u}pNMD^Mpgt?kpT7##HP+Hg* zH@DJ&f*|xn@BuIpl7L!+c7=r~9tLO?H}O!AK{5agF06V_R~N*z=MimXRaLt~*%8l_ zIJvk6uNP>2G)nm&#nM2FG@^RLZWUiqQ2}p+k|1=g4SOEOR;XQ?<`uwh!F*En^Xm@i zuG7E?#-yZNL*#){?%lhAh-0qSs6q#hYXd4UP@{z64gecg78bZvcz1ie>*Tk z&{EVjG{#}@Er!e#OW@UEfM{&_7r`IG!mNjL6eT23L=~YP2f)SN)zt^KPEM{DhVzCe z8A2O)^R;Vq1-E*-MtKdd4qA^BvG^2iX1g5h2=R%Rc8|}vXB#wSP)@(c9(~lTu(qnc zeQHRg&6sp-mf}>kxOaU@Us|#pP+3ahH+=ltV~kB+HI}@Idt3=;ufXWFQIk2Tg)EA_ z!^()us;NnEhLXdj!$7}cFMB2EQO}(G9A5{lSvyq!3No~P#DVeczPW)cO77T?I-pLt ztE4n^sUc!1THQ*crK6-Y1a|;uJ-cdYdAVbdl%jcN<`TrjYWhKd4)$9?@&QRs$80C5 zn2fwU0*|=6*Fb;+QN2joLarmVi5XPn5NJA_ls7Rsd1rSQQQbU^$up-3Y>Jw>G2v6V z+{UY`qXU2$g#x0U(@M-tB+&3|cyb{T5&UlZ83N>X_V!Qd>C?q`P$)`CMHu7geJmK- zf8|3TqHcr$`8endphUF2wFUBZREp9oQWNm5xG2cbcml<4baZr*=dX}>F&MFli9xR3 zK5qwy2_FZi9KsB6`AbX26XmF72$9dRI>mK=V-;#CjD}!VZzl%*@u9D~?_Rm;CSV0sx|e zrWC+&N{!xLPzL1^geI;e#;37TCabCkC*)I^`yw|XmBehnaw;q=G;z*T(X?dq&Wf$b zE9I6~{J5{L;_cm#pP#=qM}E2*3Nezl>pfH^_YMvq3ySedv8$ejeOO*uIXgS+^wHxX zjlc+D0jWn(Q4!Qv3-lYWAlw5Sr}zH^Hrn3P6MH>2*V`LDySA}Gcm937F%xp(fzC$x`+5B!A$GiF;U#uC0 z8VWnVMK;%fp~|ryYJ>Q~{qWPLNC4_H1B5C?bBQyfY^urQ0IAVY}`JF z>Ayp7f)%tB!G9IcZcR9YS`LY$|Fr;NJ75qHnuGYLbM@f%%^Nd;S_B~6hA0j-_(!<7 zHk0M+!@0^%*|{=+EI?cfqF4cew`FCl5p4jRq?!WImk+*_LZKFMQnIs$j5<4S53A6} zN`YdT^6}&4^tzi2d>GW|j&^otLs>mgD8{M${29R3EDt;TF(fmGi*+znNCE)!4ezij zxl!h-SlHM=2;W&;6h?W<$oR>bwCmafa*p9DwMA1c0?^9Dy7JM{Nu4rv{`?6&mLTWq zr%<|D{F*ZGv5^scacJm$TU%QpNUJ*_##~nn=75Pphy&aP2}7rk16?B}z<{;u&2*R} zvGkVQEp3Pyii!r-*18k=iJiOHx(&u#B-Oo)btap2k1EOjvew$%3Nd$_Mx}VL z6HCe+*;OaE(#^p6k494@A!8N#kRg;9OX1+8bWxn=`?%OydkSb*^np7_*uQ8i6)VgJ z79gzxTnoBa*dS<}O0A^Ige7crI}oGA$$Of<1xX@=Nh}m(FsQagdrq#)3Mv{JD9Q`d#WsLLhjysL zaZN~X5hpJ(kuFvWG<*jar;hGgzBhx@{(5>tWpAtT1o{~0VyT}z=?KuIH!?7Y0$|j& z3lU9l@7NP2RJ)!Qh1=$fOny8{A0DVC-!OpY8!^0(qYS`Kg@#9-J zZ$i%Ugy>h%{kSghLo<-`Kx18CV4%Ydwh<+q5m1GL_T~{Do-`GtWbq(B6rHyVAiFyi z;2#js+SWF7tMkyY9(+=&t3M>!&CM{Sp-WGQE1-@701dPNAOwU821GV7cXD1n6jg+C z0FoB^r%xZE`6Jq!dMx%259^#a)nMZRe>ypppN-~S$UyuD8-k=@DIoVxGo9?Rx(Enk z0Qeq)S_cw4I5$4fQwHZ2R0&|!Fos7VEw^vq2HUQKHe8dj{QYy00X*zP=h5?K+(r5A9=pngdjd!#{6Tq#R0HR^?V%bEWR#h(e`k=GxRn(%xU=8E-kq zSWDQh@|aGlhJ^H{-}w_)n@7@_R2<@U?&gz2;)%7WNO^pRf`qZ@>5HKRtY95g)>G`j z#elYM=-L^S9PWSr7(m-4MB}$NZVmJE$)lj6qrIz2zxWzs6oDP`#t5)uMwAVLWgoCZu3D`@R?i|XMLL^OfsupR0J8jx4}EHtAod$ako&~2^*ccG;<32*|;578WRH4YmA+6*J;-B?*!xw^Un z(AxRv&fUAvJQ?3G3qGpx>P*_`J(VBCH;aij?A|@8xf{?C3 zUPwpQwY2zZ>NzM%Kp6u($}1p3!0@0Qx-<~w^5TLD3*?DceTb6uz068ARN4VtQo~n{y!p{W^Gr zgj|puLo(cP#io+A-jcKh%gYM{I?~rE0XJjyA3~=Ikdbph5CTB6wzd`zBNWXZ0T6h3x|#w@4k|+ucrKz{ z;(v@1&aF=@jap;haQ1zEu(`XB|e`99k z<}lH{$p10nKUNynGdFg>6qAX<_=l-n8^)6Ni7Xl~o=0cFWM|6T7}i~&X&-F&OjNv_ znhd}mogFPM$z-Rwm19T?k>BHs^l@Wy>Hke5Cth7UtouB*ReWu0+qYqkFC-x*77!N~ zm%B@oL*1woh585w=b6U0SHMYe9cL#-3XsP`hZE%6^wKqE@nUeYl2}bKF4qjgoY(qO zp#{o)9n|W60Mi+wfwH@i2{J>sdx1@g$g&C`1DVY8V01PK2#1{pwGZBlP$qEL zCgEj3j8tMAo|1Z(U|1KFB zp%L6a^-NVT+fajt3&_&opq+X)S z3ep7@XJNSU>k9l6f&m1>ESnDodonXfUJiD4?R|aFg$pSagvu-%#Gg7CCKpQm_We8j zd5xFQLsr9ZN4*e>DzM4k!GR7mnq6JkK_dU_LQsDKQw%3)7@ax5B9{CK7HVKqNoi@` zv2GhY35i<8E7|w|NCA?k`u?L-CPEGbQ$0ew8Sxx^L6EuNh$hth6Vr_7!*7U>Um^c* zY8z#^e#H<95C7fq-!&t2f`*Lj@b9+|Rq~O86s-^+Kh9D6PkR!I^gkMuT=?L>4HQ%O zf45pr{6(qofa%GcV+v zd@1~({swS>p!7lG48T6?1mg9~*qC}m_7rG^LAeM0i>4c!2-(alKTx!+#WW=QUH}C_ z$Y$XLE*&FK7=?fjktY8Ix*5^v`{DI2I7HxTAq(CBMugcG4>3CzSi51^OC>EWI;fT- zddr}*8dA+e#NPq{k}MQ+0sij8s~>McFO4k(t(D&aM!=tG;C%V=XlvqNv$(Ocs*1+% z1@v)4XEfyX06eimTEIKPuZ6m`mA}>zV(IyLdzhWFKF7ax#BWhEfTlL^ap1cTfVDwn z#I@``mq+UED2Q%25KvS3>C`xxL%TYl_r}ZPDL+(fVBZeGXLVr&g@=0s+YGb|z*Tox z4rnw6i03~9e_SB`EEsXHG;-9uBF_k0t`rSfUPRktzc=pg;bi<*5Gx_o^~1b(FJ#X9JZ$z!tQ7qD z0T)n30_^#GriOT%S+Uj=3Etk89q^(+Z9$r2XlMwyh8+MNWLHo^pu>rgpaO9|jo)yg zwz~H@xR!Nf0(^XlHgO>dGc#I>CE%$4S>_+Xe6R*4=%R3Osf79lB=P_GdkAe)z%~Gv zMN1>01zQWu=TcSsoh*N$RIBt0gy!w-1N91b3?-)YaiIJtC=hbZIO~Tf$k0d$?Bt<# za9W1aQ!hr0Kx}+`4F!e%uY$LL;_&gg28ku`q>j}(HGhD2laiKR9|Mky9I>y>!zYLn zBtWy~tFrWe@pa~LIj&v%&o*T)+9c6r$XF7UC=xPdEJMXqif(D9Iv5Ju!uD9>D4?ptp7|P*#G()Y4PNQEgLpW zVd#ap!kovfb0RpbQ>fD~uPaI&9ZIA0{Ki}}x>wHLnTjw3dxu#St>Ym+%GHX&d@pJ_ zEYd(zfi>p7l()Fnl~20iwPmYF!#usqX%{cl5R)wHg{ znu^OC>OE)krcLtBJ#;*$A6z&x`kJ}xqKh{lo369aH(O`1xcKT`zgc^fgeb*KvwkaQ zcf>alQ^~mC#}$fdYW5d9e1{^+HTSRf+i|(=xDYNxL~P>>#5)3{h-5dh-ws2!r~5=Z zOm!l?`|AGvF1E72{dFP-;(7h2BZ4(jM7j6+j~_wI{?1WPL``g>L{ zTb7oUwTBo1uT$dc`^yhDYy9}}36?^2`s$VWy0Ifhe1pPqf1|!C@4GcwCyu)!y@u8W z>a%Inra|Cz>(;IF-q+{^SFc_rOsVxwSf8|D@#08!a+fnldU+l>LU+{C`SUl4)+d=N z)2B3AV*Z*nQsQ;rfBc|nxc^5IbbU0gQt8+s#y;MhIiYUH6g4%OsXG~?LF9P=oun#NMgXH_gt!b>(42eS3R>pJkNM+% zRaQDxNUz>W0|ZD9c+XcYX_giel6$OUX3n|ZW$8{d3xEhlEj;ZP6f`w$aHWXgav}oO$zl=F)f|$coMA(hEa;3FcyFK;8hY4fGn5T@LHZ3?b)Rk`owD{3+FZeaP;2})B^6gRZ0`R|gaDpDf6VaV7oR*ao-sot(tPlSMM$~&`Xu3c zMC7LO&zkOjY~Gf}YP3KfF=}k2AVW*bOGvO}JXXg+ou`^~nS?D-BoDE0lAj133rg@ z^y(~Kv!>~#jH|^ugOw`>iHk2?wMs8+$i+S&)dsCh`BpjfKb!@j;R%WUXlFDg1T>Uhmw zsp6s{7Gr<8ohXM|T0$?Jip(3ObzaWHtl0(9wgxg^#oiupg108dQ_n$b@V4tvj}q=7 zG0gts%d)r#}ehQVMAqcnsj~`N!k|hNNI{=21U^Rh*_B}Q2(zPo*%Z=61FH%w#FIoiq zzFxhHhJE?k*|QS>8$ze2uZ$~Ir%>o#orFpO<#pajjnN_o1}CRGIq_ylc+mKS#_h8` z4cMsyXCfw+^}sIN*LU&a#VJ@Zn6TsZLrqPsxw*M}w{C~MyoMrJL`NUDEJp33AI)z2 z_Uu^KSi%=R3VQP6UVjJ$QE=VgfBxiOv;m#q3s^p-XXu>M(bfG*$Oq)OwzoMW!|TUV z;<8mf!JN0YvTfp@J!^V>8TL?9^CKb=t8MXO-;(4P>4RB6Vmf7IWpFwSXTc4k-c?Tb zd61|ayak&A42J0?f`=OWn8#YO^C2Vp7Ca0I$%Qmn-CrX#(AivBlFDzBwKYbE&OXl& zTbmO8Dhf`;BZAom6vf;FkL3hM#AGobSa>>QLwrh#jHKj!Fbz#~bk=;?+BD6ZTmrkg zUn~bz8HTdE(NV2_f*-uE5b&QZExmnPl^jnJ8#XLN7}j7RdxkEZ_Wha*%5$G=hG?>~ zQgf)Wo;>*u0A_=g)k;Id0^!2K7Y6b=7RzvXc@;1}Xl>oPb;Jh3%O@LEhR&Fnm~`&k zSzO#-kiS z`Ow6*Km6xJNyy{gbu+ri)gYZCc;VHj6BRnZsknQmr6^e2MwAVp5A@c z-8N_~@(Stou&1Zu=+R{`G?X5tXGSY3E`)j12}?{$T5V)hSjxIXNJH|gt*yla!RLD) zJ=*lTgeGhYmBIF^^jdM-1}s&X_$P`U6*hy{NH0P+EH|E)8yFa{M4ku0Y4-o~Pk~;< zxdN1C&1P3`+Pd{yVPSmhCX?rl$)(Gg*#=#;^7@)f+$b)%px~LxjNp)vn1CKJ7V1_P z|N5&aKVO!Xv4!ck@7)^&6)P@2QBAF)tn93Y9j#de%=f9Oa&I%V7O(H`<=(72TB4_; zg6)Jkq1}Z^c2>%VKYMUza3YOcb6Zr22|`TByH8+DF*EE-B4PD<&K^8Ckd<`mK1Nr5^q4Vt2WU{F4c61sGZChs+5h-)J9~S3 zJ3ESq&&WyKjfdMlPo0+g#_91?P0gzJo=SH`iYG-c2AQyl8NrnNGN^Ho=e~VU$?aaV zdCW>1=zCQRSnJvOX?%SA%a?PHj#+YLh12YFGecfLxyL`$N$Zj7muf`w7d{g1Ek{F3w7(3RjbAfFO&J*kz zL>NkBb;hS~>AFW>@87E>yxP|`H}^u$wJ8-IP{ zIxshYqHt&+S8reA(fpDRPDCz7=e=-+dXU6^5p|2e)JuoRJ2e*L80obn)-;* zqvy?_c;m*=Rh2=6KBgoFd@-etaB*n^)bzh)Oe^%wo5~Z~XT185 z=MR8nA90s3Ql4c{5aa%Ibe@SQ=n+w!1{@6wlhglhw{WnWyG%he=viL$ZAwbNUcEMN z+46k)bHWO5%z!7|s54yWEN5Qtb zXb}NO{ynkv5v$2DLp?Z<8Uh%wVVf$@#Yq()>-p?L?29}Nwaj}ae~g3bR?bRqaFh_} zr+s84G5 zq?a$PEG%~N6@kobJ1&CJbPq0HPRz*(n*I`}D4=GTmXo@I0-gP;lPA}LVqhyvor|U4 zF5I=N7UoMrLgJnEZ6Is^)Qg7>9U^d|hXU2LTv{Xt4*XC&KDTp9$xmB#Z}1R%npl}V zYG>cQ6M4b{p!EB6SXvqoWPrceXlcyr)5lwAMDAgs)2p);R9# zt2A}0REl%3wyTy)P>P7e$RB(E1GFPWwR_ZH4P`wE(!}EDPV#Eh*jdf^Em%T zB);cw^+IG$uJ&e$GZEzpZ8v;3ba3_}lK;y?*&1}w7{0ip`Bp?Ec)LND9)F7+@oo|S zB9D6igJw!O0H)s3?Z7QeiKG*Pg#V3!BC`N}aZGt&c`F;!j~jRIfR>&i#VCo4OQLcY z)B58^YHn`G>C=~CjA*w$J&`Gd`9t_bjp1j<_U;y+o<6{QZrsV7j~|iPN#!|4uENlo z{eynvL!K#i@ON>_!s~?>i1hC4Dck+!)2CnHcz}0sJOa^8poE<#go^3V>W;yMz0VI& zrL{~>LE*rh3mL$X6)T317;znva$#H01yeJ#Dq|VrjW#x+At7($;}3{M-M-!O#Y3js znaH4^HCwlqVF0fx2-dKp2-dmA+&AnFW%x2G<&~6dAe$(xa#bK(V6e}f%cp_`YL#jh z7Z!eGyeDaeeTAM?R93b~b^X?@*BQN`W*AFtKX-0-xvvEU%b={{pXcVbHBZ*OYmwBS z+}Pqk7Q^e%n5!X(H3yys#&Z?=cc&ohtpt{D0gyrd0o;2Btvf9y0xhoU^y3VSB*z<4Ip} zj|$ms)h!+7DU&^*O0cCj=`X7Kkui4lNB~)Tt~ZVPb9DX4Xjmi~{&{Jij!^CGB9P*I zly`@uv@$c3lb8R6<3dR(64$K)W&;h4Mjo{=SEJwt6)$8UZ=+5PNCC(oPaVW_d&u#b zGoLc6ks_%ofZ`XDV{cvQ>fO8RD3!?Lt9Or3P_P3`f{0sMoV2yIDJc7Uoj7}T`mP@< z9`CzFD{B>1XlCXhxvxKe zKBD#%n2c}V#AgG|CXjk3Ovq(gLg<|fOBBiNpQ0>}fVRr5w`|+?HaS^fQRgxYbE=}E zr>86^pVCfDhiQ4hEw(AFEKiQ{h_E12U%v$^UcY$pHnXfKxJe)Ec!&yy*9#^&yJ*?F zTJNdW3j3}H+qJjV$8zOlR6ez(;0r9|>%_!8aKT6sWCHgAxOHJX0|Nqf|NJoCDIYg) z8;b+L|*+(JX2dpgoQlNAPMgC5T%EwrN=|I2QrkD04pE-m^vNA+e&?4W3k6 zSLfsFo062|b>v9Arm&S`#@;wA>#>i?zk+Gnxe28-=ly&40YJPM(jxVlvvVtzcYeNU z-wRpee*F9?Gu^?x@aj;_-I7vL`WPYJz4OzRXBN}3tYn1$w{N#mC@{vPUymMUxt+aq zd-mxASUEt_@5#qp+LPLQ^;O`k-R%hthf%mG&UkeFTqi3I6h&quc5%8xW|6YZpnYnyd>7U zyI)L<%gU9Rgjigwyos0&?*Y3b8b9ytH)qGS(CL za)^|ax>_%f&R>d)!>?S?U>Xq*h=Ll(+IL9G>51`~nVIqNvu4j8!Q9B;{*dqY2g8yNjUN;+(GWb{fhDIL4HP!DhBM~ zT;%J+mGZs~&??A2RId{$+nh*xqKh~H!l^H`fzd9m`PMfmT$_ktLI_a50 zX9t~Q!A4O6=*f;Ii?iTqF38Ntu*}<#bZi8fN;HfznKWKqRVC*RQuSOfP|DyXpfz<8 zN$ z#3KSC#>Q%gh#W?`xbaJg0!TEB9W+eaWtz6O%-kPT$52Ci3g5k3QZ!?Vs_KcSrozpg zGKH|vU9N^sCPwA*@Z6Vd*>a8ksrBnG96h?w+4%|U(?e=|qN=<+H$CvFDLo66N&Q+r z30NtnF9|XaL7{~50w-AwTbExcxC@ijj~_oE*0XQlIik=nbQ4XIJW5RJ-Mw<~1&&yo_DkhqUz!~8+qMDUU>?N_v!#K571hj&+kZ@?-(Usv}cu!wx~ z;e(-$jyZj4;N1;LY7-`Kjr!m@1B1ybD$795*p~5$Ru9*A7qfGR1DYAOWYrYEW57+Tt8bi3ySr#`kBx~4$@2>w&o%&El&moL6;xOTW4S(2uE<%Fuqh1NSM*3 zu`x003=IS9L!JvUXhb-#M!1^|Xp^y1Re(_j_f*00!}SOcPMm~CZ6n}aZLH_rYA zIk!Hfdp2>)=4p}G&;rSC-0WbFnLPwXbjlRy7I?F*8#e|r=&ju3#L1I*hkVr(9Kzk* z-GyeaPn}m98()Wws|mak5dk~m)YdXsPGaz24Fr9LwPM*Tl-NKMiWpu-Y~M{aqtEIu z*3*j=w%8l1R~GqKe`=U>ky6!!D2r z6K1`=`z-K-QY$GfO@<1;sJCE2_+Njiq#P)XTn&r3J$Bq7$Gv2aHAY5WbMSwu7RL); z^Xk?4<0yYuQiaZ*MUlajv%y1#q@2}MWARqMn00bAH>I*n;B&h@efpGjgc#;bKN*qL z`7~1vwH;~{Wo7fJbw%YqsU>wHbr71_9`IvT@4jM-z~80;$;+K*rVy|o;`s~4AihTU zhSkA%9L%(|o%7_~X|6*Mdca=iXZ!XlTEQ?2q<-J&PKXk`W6+@49yi5xc1~?YvopH8 zj z<_Hsye70A;0HkqUsA*tFXd#5K%?ORDCp|yI|5It><73*j*q}kM|Lr(PSOljv=du=A zYShnQIMx&@!L*=cQ)K`kZ^N8y?xb*5MXsnYK#I9(b0Gb26_=D)Z`d$!=+M?b!w0mz znKkk_ayBhG3of!sj>>3$z`VzqX(oknEWIgpH2muym=vuKb8m zqf*YOrNqZ~_O-Ojil;gxp!Mw43*`XYGg0h7I=(wpwrZl}vu8Tg&~(S1-18dzz@Ed{ zAUks8T}CJuJ((!ko9jX=0SN#wxIM5>g$Agiz$R`7u*5mwD-T!TFgL?Dpb8#|HyIcf z7B;;9Xl-p**_73)6DRry1YpJK@M09w2%PJtfyspB z5UOPi@rHg&5W{nF%K|~0QJTMKQYH5?@Kf+aaHYZ`nGu8n_647q2HODc>gdsAO4Z6b z>Ol|-p5^3MuN;1T?cqJAaLG&f(6YSPc4Qyi2SzX^7(ilfXxgHXq8XF$pRfW#UEAhe zL;_AsP4$1!$3uBJ>pTCWKWvug$B~u2ie?u{6wR)y(K0CvQGU^GzSwLOD?~wIQr0Wr zTfq>=*fqf8W5?3;WF!p1nGOyPGPouW*K@MUTx9eS5C1rK?ON{*3;)2N(DVN`D1`UE zX_znL0kZ^lMP8R(+%b$uL{j~RRKl=B;f*Z^TF`!l9fAI{ZCknCDPg9t$l9(=vhAIa zYGMkEDHis%FBeMRxcWE)=YLroX8d-`iFE5OuV=`TR50!$5~HM^B#@X2LVd;o6q6rN9_0ts z%(=hI!s3}xkopomyY{n6|Gu;3@J-B6EjrT&pAFmTftZ;Iz`-K6;5~WaI%SX^PqZ`+}`2lknBbcDU?zamLmVMbx3>4 zcGR75`Tx94!iz9#2sP)E-=4#Nmf>8yd+cT*8Dk6i-}e-X)#Tr9ynp`j|9%v3%1lC- zE{5-fdcQ|~hm(#XmcRJ@B|0kGzxjfWN?3R?ul_IJ?Y|gF{?m@q`9CjH_~7<=9bdk~ zp@I`#SP;Tb`OE)xyZnF4?f<-6i7|{FbOf$(JPaBXIcxsn#qBq;2~Bhe5IBJL%?%As zO*I=Yc3i#4ITJxnU~dxKu@2BhgQFSZ^8k)%0`9s;X7Sx%%jA;Fi4g?vZ8kP^3RO_V z0$-3tzub@&zHiIcrE?yNxXvYJvtyp}!so!g$G||SXAl>~ySVDLi17sv_y8A(nMMqbI z^2K%V48V!LK?UV#lLH6hDL6o};yMHXOv1|z4g2=!0rwWDV)+WhLmFKjRm}+b9Xp%ElzJJFW(kWO0_z2amY~vQ|a zC&u97Ms)p7sRUsY!*M~(_)p8tE&ui{A!_hh{gk(F=ZIdpd|5dh?{cB&4m2=zCtXpF zjOiiAaY30TUG{=KvBl$@OQu}-$BK&jQC@x{A_5^&9KCnYAbaMp(Yz29MWJZvD|?Go zfn&s1V>YlemP${)f1pWpufBb?FtT4Yh^Wk07w%46-oWGo76wK}C>xLMiuL6$F^Kxk z9RuwBN}PDw?Z7^(smGGOQtpG5!E}IDeqf;i${Ycf?``tLAp-zw!u^t#6Xq3%d?$aB zgju+Vbed@$?l6(cu4^;8YYeY9fBpJ3+k_$q;S9Giaxk1{s84F6~lN8J`_U?rGNhBGh3#2B-$IAE&KQSYaHC`KmAEc=FX{SHT<2U+dK~+ewCQm`>qOH9uHID z^Tl?-eSxN-sAzq$mk`C>Vl}0_5p;E%`n!uc_6%IOVRb@Vke=Dze_06oR{v*cFPiBx zmqPaMi02m8{a;zqRzS7K!W)HKA|mpZ-~En>9dW;)uU*F z;XksF5O@4i4otJnj|&)Y1c~jj6^{mYA7~xAnc|MxM=h<-q+{y$Qv4d|U#BHS7`#iA za&_>A|H4}7Om!ZXV}^)>R4&;2D7($9gIy<4R09kACv;WI1i#lHPR9GC4vLiOcEF5* zDN0H*!-nY(=QNJ^xHz}Q&8!J)mI<3vepyjUXk=JeYI^zsu~$h+RW&tKabPfN90ztm z?nq{`#>haL$7kn@hM|JU*TZp24IYejA^O3C#v?X}-bVDqir7*1BcrNe_G|2a5AXAi zmCKi(ftb%i zXLc(ilTbml*1xYG>r{1h9{$2>*K|dh;yl0$cNWWx2)^gm{rjiqTp$lnQ5T+olVJQV zP0u|K9;4e0&I|>Kf*J!R{m?cv`7vGw^Z1YOo2xeufF=!K_yXPI6?%9N2XGG;k*&L4T z!SKBNim{Bp;x~*t|zNHwkOM!x)1JcCgg&{u@F0v+l>EK_DYzNAtfiSfpLj z4H9fycU|o>1mX%($^9L$ao+GWsth{D zcJ17G$f6keR6`>>A>kY8l9}qTh{>CU2AdUmlr-pD5E&S=)N${EqJ;(!c(%geUZ2THR9o=zG4}Tjxdf=fVP0hn0ylfse>ET$r)`^xVus@5VvF7`6$P2nf6(>yasjsAR0dwGc z8$J386d30M?9Km{oS2A3_AK-}G&g9842+?IjnJM@XLjW?A{@j32Up2qPJ)c4q%=Pn z9$t(u6BJcd@r&Aw4gTuYe5igXG$hV->v9oT3w$?X7#%U<5)Y(irBaB zQWtT#Q`OZGcbWLl+?-{K5*UK4tcr;k{T|P`UxknkONqKNzda zEyEteF$U+($v!?#W7it|^6t1xa7^Lv<57lPzN}7bDq^eBUF{E9Sq-n1VrAXIGqn6Y zpw1#>ZO;v(sf~h?B_qJQB4f(p|7Pd>3)iEH3KItW7LJ6SVwzL_fN?BndHDib?4R%Y zQc>Yrnl({S*$vG{@1||T#YIa;Z`Q1MjFeOOc-~aY~EN?QhiRrU43n0 za?`Y3&5t*&%FUu11A8T3o=ix*=Fm0zb=?I&?zwZOG^2)xkJ2gmt@N?08z%LXbJt9< z?UIcbBRrkn*X>>A$@>#_QbnG>d>N{~{X92${-vDkY_@VQH6Lt&5G&kJp&w)0=O0+P zIO&eifV+)w!X4squ3o&j0_>86q6SaPcB6rUxF)?%e@MwJ72ML*Wdg=nZ|ex}UB+H54jj5yk&_BSWwUn1NP{ zo|h^Tb?KFa>3E>vM0kTJ6NqL(if`$YHLA|$u$H#8y3@1&;$B^>ug?sliBqQRrt2W` zS8SJ+29BZ?2`>L>P@Q?r;h)A&1f@L;)G*HwoX^9iQ|}id?|@ryw6^_qJy_EoJ;9t? zMz5p*iy+qvQJ1I| zM(GD*#%}v|$^=1;C3)fedC$+dQa&Q!iqe_=yLda=&Jwjg6(c}0SScqh^2uJSi5qI z`#atPLh&oszKRMPYiqrQ`^>i^j3bNUs^kSO?e+WCE1`;(jNR+3~p9l%LarG)sBRwaFPTVhyjS>vL)Q%P^!r2(?Ta(8z zr;WTUGvJa2_kO{L(cC#&;uV}&Y?g$y^2)>i)gp{y(m7UIl&r=`s+ppUn`o&1O zaTO9uSfq?S8t`~PunpAJgvWR9)`52_B}-=bDBQ}%s0%9J2@6Vn5j=Gl=9E9=-qZEM zIUiV3F?lyPH67Et2ybj>okTGweyVrKUoXClYaLc=sda@QN!eU!j{g-6iD3j9@2zD! zcD-iDuE(UxElMKDa&!g;M9D-fK|-2bh{J;P4HV=DQS*Kfzr(q<=V;cOH{SCvaSn+i znxDY&W0uw{14RQc;G_{63x56-{*JDr0h=k*KEHpz6YNK1$5;R8;lmFZ8C|<|gRI<5 ze^$eamzE@lBZm*?+%$_PWAItnK^VLwu7h*SH_bQrDpK|TIHEuWQ9PidM zf825|*xu$oZ~lBfb{2M;v-)|_R{gZKT}@5%`M9X)XfG4db{%+%zG44nEoRI0j~F&> zk0}bw)~(|wOt^u4uDR|O8ydGZo;zLEHWxhB@S17+*Jmz(X3!xG@I+F|($7D;)A^&?FZ6ZU*`t77n4NXLK980P_9L?c287WY~DSSs6St zEN!r*4_w=|ZoNuKn9T>#LoK(Hs*5Lv5N5y;5(WmlnEV8LhmvfeA{eF>C74(cwj^ei zQjxQ~JGD5Y8;I8Q?892)e8AiRU}7KQ@y*E2W^mi2EGfFOq=pOu@h(?9(RLWo6`ySW zj~`bfBN@=RjoXP;E+&FEKsE<%@UJQOaav&k1A}ef=DK0UNKTEf?hEvT(I5#!>#wf+ z+ghzTO0qXVs$2Pa2DTLx7h{~ef)xSZ9k1oDI1^(~q*9Tg`D)rXkV09|qvhmonJ4s| zw%*q+NyGHU3cdo35tI_FbR-tiB^5YGCQhk)(dT9K{)lO{dhWys#5Eur5bwC2!hvIn zVdKXebEX1;h(0_XXiaUc*MseyHGD+F2v*=!;kXY)W)dv`O3-H|@6G~dYF}}A`2_y$ z=+PT#tTHq_q`kXo%e7Jdxv($aRxWzW zhrBDVcVyhO5+6GB4UPyK8(%RviR7dtIV~6Z48U}JQHEH+iCC;(e?ar2?a_vUgkI+> z-XtcLfIi8xz{6)UU7@5*9>>Si^hJG5kLMZC6c8Ib!s$_63BJJBzV!`)K@Vb;xe6w? zt^LoO5zHr^XW^tV=@rt}g4bPGO15A2gR;ucywA*3n>g{yw{MdtPLz9CEUqKa*9}$c z7cF{2SR?Ub0%d7Kr19$w$*0Bf%kl34ulK0|_^cZ@d>$lAN6ivN3ML~J^veKhf}Y!Uj00jT%IAtV_Ws|^gMm$ z3|tC{@A&*nknJFgn)=VJHgN_QvS#g4+70aiAIQK=_d$ymE~Jxdt%-?}oZLP!jF2!V z!>7C$`gs*17@49f3;|c2Z><$A>#qk>!E$eivQU<6b=K`4drxopF?j3Lr|FoXaQLvR zXd~eHdgv&dCw4O#qdQ{;c5@|TIEK_zQrK$qYgbu!DIW@#=(Af3%1}XcMCoAik(zv% zwntlgmzVGVAJ+A; zA^J7U4TX;?a4)9ol)bK(Df@b#DHeS(x_^FsTfh?qgn!YW|8_*N+k<7o2b?@PV#9V> z_ewL?kaYgH%Gt@M(987<4E}t3FFHCsGxO9cYW&&BazpaBOW=|nmc*_cb%-lh}x{`?sCcrM=a@?}Nho?H~(~6N?dc(tKUSrAxEl;~KT1|9Q`x zkvfSON*d(%H2xaF5kTn!+TCY}qA0$d*hH%Id^>y0NvQz?W?foFM=~KBSJdf#)}Y~S z+a}me*08eYpc!{R4w@;KN9blE-UrwAW|M;c))=6V54ee(>F+#DaGy~Ct8K}O3u`|% z^tDIl;>D5$MkfgtfO~7JF(;?SwXMJLT;QAca+C-rVH^XV;;PiP0RcQNw?Tp0uUExv zTorSmxn(rXg;D7vnsMpu-RL_n0hO8U#7Ke!V9}0ryn)7 zwWV9b84D*G6AlYwLC$-Qrc=XBA~ebT)-5U_rHS*lyN%bld}Oor&&}rM{Ra>Bay`es z>FJ>Y2s1a+8`L&N>?U7LwuFG`m(x3?#o^G~2ISDKlxqLfC}`3yuT^4vtyK zF;HL@K~oP~g3GGXh?M|!Q7OWyMEp!<5DgDwsgcoZ=~K}{2R_0%zH3%Fs`-E+Lq6uE z7eKYyoxzeDQ1KU46x^1%fBbMbeoW(xe}P{Th}x|=s^(L*7EIc4zoseK=c#G_#v#w8 zHwy!N!1u!kqGXEe&?$;*o@Zg%ej86Ml>LbV2Rc_*3{>vjR-R|1AU}RQJQhlUO}z=; zE8X4}V-*#7q4ss{^r&GqjXUQdyfigA6nP}!WxJ(qy*@w2 z(gP3WiZOH?@^j?=Yg(`#K$NL}A@hR^ZB6LNGcht6QWfDg=R%+1gQlKE*O@@K7UMsK zK>{W*Evz6cZPN37Vh?+Me*o)JZX^E}O%c0xO`bTUw;Xcw3S;Be@!@Y~D2^F3W%A^u zS5}y}t4eLXX|~PLQPZv;Engw~_y3_&0#E~+!Q|bEic0(NVPuGRrpW^NyNMg!24&V? zj>LP(GRND*4=&`Ry*`+DpWd!sB0a%kgS5W!+e*owkU;V6<{LJANJ~2w80e*TGb+k~ z@Qx}o(UjiB<;3cN1DXFtyV?E$d}#{cH)|i^-9qWqI$%t@o;#zKSYo#vYBw6w%!M~^d?Fo%Uk~dB$Ks8Ws>XL(;u`8t!_^@>-y=r`NJ!99;TT%X z8IHN&U<57Oxeca$8a>J=acBF@QaKDd4?cc;J=^ZJ#fNLF(eJ4RJ6Zz&y&<7Nmv&sn zhQ&|`de33-Fq(x^Xv*1RCr(^kc7-)a{^5};Y;f-#eC?XRYOoe@?O~G%4Byzd3uQhe zBJB(TXU`g&n6#q^0BWQ1rdICy_Vt}sDL@frnLNSMfKECRKJXS3&Gt|vL#e@7Q|56t zTA<8V%`Au7Bu)mub*qZiPcCL<3)7;JoZ3zgS>fGn*%km0#x^`im=U?M(L_+;@;aR8-fbV<`zCqkKTsBL(?+E<77yv!JysiK+=>Fr}L)TPa zXuN=c4}f>Tj8bkkJ#R}iSu9L=+l`EmZg(=v)CA2E-p=^!r z&89;3891;GCKwtChL7h8XDH{+b@KKC%|`M%a|T{&(cAG}Vw7J$zki>num58Gqt3DS zW9B-u+Jixhp`nYXg z64gaV)YlSuMIjglXuowr|@-(n96hZ?>Mwzq+(kZtxx&37_}dZmECb_;H?`9(5XJ8}bAr z5~$pe_5fpWPIEqW9XV#qLrPiNEMO^n_2@yPx!c~LNi+5ItRbQ?xgf5DgAt9QEat6U`-zkf zF&xa`1%)kHWN_j9PoaCn+newWCg;e+@@m_D7uPxWA2=|V6V|Xi;TD`X{8&`fFS1$X z?b9UnU(XF$-e6=JIX07oC3+@s^25TBgkp5j{#L?pSlGTm ztL_ea_FyYrd*Aj8P1IESs0PU8a0&Tq(SY1?b93Q8IICAsr@1B1SBxFVmS+9Xi-1F( zenBSViEQtua!{uc4;i|>EzBG;0nRH_Sb^}q!<+C`W82#txiCtqRlCQ^%SR!%K-r+S zL$YlHU=v;${(+Gxh@C*>U>JP-uYk5FBEti|1KkO$9ySp!p&Vdty(&UB(0@R3J7n|8 ztUW9-KHiSQ6Ix#X7~5OO9-V|PoRWv}|LnqX?W5c_$g!s;2&v`hhnZid9q7_>{ib$m zS>7T2@BsrHi0;53#xG7uJ6Pa8=R#R_^vk@w(1xgXHPPO^RaI1eMA|#;xjAj^0JnV& zFh1f_8PtK#GbL#9Pxu}M;|X!twQ#KcAA?)866M-WwurAf*0wJXCPOnuQSX%HDm49`J$j%3peBk)N|Ob_zC5ozd-q~d zq8WtWwzU-vaf^(nR&7{ZJm=xO4zEnNve3zg-x&OD_W8f9M}k{vPf$NWZ2q?{m;bTd zG+GLp)vsSnL}+}`O2;6}>@Cy+>sB~&04Kj*=9a{14mAuOt2lCoX-+P1q?ErV{F9)p`pHFu>Vi&D?%b8D@e%PAxNm4v^15p zH-d=WvV3NjY!9J}9*)quzLB+usqoIDM;{qk5FYA+G32PHr+~d@VWQx^8~$uJe*2G+F8bmU5-XN1qi=Ee-KUWZH}E`xPTKR}d3Xk~hs=Xu z2Xgf4U7Nh}FJG3@=Z6r~^4w9lUA+gkM0~L=Vdd}qevE4A%$Xtj6vAj;(|48#eincK zRJ}pAr!f}rOkEO&@a~8tlmy4|$$tH+JMy@=4&qMo=Rg_}+0*Pl_^%aX{*+s>Y8Aaw z`j?i`$O_?`Gm+!I72#sYJgM8Ls=UG*a)B974dKW-dTcK8MeRa9CVvaLWItVzZ`*qw*9 zx^iVB)3&(^QaQ)Qq61R$Rpl@Xa*u-ph2ob#a37O*KrT2eljs?E`?j1E2hoq~9JQwa zeRX@X1kVwbhFJReI0PE-0u&a#@+#U9pAC=4Bo-7!c*a??4m{X{R{~(6D>`)A9zS2- zez6GRN15<5VFK$UBQ0&m_U&eELy{;*35m2oQB~m9rv47tZF*w^Lyak2>HDL4;}=we z+(Aa0gmxc+oQ*#rE$tUehx^SV5p6YbtRE^f`)qr^mg&(Wwl1G1D>~1*(=ibp>uwX5 zjd$~WxmSCd#PjNBpSO1(HoLEwkJ6a^w-rSto3C76xgx)}zqR$SWjZAz{0GW=ZFH}B zbGNZ|VlhW!@7{2C#F@}CZ>1G|g5W?&{OOalgv7;+O-%09LU+Ye zL^=yxe3-!za2TWH11whu2xqA5^+0Z=%B%e}mx)_(v1Sq8`@+X@jH!aP)@feSkMIn|-Yv`11pY(L8KS{r$y?TK@lJI;<9@@*a)iW+CMlV{Dw(v3_hqwpV=;SVO66$_u?aDHyxGssv z5GI-lN4lcva-zH@CSaLFx4uT=JTU1ixQeqDN`NxhDm ze+k<5>WVQ3bO~YD3_DK(N* zms72`W?H9`|pRTo7q_010qPhFBD@VP&_WrmMPER{neZ*N}r2%9rLI^-- z7>z(H*XY7PTGJm5`Qx%+nhc>Llb^01O+ELs+JS-vVHfOQ@<*F&Cem zI+;IJQd-=wj5=<5WnAa^m!hyML%YU)TjU)8C!Y|t1rXt7jZnq@`5J#iCrmg7xBV&~ z4TTdm?o4oS@~A|Nh2C@0?`&S_GKyLDK{#U|3s?aI$E~X53}K1|$A$|Sk@D}gY?AnR zVO+5*zQ$@fVU!%T7LEjE8_Ku4*PnFl-u={x6Itcaw{A^TRMdNs|LB{@b|Atm9pNzQ z;cu3Jj*{_RU_l9NnKUude03cQiKewVRN$_2iq7-}y9?7HrgH?Z-M3eNetELGF4J1o zaQ2dD&>7X|uL>2@IFSHL;wHl_JehYC7@FqtJ?#?Kuuz*uyy*}WjSDyK-P)M`{%0*f zkGUm#Ycp1Yu5nzCtSt~cr~NNR9AY#f&W6_mKc@abRfRU*bSEb+ZpQ06R`|%_!?QkL zw>3+nj=G-X9rlOj_uMDVT&vRB`<1LBqMHKx5hkEbch=-DBC-;WGue0)4*iBr%d%Ge zr8YOmS;@XtYYEbiw(*N;y*`>MvnNgu_-YmEY)H}o@M(3i|DHyF_BD#Un>Q=>bsu)~ zbJ+Y#4>|3inOVK`*-@{dFh2OEPU6k^^Qiepxfn&sL4#HJ&vayEGyL(jCWA#m9S*Pfc%rf~>aGF!+W? zep_Hsvi5T;^xW63sF1hmb%!TBAWe;cdCGgD=0EvKySYyEh-v48zl#Arc=lQQt8nT?t2 z(p&~!jo3E-(yu_Pkj50(HpXBH6TpN(LOx->Ig&GhXT*6BF^EV0E0pR-_p`(&!#WvA4%Vh3=Tx^LxG=95n5oefF$C^4;$65K+)Az;S!)O{S|D zjLBGO;=Bu_5v`4n$n7joCn#C;;dEkZWdMhlvCY!n_ckg1n{f4m{aG`+hl@Bzo$GLuQyD$mf)@F_0QBU%bGB#G-)6zxVJV69!UI zD;-M0Jjil94eraZJ^B0kX*I~Ish#o+ajwgLRQ~YwCXaxXzsN=oPlSQ4Es%uAu&tqE zt*K*ndlBrbYwJ%F2z8@j8GIHEMM?c;{H$$a!!ictw2V6jrvTh~J4@aH7^}vQd);@~ zwEG;&jG2LDL}ur4w!g7)86YJ!UsvcJc5d29YO}Sy`BO&N0((&mapOr^c_uDB@@?ct z%a7acmIaIth}fNTXU%nu;;LQ&5yEfpmhC>(nAehDZt>BaQGB274li24fV`u5nj7s5 zudSL0f9uj9_}yD(@wxP0KJJnfb?wxEizeRYk2cqJm_bD=Bf5MK$h+8q?#=cDE&doy zD~^eb03>g3b}{(fO`yc9{vN9<{IbI}zAk@dkN$QYbCDm%MgE@RyQkCd|Lgrd;W(|q zVE^;A9piZa?|-+?WQ5+Qj*f}^t}efiM33wB`zYxdQnB;>Bs;9Dm4EQd(;dSt|M9XG zrvG*{HN^GemmS93LB9{Gm98GL1UT zq(HPsR#k}8uOzTfW79d%mk*2KN3DZAYAxQ%Aolw4#ag3x=@CR#6cpC8;z-+dwLLoB z?xW|oTWFJ~L6?j{J{L|&yR*rFsg7(cXXmx_R=iYlZF~(&L+TiYe8OD1zJ2EznQL8`*3Ofg!2x=rcaBP~cj{1FaoyOil_%ngxeIIs!qh^?)%`1jf-wLB+bsRNl^ z+uKs6*Av2}#VJ4S68+>{o6gBOYKQ<8IMtaz2jcCrZM3uTHh z4?ZU5tBA;i?{xWs3&d;7KC1mDd+GK~{aK3@6}^A5WN%P%dwOUa$!4A|6%&;v_h%W= zWfa5x2ojk4mxV2Hs(jj=9PM-DNOZFSNovPo2hd6A4jN-wo1TLgC1-r078p2oZ#<9uaMmgof)zs>5y&Tq@f1n?E zvxZZ*?4-jpfAUPwHQHH&?PNF?5)p>%=k|~{ag$P}RdWk*XVY;k90%m`^ZGq1WD?_6 z`GS55%shQkpX=CC_0q(tDgh|j*mSJ3$o+l#;jdFth+-R@oTiCmG}55WUTCF=ZFAsD zgN(frtyQ&fx6vH|;s>(r;f=S{D^wbUo;mY~F^cd%5J=nb>||Q?&R;-;*qOPubn@m8 zPMBEQn=)hnK3a?+m`?ObOg+1e8%X+Sv8@BjyVORQ2t2Bi!BiE{0*#XcgoGQu>`L3O zQ?ZH?hY>>QNS&*z%c?B`b+yxT*4XavT;MgTjj;mE16ox!eEj&W&ppL;YmS(-6dWA1 z4jzs

u|Z_9`LzQ#fq8^N>s*(_CJt*-uK<(~~mCN>!@yn_0Zj;O*ndnkV1Iomv3Z z+XNt>x3Tw-Dao0xQ$<7q8;*TBV-u4U>BEs~UZXVan+>OX! zNP7bA^(mxR^smgH*r6JZ#fA)E@)=K>(sgx<60<>uW{8Ln?%nNpQq2b=8_2J@OdI|A!P`voc+&IwFWol{-P72o;|n)kmjjIa00s(YZ4?W#`n-cxWHf@+)5u zD+0_Vr!LYZ&lZ|w(O(f^Ov{x~enjrCk6EV54NZ?bIrZp~wg$(GHYFkTiwe!j;ADtJ zh?!b!Fr?Z}Oy$F`Ok@&Eg)V=emWGNjOPAV(Nr|p|`9hyR(*Ob=8E2|od8M4&3P;@X z8iP#8b-)~A!1w0Y=~Y@ICF4pZed!S_z_8Dw*=i->jlssLD=58PiW$MM>m=SPD|nTZyNQ7Nv()!kJ?e2rM!!3Gff1CIu~<3 z{kuQ_@EN>kpeJ8s5VJPX8eR4af*27T z%8u0Yvs<g-I1=%@8~n`NA97$EQgsdK@Tbeu9T`yxJ2LKmE?05=*~5O+$ALzy3$pkz z2NiODOS~`}vb3Px%zSl#C(QUwo^NUC7CL%=7XEP#sGF0s=EY$7sI6wJKWsW)7WE)u z)4$$w=wwE0TPrObnO@;i{|G|>6>mvx6L=(vv4E*M<`B6J;+Gvw zLP~MYes$ZWGe3R0ma(bU;Yp-#SCMnZ3{eES!>%stexlO5&vp0G6k8+C5%expzmU2L zgWIwzc(EHtz57GNE&Th=hk#|2K1ReG)R8O7Gj&=&D?+|-(x=^UO;RtnQ$y0`2oK+& z@`p&J)SWd$iB{6mYq8|)*f2ciR+xc*)!=Q9{A4TQwg($V;6(%g#Apo^ajR2QRb9dU zY+Dzk(D35Whj}el_ZQ9D|M)sL(B3$+6R$8_@b&jAI=|fR@MPF5 zIS~;pwjCFCT1xn&UHuyeZ%gNs^7CA`h`3GBUAVudcH$qRRTaL9eFc&?hfSq-6*w3K%c^&RsJ!&__ZFXp88MNWJ+=UpEA(5v z>&fYhicl`+;@RMwWvBwYHjx>fRnK|8gc?A1K_N8%(hlk)ilq&!2g)5Q{u@fi3Dq)_B3&AJ)3tZE3>M#bC#N{n zRg0Olh~bPvf4na8Uh|p*Pb)bofg_)QMZ)NCR<2VsGHC)%W93xx_g|$r`19+-)cgN$9xUW zj%{zXkq2~bv9=cN*DuzoaR-tqP3S@^!Rpn43<&t5U%CD=g~JTZlm7k_ndkx0MtNuh zwL}cnWjN{OtWY2Vf+|I>{%K`7ltB(e=C}*?mjDiHntnziGCT2rg_4){@W6$-x5@Db zo+^`H8CN9$rn*Itf17gKp+KFS`^0(CLsQ+_?=jE&sQK~G3|VThQjTtg7wkj>D6L8@5{cmCE(!zbNZmD|V}u+dvmQc{Q)jxhGjZx*LR_K=Nm zSQub+jxHl^oj`)v!pNMP8=vvO5bbg@Gdn-WjhuaF^Qk0@AQj7=;?7kGg97f&zH24i z4Y&56AX@4YYn4E{5MKFe4M^pikc%xn>Qw6%Kx~M^ zkJ0i4>OKMr!6dHLTpA6t_RAZ0D)&wIVyd;LXP%XwULqF;KjrK^ct+o9iPGOtT~_Ot zQ252Q3sZ|&Ie0xm(Q2?S$!Y@IJPoCcORWIk3WuK5f3n+c(wrIFc88h=qOdeQJcR?C zt4_tX9Q1_g3-b4`g174@shkg-;1E(d`9o&w9KK&-PYOfVsbFFo-`LQ0wMZArqgZ_c zlN>fplgK&D9@wlJghjL3qr2pF!_6+DePc}@Y|1u&u<7GA`@q=I?vIXdJ#rnn9u{cc z^gtv8V&}>U9_u2RpKZujcW$-sBx0TE!p*;YS<&TMwm30$^T?t}TP|V9xO+EX`ytOQ zMymqu}T?e}3e7ia#C{2TvKB>$r_{;YFp z+4r4OO=P-?%xJ!p{#{pVcE7x=cT>IEd*3tG^RqXV9LY?}u&}3@-8tUK1*0sp?bU}G zK2w`=dyTwy{8p*{Ux!K_TB;~P{Ec1LQzU-?4T=s=)`czcoaZNmXgwi_FK=oRSu-fW z^3R^LnY#m2Sr-Xq3!6$F6L~j*R#IF`Pu9sGaV$|>^JFDkG9j8A1`4u>R(-*3F zQ{w~Y0$eYE3ppR_Q(3s65{DF#XrEljVqa6j{4U9LRY770b^Jp^Q*5T={1)e*9yMWd zEkoWJ1f+1DZOq7+^!f!_~3F&1v^a^_D4)N>r>^(p9QrDyGPvpIJOn@caH$EJO+0lr$9 z`&C|7EzB!X>R7W^XolgSMG{)>X>t;383wV*Qj17$V10x&&k_JAvKJuD2Kp3wmk~{P zeMzL;DsZj&xSz z`rG$?N-34SL}VNLzAqumU`E!-l3k*Zea%i%lYIt7qA&=dkUd&tuPjM+A)+Ws#{RvJ z&-47A&+qqozQ6vcj5BA>obz7p`?{|Cx+6A6U^lyqzx?ig29f9k5G-h2An&RnksKkf zg1un@C@`>*gA95T3bK%<4`yZn`p(YH!9oPsV)uQ;O2`Z#DdmKy{=_k;FoC>gt08@& z49Hoq69l>sY~R4Q4^*K_-cvV#P=L*P4pgUXY;Hm9ZfoTxJ8RNF+hrptH-yhnVY=ML zQ4?wktJQkVIShvUXT%c{_7O<&kYq*&qHc&*fRdU1)+_z z6@0^CfQH5cjv6UPeYh_XhyhsEsZL*u#J5752PnT%9e^h?00xc6kF%c*Zv;vLo;pb3 zRiH-Pwj2ugM9$vOM?-L!0bjg%XSE#aX^?3g8ta1s*5J&U5C{@Qx*#WpM53<`7v2wa z#2W0d;G4Jz5H$>4K+&KLs1LMMgzase2H2W90NNn^gS-N?x*IKwhH&65ry;Qa;HCy| z4KTd^N1y#-X|94wH!C!dmlgSa9$LEEur;Q1pwS2_0B{J>DF(hYq_5D#kQZ#zFQ!A2DF9pffs6nP9@yS# zcYOej+XGJu;9kOv0VR9_TN^lXbS#BH_=Twa;8{Ux0(l%acV2J1=N^Uv8VdX)5Grj6 zo#oa;gP0hdN+7%}uZBcjPVSza#}`;BBv}&xoC3NC2F)h^5lAudK+lrFKIJf5+wu?I z#t3LLGpG)Y6LdhJ0UY@?knaek#r~+Xe|a59JxF(>c2W0Jvsv zqTmKZ>cH-RAY>56K$ZY)jkE1+K=%Sxpz!VSUtt+hgeB6ho(G|)AnJsutrAWLUj_|^ zfLlosThjm|faC!}QTTcbP;VIq-86L$uON&VI9h@gAoO#f1p+*CVym^<5RIm!hZSIT zVIaJZ2Zw-Ro{R0)A}1)QIyfm97=cn`EvFth;^mpBS; z%kJ4Ni_W{SOz`GHOumx$3x+Y2$|7hb1WIq$<{^Xx;V2a5gHC)0t`X@~cZVwF{{5|y ziJgGI!IscE#NK20M7)Dl34}ia0@wZwU1`{LgJKFmt(&hcbJN3VL2w9h^-*@ER6MA} z01tvm+O7Esk}ZgGci|I&R84>I0AXGrBkQ_oEey|vXAqW{5D*b8{$cqq$VRGXGw`TT z!BJUJ0p*P_^PzvKQ#dWe6e3+vssmkA80+6B8)z@&9f2i<5^g^kn`mizAgqn886{0Cv|gPb4*wCW}LhRVxO(-*5q z4RM0n2qcYtqI;X7ZoZU?%Gf9o%JOqGsdmho!S@$NGeEcy_k@4L)AQil?{mtszJ=a= zr$K((DO?VG0U3k_5K)8kXhmKPg2@4D4l}bBknLXvj~@*zz)ZcTR{+Z>Xhzn}n>UV6 zlo<(t@|A;?wH^j0f=SEVgEWTT81^iVU2+>(SNO6F#5)ZEtPc>rG;E5d0!$#w0FBV> zgRb;Dcfhy>*Yz%74Ky(ouqMOWXJxwxe*+=e?L&EA9u&h_1~ov@jWmA%@LHr_2%c^g zSG-w?EbY$9KPjoqLv?3{sz1SEGv`U2N`ewZ@Ua}c`+P^5+b~+uUwwD5%p~LzE$xAnF z%Ms0L$nURFR2c^2P=* zX~TKo<_S&M*F#(YlRy@itiNHDI%pC3Zx}_^oWeN^34~Q+-+JG-EpMBYg>HjI1Fp|) zlUPw|SCOF2pZsbcg){HZn4>-Sxc;WiWDq{=>mEF^$M|vW=>Y#jjUkNRe+gVsOM5Kg z9y^EJw8;biPXzr>Hn2bRU-$KYVGRFG&Hd{X2>{J>|A+khzfO<*E^#_E3>g3vJMRH? z`QI)CIkG0+p~OTe%V6%?c}U8d!RH6W1FOaqk#rEtmtFdA3DM-o0OWx&kiPjoW%)Z9 z0v+G@;FbLpJ*QzS`nDPzqXDg8d+rH_g;6ZcYz!Xjs-TYH77%z;RD>-byeb1Q+2Xf; zxX;XfN;aTpvn_|M7HC6}Z|1Tz6lW2X%0KiN@&BI%?UThsf}vL*>uU+!eB#BEu)$S` zxB>`*WIt!=ZjcZ4Y#;-nOP=PJ^c^R~#MeJYAz8V#-oU&}|8Vu!UasR+y_f4yJd|B2 z*-SMSM{vV`JTZ9k-rvB7o#G#$qT0VwOc+T2AXpv#?zD_rvKV<`JleJYY;NX!TpNX`)qZsBID(blE zQtY3wXUU}YZP1c|OyvvV1gqT;$PG{v1Kx#kSVsD1O> z+H74A%In3T$%WP@^NfscIlt+tqO;?;J+1kBE1LC7l0%%bLeVWeHM~Dk-qiG~Z@{2w z>Zy2bnq_0kcg{0HoyQa#z7-4o_LyjBoSfOd0%aUQ$*=e$kwtn^pRMf+hr1jr9UHGS zYwcFv+w7BJ?u-{Z&L76L&OaNzav5@Qa>*8tzBZ2z`Ih;?oAKIWyO@+Sf%6(CR(wKN zT#xlFjQ+fI6RuUY#oO9Ht#?qTx7yU4MMdT?nv-u`SyuAsv@6U5Me>#g98{&r*2Y55 zX>&=2I9EVlGn>k*s2;0G(-3Ak=edIX-m`XppH}wsNa9q=_hi0l`%$^r#hfBd_!Y&X z#kbJfgkO~N^UBDBq41dNJv6tIAyd&+jQq$+9AvmV7S^ekJe6eFI?si4Ggdo;;^C}2 z+2MF8CUD`4M4QA>SlHoHtj6DKtY|#EG4ZCl;c5xXcQetg;xTQ>5Qr3k& z#r^Y81P0-RVL^U=&~Nq$$k3ql2g0x5-!@YW`+|Q{_c$|&iWWTR{H?{D>92Hmr{gj{ znE!x{*aud9a^VCGq3GQb>eR?!FgMXM$)VxtNx_1Tu@gBk44aYU2!Tgg6E1_Q0aE%Q z@$lt1P#=B)rgM4KyRO&}XY_#d;@n*3+T{t=;9BF-%Y92nOrMl(zkVwbMvT4?Q~tj6 zxNj<9)PFNCj`05Cu|5wiquO2<`d;H0VtcgVG>pZciHMX2I!TQ;25l`s2m>e~^`KC z)aBkNGKu(OlA`-2o|QuzO{*7QyAl0oY8|vi8wHfD{FHRYL3KN?GWbeZ3pHMne$y_T zEWrt6RB-su8OUcUBsyh~QXiHryWYt!)_py6Uf;6xHlEKgVR&=EJzTNKF8R9CyDXQ5 z6egy}7o($xo=9jdWn1PE$bpW>O>7I!h7@DV=kNkzs+^L75^8Tpo7|Nn>g&qhO};iv z4MJNjc=jDK|2j%kS6NlYEYHnuC?ezQARr`}(6Z5~3!+K5J!)u33r9ii0G5IfYyaw& ze&6`J@7!!(2he+<_Ju4dttjaVOn+Bb~HyFnmdY~Uu#m7NM@JQ@Li=^%(kg9WR^L` z#=u5jgvwn~*?*5Up>F8e#Ow!GjUlqf!krWS9XqBS4Wdiu`oI{%Peo2xQj(Km0<8Mhe}Hwi|qPsq=HZ?gH5x0Fp_jmb3~ zC1Uzg(tbRk5}aoH{)c&8s6_|nVjf`h;NjvPcpjh_pgxeAOsFYkVgf?F6mZP&eqeG> zf_7J;8;~Q!)e}7dI>gDB(V*@Nzj+bTr)}uh0rG`i0GuHOYu+6DVIqX|p@V??xwL8- zjiXhS#`KLrv6^%}Hu|vvQyYsM@#jT6x%aGYX3F~o`kONLu2Y6IVF}^hF6T{zO?b(*>4X^3sBDVWyQI zU&`$>5%r3w8^@BE?oApS=aiSqrcW%7&KI1>>TsTVm+he%Qk;Qj2&QDR`S5XoMIjfq zB|oL}e(~p|wH=dF?}n1oy_2k&3DRyYjdWnpSPI(uN8qCRu{3iLL_YY zALChgJwC=}LGb6cgUnJpkAO#{3jvF+;qFj_IB~(kLJ=I)n|d9kTx3|q@247Er9Q+I z%dR~gvHO{Z+M%J93LR*&Tq2@*Hf{d)qw`lxACNfni7u`(f~IG|^wC<;auNp^`p*|%#sin+vKFJ=_6UzBM|``T zt0d6iXS!P>-^P+)jV2MDSP~!2)O$c`L?PQH@u4*2#S@utE3UgyFFUrcpM)SGaT-63 z=c)I$8T_%t`RtyP1z&!Ik#Am6b*mbB)xVYO;d)a*$dYA5%RL0Qb%q9WjK-*v`hc{( zz1MD(I@-j?W>B72LfIONCKz{aw%t7ND9k8YCPgx#^+*gh{8p}OGLGLOE|jbFF|bk( zkY3-r4J56E41J5mlUhqyPsh(`ngHf0B5CZOG+^owEK5M&o6`8ruv{?c@Hagl6O2!FXG)aGqglDKoVqZpB1R62z~ z-V0oDSQ;BU`5+Xw@p-sF2jX6x^8nB52A3gG6c4g@J1Iyh(Kx z92r{Xg&BrhSZ-Aa)F>{{GbW>3MGYNA<&Tb>QxT~_Im4@wf7EQfR{Ub#@%VnoqPcLy z?m&w!yzKe1xlYVy?f?^Gg0f_lXmlnciCpR9CyYX(yU0NSrm`P-Holt&nASzSm+8AS zNA04kLPirW-y9hOp)|!f{lH)JeY~gZBVTdhI2D3K^~VUD2WEZZf}SMD`#!PqL#HC4 z)4rP*akmZ#FdhB8rs6n>$+3r@Y9(tv9n<38pO&Y+{8_(GI_$#daAI+EB6YFB17nw< z1Ue45eT~rCJTWAAwmy z+Mz_aCe@b`dVV@zKHa+Rpd@sh`XJ*~v^^6wUqPPFcTM$P_O$FH5@z%5HfPD)oA!?_ z(c5Cx@oP`EUtaC9dl^vm{q0rwSc*^-(9?3 zb?Uo9BI8bZUe>2ET{&Z--HSFrCth6beqBIbGgp4^Bs*g4R+vQg{-K|2K$~P|H0;Q8i4RUE@VM$ZD&!N1k)g77mw-WJ2$8hAuELB=upDZzW7!IHa zZ+%{e2{lPsKJl3J9HVk5iuAn-Ns%5tr+<*@I3p0`uhWn)UQ64zq{y>3Vm|pO-71is z;XQm9huc0ssciMjJqI~a6ZdM{Wy)FShOke=ox2RAcq>8ot2z6Um>KZtke9;|oEw9i zl-E<-n|wCVSZwCC$3MFq8+oxEgv4s-sGZjhjp zdrzY#s03BZV!OKnJP(1M25_K-J-v06)*d9(k$zo|;ntHe*-u@%_u8if7Gv4kt5qU& z%wwl(!8NnyJ{O!=O&C-`ZaZ&Oo3y&wSc5b_5m&PF-aSrdj#K7r*e*Tsy=*wZ!_^f^ zpg`awwj~O0p%{(?5GqX<`fIh6#~wcSYCzY@;9U6B)s#>}vl**cWj?|cr!`))BOnqz z82uL)lFM#g0PyRjJ$TWeLcB4PMU$@vlnBl1+|yhW%4VVF1a``5C8Y?RyOHHzq4T7y zoScIL;zCV6mIoGQpP+Y(QF6k%UyYrVkTZw3AHcsxh&+>ltNm56zayrSi^ z2C4@3?jt~qAo>zIZg|!&&VnskJ`Rh9OG#|Kl<|O2NwA9Ql#p&f2OM4Zd7D*J90<-8XNiR4$owI?rqT}pUj z+U$X@-IX&>-D^f&2cqnrEZDD$ao?SP$L1DzHo!u^5m|hMah|g&GDcSeEnMSM*f|!P z%KN+d!?SPqpFiOnw1~588b9NN(Xr~_9w9vcAfSp~`QFs&aj(kq4n0SQ`UyWjZ;|rs z__LWjkDMS(xjLECiSKt77!~6|$Fi*ZD^-XznqKpq5up#xp>VmI!b+0(Y~);1m}l(P z^;?^!udv@s(C8;)3ZH05tR3G=rQAHtimzrHkgG0!5@Zci!KcXjAR|uE#&DkIFdZ0pc>l^o_gk!i}!8m zK3f)C(=B;^2=2QH+kwvHeVcwVcSMcqs@OgFyrYK=%-%*ePw0l>nRO6^Cd9l~H!OIIJ$c1xEFbW-I z`lV4A!l;Ugl4b}hZ4Y3|r1xgd`)O9_geiP;&R4W&NmYx+H1qI8^H=AA-ik3vk1mzu z==Wv)*rCKvRD5Uw>;1@#cBqEn%yvl9$RQk$W%bbj>`T;Y()i&|fxwcu+9p z*6eHFU~LDn)WzeKDV$hqJX7UD*;BWnGfY7@W-LRQoKsmT>DgdZleDoTI|e0Jr8G$K z#U$Z;-I^Ai!V+%rIQNFE2@?J8Ui?z zA#I5qJ6HrKy5_K>pDL3qn#_uoKvQ24uh|+*TfQ3A&wTnxz?Y5PnuAzeXEL#vC`O}l zt{APfAimlja~B(2Y8>E1&x?iWMXT&BjKUrz7%-Ch14Hy@j`j&QY7+u#;=-7 z2FZI{uW}Hh4%#El(thG>w0<~v6?OeB%8g)b8u8Q9oY=_ABmm4Idx z6c*Dtbb{eVRx|CgxAA^6pC_H`gm`2Dc|twi;wwLj^`IuqxS=yia~PldN3skWsx!Ax zZ`2(jqovS6eZe$j#qi{*Q59fu2SY13*3M)3)D|J}? z&-F1=N%k9?U0>m?J@0Pc4;E>Pj;9m!)IEmPyHVDn<6{)bl$bQ*a2+-O3q#pQB7eY$ z-M|n}n=YQNi-!SlV%aK7qm~jjXU#ustdio)94zkU?8pDK$Jo#1kex@q{%+S28yQ-V zQ73+##7uQYt-yfZLGh{%FM6(VV1TmW_IZ;gf@ zyt5)`v5_B~eCouBQaUX7v5N)S^8}VjU1_%JJKEVzeRqsA7gGDocR^2SUHx3ikePF5g6peTzXZjzVi>W1P)*Av$< z!Ow6wD(1WFwro+9*is&}(%5Zs087`t1n!2zYF-fT(P`pRnHVEm{8Fk!R7HlXYl_JN zE0z}g7>2A8Wp?h4XK=UFTPxzVYupf$$_htSl;z=n37?wvYevgZk{#oxmueY_Dm+ZI zK9agbO+YD#>nvO+rx1q)d`eE@(~)~vtH3-me_EM|+ey5ZNR!{HL*VD#5gNiKqXNTk zr}xY{;FXZ})9VJBv@=eMf1py87wNjJ|C0yU{+l-^ztE5OPJa9%OSbVlDZN5<3n<<0RSfiVn*UvCt{0VRm&}xHd25q72;lg;KRR;g<$~UPq!9-_BcL z5|U7GA5yr8j>maytnc_?UL_dF*L`Avt3nZ&c*n@`-pil$#w+6Ezdj>l5*bkIGU!+r zrsjykTS6|Eoes{67jH(#`=k@nh(W&2YKFvavv(H(34piS@;q9t;} zW_9X|Kb`Mii_M)b*GXn<++UuT!wjn$SlxjGIg=VrFY!DXu%S&N$sOllZvxp% z$ByLj!IH=ovoR|Vzx*D2Jlv0-jk=Znauv5Ku^s`b^+E+&t)A}{f^yPfU4Rd-#pO3@ zuXs74NB15J?Y3@$7gfzD+kC5_!b*U^JS#OX#z$1K?n^jYT)C%LvpGOC54p^1_pFwZ zPA{{Z6K40iwk literal 0 HcmV?d00001 diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json new file mode 100644 index 00000000..6d9ab96c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-api-key.json @@ -0,0 +1,18 @@ +{ + "type": "apigateway-api-key", + "category": 4, + "descriptiveName": "API Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Key by ID", + "list": true, + "listDescription": "List all API Keys", + "search": true, + "searchDescription": "Search for API Keys by their name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_api_key.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json new file mode 100644 index 00000000..d83a67fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-authorizer.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-authorizer", + "category": 4, + "descriptiveName": "API Gateway Authorizer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Authorizer by its rest API ID and ID: rest-api-id/authorizer-id", + "search": true, + "searchDescription": "Search for API Gateway Authorizers by their rest API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_authorizer.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json new file mode 100644 index 00000000..cac6b30f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-deployment.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-deployment", + "category": 7, + "descriptiveName": "API Gateway Deployment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Deployment by its rest API ID and ID: rest-api-id/deployment-id", + "search": true, + "searchDescription": "Search for API Gateway Deployments by their rest API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_deployment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json new file mode 100644 index 00000000..412b9a24 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-domain-name.json @@ -0,0 +1,19 @@ +{ + "type": "apigateway-domain-name", + "category": 1, + "potentialLinks": ["acm-certificate"], + "descriptiveName": "API Gateway Domain Name", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Domain Name by domain-name", + "list": true, + "listDescription": "List Domain Names", + "search": true, + "searchDescription": "Search Domain Names by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_domain_name.domain_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json new file mode 100644 index 00000000..9094090d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-integration.json @@ -0,0 +1,11 @@ +{ + "type": "apigateway-integration", + "category": 3, + "descriptiveName": "API Gateway Integration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Integration by rest-api id, resource id, and http-method", + "search": true, + "searchDescription": "Search Integrations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json new file mode 100644 index 00000000..c1fd53f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-method-response.json @@ -0,0 +1,11 @@ +{ + "type": "apigateway-method-response", + "category": 3, + "descriptiveName": "API Gateway Method Response", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Method Response by it's ID: {rest-api-id}/{resource-id}/{http-method}/{status-code}", + "search": true, + "searchDescription": "Search Method Responses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json new file mode 100644 index 00000000..e38a3a81 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-method.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-method", + "category": 3, + "potentialLinks": [ + "apigateway-integration", + "apigateway-authorizer", + "apigateway-request-validator", + "apigateway-method-response" + ], + "descriptiveName": "API Gateway Method", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Method by it's ID: {rest-api-id}/{resource-id}/{http-method}", + "search": true, + "searchDescription": "Search Methods by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json new file mode 100644 index 00000000..8b42b277 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-model.json @@ -0,0 +1,16 @@ +{ + "type": "apigateway-model", + "category": 7, + "descriptiveName": "API Gateway Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Model by its rest API ID and model name: rest-api-id/model-name", + "search": true, + "searchDescription": "Search for API Gateway Models by their rest API ID: rest-api-id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_model.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json new file mode 100644 index 00000000..aa66b3d3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-resource.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-resource", + "category": 1, + "potentialLinks": ["apigateway-method"], + "descriptiveName": "API Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Resource by rest-api-id/resource-id", + "search": true, + "searchDescription": "Search Resources by REST API ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_resource.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json new file mode 100644 index 00000000..6aff4a93 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-rest-api.json @@ -0,0 +1,19 @@ +{ + "type": "apigateway-rest-api", + "category": 1, + "potentialLinks": ["ec2-vpc-endpoint", "apigateway-resource"], + "descriptiveName": "REST API", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a REST API by ID", + "list": true, + "listDescription": "List all REST APIs", + "search": true, + "searchDescription": "Search for REST APIs by their name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_rest_api.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json b/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json new file mode 100644 index 00000000..b165df22 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/apigateway-stage.json @@ -0,0 +1,17 @@ +{ + "type": "apigateway-stage", + "category": 7, + "potentialLinks": ["wafv2-web-acl"], + "descriptiveName": "API Gateway Stage", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an API Gateway Stage by its rest API ID and stage name: rest-api-id/stage-name", + "search": true, + "searchDescription": "Search for API Gateway Stages by their rest API ID or with rest API ID and deployment-id: rest-api-id/deployment-id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_api_gateway_stage.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json b/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json new file mode 100644 index 00000000..1404fe1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/autoscaling-auto-scaling-group.json @@ -0,0 +1,27 @@ +{ + "type": "autoscaling-auto-scaling-group", + "category": 7, + "potentialLinks": [ + "ec2-launch-template", + "elbv2-target-group", + "ec2-instance", + "iam-role", + "autoscaling-launch-configuration", + "ec2-placement-group" + ], + "descriptiveName": "Autoscaling Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Autoscaling Group by name", + "list": true, + "listDescription": "List Autoscaling Groups", + "search": true, + "searchDescription": "Search for Autoscaling Groups by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_autoscaling_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json new file mode 100644 index 00000000..129c0164 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-cache-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-cache-policy", + "category": 7, + "descriptiveName": "CloudFront Cache Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Cache Policy", + "list": true, + "listDescription": "List CloudFront Cache Policies", + "search": true, + "searchDescription": "Search CloudFront Cache Policies by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_cache_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json new file mode 100644 index 00000000..7c0164eb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-continuous-deployment-policy.json @@ -0,0 +1,14 @@ +{ + "type": "cloudfront-continuous-deployment-policy", + "category": 7, + "potentialLinks": ["dns"], + "descriptiveName": "CloudFront Continuous Deployment Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Continuous Deployment Policy by ID", + "list": true, + "listDescription": "List CloudFront Continuous Deployment Policies", + "search": true, + "searchDescription": "Search CloudFront Continuous Deployment Policies by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json new file mode 100644 index 00000000..ec2973c9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-distribution.json @@ -0,0 +1,33 @@ +{ + "type": "cloudfront-distribution", + "category": 3, + "potentialLinks": [ + "cloudfront-key-group", + "cloudfront-cloud-front-origin-access-identity", + "cloudfront-continuous-deployment-policy", + "cloudfront-cache-policy", + "cloudfront-field-level-encryption", + "cloudfront-function", + "cloudfront-origin-request-policy", + "cloudfront-realtime-log-config", + "cloudfront-response-headers-policy", + "dns", + "lambda-function", + "s3-bucket" + ], + "descriptiveName": "CloudFront Distribution", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a distribution by ID", + "list": true, + "listDescription": "List all distributions", + "search": true, + "searchDescription": "Search distributions by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_distribution.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json new file mode 100644 index 00000000..d2583d2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-function.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-function", + "category": 1, + "descriptiveName": "CloudFront Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Function by name", + "list": true, + "listDescription": "List CloudFront Functions", + "search": true, + "searchDescription": "Search CloudFront Functions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_function.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json new file mode 100644 index 00000000..2172ce2c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-key-group.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-key-group", + "category": 7, + "descriptiveName": "CloudFront Key Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a CloudFront Key Group by ID", + "list": true, + "listDescription": "List CloudFront Key Groups", + "search": true, + "searchDescription": "Search CloudFront Key Groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_key_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json new file mode 100644 index 00000000..50e96263 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-access-control.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-origin-access-control", + "category": 4, + "descriptiveName": "Cloudfront Origin Access Control", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Origin Access Control by ID", + "list": true, + "listDescription": "List Origin Access Controls", + "search": true, + "searchDescription": "Origin Access Control by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_origin_access_control.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json new file mode 100644 index 00000000..76ec63c2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-origin-request-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-origin-request-policy", + "category": 3, + "descriptiveName": "CloudFront Origin Request Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Origin Request Policy by ID", + "list": true, + "listDescription": "List Origin Request Policies", + "search": true, + "searchDescription": "Origin Request Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_origin_request_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json new file mode 100644 index 00000000..5b26ad55 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-realtime-log-config.json @@ -0,0 +1,19 @@ +{ + "type": "cloudfront-realtime-log-config", + "category": 7, + "descriptiveName": "CloudFront Realtime Log Config", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Realtime Log Config by Name", + "list": true, + "listDescription": "List Realtime Log Configs", + "search": true, + "searchDescription": "Search Realtime Log Configs by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_realtime_log_config.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json new file mode 100644 index 00000000..5eb9dd3e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-response-headers-policy.json @@ -0,0 +1,18 @@ +{ + "type": "cloudfront-response-headers-policy", + "category": 3, + "descriptiveName": "CloudFront Response Headers Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get Response Headers Policy by ID", + "list": true, + "listDescription": "List Response Headers Policies", + "search": true, + "searchDescription": "Search Response Headers Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudfront_response_headers_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json b/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json new file mode 100644 index 00000000..5443b821 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudfront-streaming-distribution.json @@ -0,0 +1,23 @@ +{ + "type": "cloudfront-streaming-distribution", + "category": 3, + "potentialLinks": ["dns"], + "descriptiveName": "CloudFront Streaming Distribution", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Streaming Distribution by ID", + "list": true, + "listDescription": "List Streaming Distributions", + "search": true, + "searchDescription": "Search Streaming Distributions by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_cloudfront_distribution.arn" + }, + { + "terraformQueryMap": "aws_cloudfront_distribution.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json b/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json new file mode 100644 index 00000000..1dd40323 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/cloudwatch-alarm.json @@ -0,0 +1,19 @@ +{ + "type": "cloudwatch-alarm", + "category": 5, + "potentialLinks": ["cloudwatch-metric"], + "descriptiveName": "CloudWatch Alarm", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an alarm by name", + "list": true, + "listDescription": "List all alarms", + "search": true, + "searchDescription": "Search for alarms. This accepts JSON in the format of `cloudwatch.DescribeAlarmsForMetricInput`" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_cloudwatch_metric_alarm.alarm_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json new file mode 100644 index 00000000..036a6813 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-connection.json @@ -0,0 +1,24 @@ +{ + "type": "directconnect-connection", + "category": 3, + "potentialLinks": [ + "directconnect-lag", + "directconnect-location", + "directconnect-loa", + "directconnect-virtual-interface" + ], + "descriptiveName": "Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a connection by ID", + "list": true, + "listDescription": "List all connections", + "search": true, + "searchDescription": "Search connection by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_connection.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json new file mode 100644 index 00000000..6a818208 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-customer-metadata.json @@ -0,0 +1,13 @@ +{ + "type": "directconnect-customer-metadata", + "category": 7, + "descriptiveName": "Customer Metadata", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a customer agreement by name", + "list": true, + "listDescription": "List all customer agreements", + "search": true, + "searchDescription": "Search customer agreements by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json new file mode 100644 index 00000000..57ce90e8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association-proposal.json @@ -0,0 +1,19 @@ +{ + "type": "directconnect-direct-connect-gateway-association-proposal", + "category": 7, + "potentialLinks": ["directconnect-direct-connect-gateway-association"], + "descriptiveName": "Direct Connect Gateway Association Proposal", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Direct Connect Gateway Association Proposal by ID", + "list": true, + "listDescription": "List all Direct Connect Gateway Association Proposals", + "search": true, + "searchDescription": "Search Direct Connect Gateway Association Proposals by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway_association_proposal.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json new file mode 100644 index 00000000..62c19590 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-association.json @@ -0,0 +1,17 @@ +{ + "type": "directconnect-direct-connect-gateway-association", + "category": 3, + "potentialLinks": ["directconnect-direct-connect-gateway"], + "descriptiveName": "Direct Connect Gateway Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway association by direct connect gateway ID and virtual gateway ID", + "search": true, + "searchDescription": "Search direct connect gateway associations by direct connect gateway ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway_association.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json new file mode 100644 index 00000000..d4d7a711 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "directconnect-direct-connect-gateway-attachment", + "category": 3, + "potentialLinks": [ + "directconnect-direct-connect-gateway", + "directconnect-virtual-interface" + ], + "descriptiveName": "Direct Connect Gateway Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway attachment by DirectConnectGatewayId/VirtualInterfaceId", + "search": true, + "searchDescription": "Search direct connect gateway attachments for given VirtualInterfaceId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json new file mode 100644 index 00000000..e35ae42a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-direct-connect-gateway.json @@ -0,0 +1,18 @@ +{ + "type": "directconnect-direct-connect-gateway", + "category": 3, + "descriptiveName": "Direct Connect Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a direct connect gateway by ID", + "list": true, + "listDescription": "List all direct connect gateways", + "search": true, + "searchDescription": "Search direct connect gateway by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json new file mode 100644 index 00000000..d8502036 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-hosted-connection.json @@ -0,0 +1,22 @@ +{ + "type": "directconnect-hosted-connection", + "category": 3, + "potentialLinks": [ + "directconnect-lag", + "directconnect-location", + "directconnect-loa", + "directconnect-virtual-interface" + ], + "descriptiveName": "Hosted Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Hosted Connection by connection ID", + "search": true, + "searchDescription": "Search Hosted Connections by Interconnect or LAG ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_hosted_connection.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json new file mode 100644 index 00000000..33c50a03 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-interconnect.json @@ -0,0 +1,19 @@ +{ + "type": "directconnect-interconnect", + "category": 3, + "potentialLinks": [ + "directconnect-hosted-connection", + "directconnect-lag", + "directconnect-loa", + "directconnect-location" + ], + "descriptiveName": "Interconnect", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Interconnect by InterconnectId", + "list": true, + "listDescription": "List all Interconnects", + "search": true, + "searchDescription": "Search Interconnects by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json new file mode 100644 index 00000000..6080c105 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-lag.json @@ -0,0 +1,23 @@ +{ + "type": "directconnect-lag", + "category": 3, + "potentialLinks": [ + "directconnect-connection", + "directconnect-hosted-connection", + "directconnect-location" + ], + "descriptiveName": "Link Aggregation Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Link Aggregation Group by ID", + "list": true, + "listDescription": "List all Link Aggregation Groups", + "search": true, + "searchDescription": "Search Link Aggregation Group by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_lag.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json new file mode 100644 index 00000000..626e8c19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-location.json @@ -0,0 +1,18 @@ +{ + "type": "directconnect-location", + "category": 3, + "descriptiveName": "Direct Connect Location", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Location by its code", + "list": true, + "listDescription": "List all Direct Connect Locations", + "search": true, + "searchDescription": "Search Direct Connect Locations by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_location.location_code" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json new file mode 100644 index 00000000..05264ca9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-router-configuration.json @@ -0,0 +1,17 @@ +{ + "type": "directconnect-router-configuration", + "category": 7, + "potentialLinks": ["directconnect-virtual-interface"], + "descriptiveName": "Router Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Router Configuration by Virtual Interface ID", + "search": true, + "searchDescription": "Search Router Configuration by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_router_configuration.virtual_interface_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json new file mode 100644 index 00000000..d3b7caf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-gateway.json @@ -0,0 +1,13 @@ +{ + "type": "directconnect-virtual-gateway", + "category": 3, + "descriptiveName": "Direct Connect Virtual Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a virtual gateway by ID", + "list": true, + "listDescription": "List all virtual gateways", + "search": true, + "searchDescription": "Search virtual gateways by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json new file mode 100644 index 00000000..30edf074 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/directconnect-virtual-interface.json @@ -0,0 +1,31 @@ +{ + "type": "directconnect-virtual-interface", + "category": 3, + "potentialLinks": [ + "directconnect-connection", + "directconnect-direct-connect-gateway", + "rdap-ip-network", + "directconnect-direct-connect-gateway-attachment", + "directconnect-virtual-interface" + ], + "descriptiveName": "Virtual Interface", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a virtual interface by ID", + "list": true, + "listDescription": "List all virtual interfaces", + "search": true, + "searchDescription": "Search virtual interfaces by connection ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_dx_private_virtual_interface.id" + }, + { + "terraformQueryMap": "aws_dx_public_virtual_interface.id" + }, + { + "terraformQueryMap": "aws_dx_transit_virtual_interface.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json b/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json new file mode 100644 index 00000000..bbef222b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/dynamodb-backup.json @@ -0,0 +1,12 @@ +{ + "type": "dynamodb-backup", + "category": 6, + "potentialLinks": ["dynamodb-table"], + "descriptiveName": "DynamoDB Backup", + "supportedQueryMethods": { + "list": true, + "listDescription": "List all DynamoDB backups", + "search": true, + "searchDescription": "Search for a DynamoDB backup by table name" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json b/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json new file mode 100644 index 00000000..2f9933e3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/dynamodb-table.json @@ -0,0 +1,25 @@ +{ + "type": "dynamodb-table", + "category": 6, + "potentialLinks": [ + "kinesis-stream", + "backup-recovery-point", + "dynamodb-table", + "kms-key" + ], + "descriptiveName": "DynamoDB Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a DynamoDB table by name", + "list": true, + "listDescription": "List all DynamoDB tables", + "search": true, + "searchDescription": "Search for DynamoDB tables by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_dynamodb_table.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-address.json b/docs.overmind.tech/docs/sources/aws/data/ec2-address.json new file mode 100644 index 00000000..9248b320 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-address.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-address", + "category": 3, + "potentialLinks": ["ec2-instance", "ip", "ec2-network-interface"], + "descriptiveName": "EC2 Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 address by Public IP", + "list": true, + "listDescription": "List EC2 addresses", + "search": true, + "searchDescription": "Search for EC2 addresses by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eip.public_ip" + }, + { + "terraformQueryMap": "aws_eip_association.public_ip" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json new file mode 100644 index 00000000..e2758bb3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation-fleet.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-capacity-reservation-fleet", + "category": 7, + "potentialLinks": ["ec2-capacity-reservation"], + "descriptiveName": "Capacity Reservation Fleet", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity reservation fleet by ID", + "list": true, + "listDescription": "List capacity reservation fleets", + "search": true, + "searchDescription": "Search capacity reservation fleets by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json new file mode 100644 index 00000000..e2a9ec25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-capacity-reservation.json @@ -0,0 +1,23 @@ +{ + "type": "ec2-capacity-reservation", + "category": 7, + "potentialLinks": [ + "outposts-outpost", + "ec2-placement-group", + "ec2-capacity-reservation-fleet" + ], + "descriptiveName": "Capacity Reservation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity reservation fleet by ID", + "list": true, + "listDescription": "List capacity reservation fleets", + "search": true, + "searchDescription": "Search capacity reservation fleets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ec2_capacity_reservation_fleet.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json new file mode 100644 index 00000000..4a9002d8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-egress-only-internet-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-egress-only-internet-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Egress Only Internet Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an egress only internet gateway by ID", + "list": true, + "listDescription": "List all egress only internet gateways", + "search": true, + "searchDescription": "Search egress only internet gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "egress_only_internet_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json b/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json new file mode 100644 index 00000000..c76f2643 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-iam-instance-profile-association.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-iam-instance-profile-association", + "category": 4, + "potentialLinks": ["iam-instance-profile", "ec2-instance"], + "descriptiveName": "IAM Instance Profile Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM Instance Profile Association by ID", + "list": true, + "listDescription": "List all IAM Instance Profile Associations", + "search": true, + "searchDescription": "Search IAM Instance Profile Associations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-image.json b/docs.overmind.tech/docs/sources/aws/data/ec2-image.json new file mode 100644 index 00000000..b7bf0b29 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-image.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-image", + "category": 1, + "descriptiveName": "Amazon Machine Image (AMI)", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an AMI by ID", + "list": true, + "listDescription": "List all AMIs", + "search": true, + "searchDescription": "Search AMIs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ami.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json new file mode 100644 index 00000000..42d660f7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-event-window.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-instance-event-window", + "category": 7, + "potentialLinks": ["ec2-host", "ec2-instance"], + "descriptiveName": "EC2 Instance Event Window", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an event window by ID", + "list": true, + "listDescription": "List all event windows", + "search": true, + "searchDescription": "Search for event windows by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json new file mode 100644 index 00000000..fb1af5cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance-status.json @@ -0,0 +1,13 @@ +{ + "type": "ec2-instance-status", + "category": 5, + "descriptiveName": "EC2 Instance Status", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 instance status by Instance ID", + "list": true, + "listDescription": "List all EC2 instance statuses", + "search": true, + "searchDescription": "Search EC2 instance statuses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json b/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json new file mode 100644 index 00000000..52182893 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-instance.json @@ -0,0 +1,41 @@ +{ + "type": "ec2-instance", + "category": 1, + "potentialLinks": [ + "ec2-instance-status", + "iam-instance-profile", + "ec2-capacity-reservation", + "ec2-elastic-gpu", + "elastic-inference-accelerator", + "license-manager-license-configuration", + "outposts-outpost", + "ec2-spot-instance-request", + "ec2-image", + "ec2-key-pair", + "ec2-placement-group", + "ip", + "ec2-subnet", + "ec2-vpc", + "dns", + "ec2-security-group", + "ec2-volume" + ], + "descriptiveName": "EC2 Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an EC2 instance by ID", + "list": true, + "listDescription": "List all EC2 instances", + "search": true, + "searchDescription": "Search EC2 instances by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_instance.id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_instance.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json new file mode 100644 index 00000000..1df593a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-internet-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-internet-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Internet Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an internet gateway by ID", + "list": true, + "listDescription": "List all internet gateways", + "search": true, + "searchDescription": "Search internet gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_internet_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json b/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json new file mode 100644 index 00000000..eb91caf5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-key-pair.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-key-pair", + "category": 4, + "descriptiveName": "Key Pair", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a key pair by name", + "list": true, + "listDescription": "List all key pairs", + "search": true, + "searchDescription": "Search for key pairs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_key_pair.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json new file mode 100644 index 00000000..1a256942 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template-version.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-launch-template-version", + "category": 1, + "potentialLinks": [ + "ec2-network-interface", + "ec2-subnet", + "ec2-security-group", + "ec2-image", + "ec2-key-pair", + "ec2-snapshot", + "ec2-capacity-reservation", + "ec2-placement-group", + "ec2-host", + "ip" + ], + "descriptiveName": "Launch Template Version", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a launch template version by {templateId}.{version}", + "list": true, + "listDescription": "List all launch template versions", + "search": true, + "searchDescription": "Search launch template versions by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json new file mode 100644 index 00000000..52357a89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-launch-template.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-launch-template", + "category": 1, + "descriptiveName": "Launch Template", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a launch template by ID", + "list": true, + "listDescription": "List all launch templates", + "search": true, + "searchDescription": "Search for launch templates by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_launch_template.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json b/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json new file mode 100644 index 00000000..2cda0300 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-nat-gateway.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-nat-gateway", + "category": 3, + "potentialLinks": ["ec2-vpc", "ec2-subnet", "ec2-network-interface", "ip"], + "descriptiveName": "NAT Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a NAT Gateway by ID", + "list": true, + "listDescription": "List all NAT gateways", + "search": true, + "searchDescription": "Search for NAT gateways by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_nat_gateway.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json new file mode 100644 index 00000000..3c776a4a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-acl.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-network-acl", + "category": 4, + "potentialLinks": ["ec2-subnet", "ec2-vpc"], + "descriptiveName": "Network ACL", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network ACL", + "list": true, + "listDescription": "List all network ACLs", + "search": true, + "searchDescription": "Search for network ACLs by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_network_acl.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json new file mode 100644 index 00000000..e8406928 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface-permission.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-network-interface-permission", + "category": 4, + "potentialLinks": ["ec2-network-interface"], + "descriptiveName": "Network Interface Permission", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network interface permission by ID", + "list": true, + "listDescription": "List all network interface permissions", + "search": true, + "searchDescription": "Search network interface permissions by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json new file mode 100644 index 00000000..46d71875 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-network-interface.json @@ -0,0 +1,26 @@ +{ + "type": "ec2-network-interface", + "category": 3, + "potentialLinks": [ + "ec2-instance", + "ec2-security-group", + "ip", + "dns", + "ec2-subnet", + "ec2-vpc" + ], + "descriptiveName": "EC2 Network Interface", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a network interface by ID", + "list": true, + "listDescription": "List all network interfaces", + "search": true, + "searchDescription": "Search network interfaces by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_network_interface.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json b/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json new file mode 100644 index 00000000..62c7b33a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-placement-group.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-placement-group", + "category": 1, + "descriptiveName": "Placement Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a placement group by ID", + "list": true, + "listDescription": "List all placement groups", + "search": true, + "searchDescription": "Search for placement groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_placement_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json b/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json new file mode 100644 index 00000000..539b9d30 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-reserved-instance.json @@ -0,0 +1,13 @@ +{ + "type": "ec2-reserved-instance", + "category": 1, + "descriptiveName": "Reserved EC2 Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a reserved EC2 instance by ID", + "list": true, + "listDescription": "List all reserved EC2 instances", + "search": true, + "searchDescription": "Search reserved EC2 instances by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json b/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json new file mode 100644 index 00000000..83eec9ee --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-route-table.json @@ -0,0 +1,41 @@ +{ + "type": "ec2-route-table", + "category": 3, + "potentialLinks": [ + "ec2-vpc", + "ec2-subnet", + "ec2-internet-gateway", + "ec2-vpc-endpoint", + "ec2-carrier-gateway", + "ec2-egress-only-internet-gateway", + "ec2-instance", + "ec2-local-gateway", + "ec2-nat-gateway", + "ec2-network-interface", + "ec2-transit-gateway", + "ec2-vpc-peering-connection" + ], + "descriptiveName": "Route Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a route table by ID", + "list": true, + "listDescription": "List all route tables", + "search": true, + "searchDescription": "Search route tables by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route_table.id" + }, + { + "terraformQueryMap": "aws_route_table_association.route_table_id" + }, + { + "terraformQueryMap": "aws_default_route_table.default_route_table_id" + }, + { + "terraformQueryMap": "aws_route.route_table_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json new file mode 100644 index 00000000..d497780b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group-rule.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-security-group-rule", + "category": 4, + "potentialLinks": ["ec2-security-group"], + "descriptiveName": "Security Group Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a security group rule by ID", + "list": true, + "listDescription": "List all security group rules", + "search": true, + "searchDescription": "Search security group rules by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_security_group_rule.security_group_rule_id" + }, + { + "terraformQueryMap": "aws_vpc_security_group_ingress_rule.security_group_rule_id" + }, + { + "terraformQueryMap": "aws_vpc_security_group_egress_rule.security_group_rule_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json new file mode 100644 index 00000000..52075401 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-security-group.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-security-group", + "category": 4, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "Security Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a security group by ID", + "list": true, + "listDescription": "List all security groups", + "search": true, + "searchDescription": "Search for security groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_security_group.id" + }, + { + "terraformQueryMap": "aws_security_group_rule.security_group_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json b/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json new file mode 100644 index 00000000..fd62ef95 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-snapshot.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-snapshot", + "category": 2, + "potentialLinks": ["ec2-volume"], + "descriptiveName": "EC2 Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a snapshot by ID", + "list": true, + "listDescription": "List all snapshots", + "search": true, + "searchDescription": "Search snapshots by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json b/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json new file mode 100644 index 00000000..3f82850c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-subnet.json @@ -0,0 +1,22 @@ +{ + "type": "ec2-subnet", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "EC2 Subnet", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a subnet by ID", + "list": true, + "listDescription": "List all subnets", + "search": true, + "searchDescription": "Search for subnets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route_table_association.subnet_id" + }, + { + "terraformQueryMap": "aws_subnet.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json b/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json new file mode 100644 index 00000000..6f51fd4f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-volume-status.json @@ -0,0 +1,14 @@ +{ + "type": "ec2-volume-status", + "category": 5, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "EC2 Volume Status", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a volume status by volume ID", + "list": true, + "listDescription": "List all volume statuses", + "search": true, + "searchDescription": "Search for volume statuses by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json b/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json new file mode 100644 index 00000000..1420c27c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-volume.json @@ -0,0 +1,19 @@ +{ + "type": "ec2-volume", + "category": 2, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "EC2 Volume", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a volume by ID", + "list": true, + "listDescription": "List all volumes", + "search": true, + "searchDescription": "Search volumes by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ebs_volume.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json new file mode 100644 index 00000000..5c175313 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "ec2-vpc-endpoint", + "category": 3, + "descriptiveName": "VPC Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC Endpoint by ID", + "list": true, + "listDescription": "List all VPC Endpoints", + "search": true, + "searchDescription": "Search VPC Endpoints by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc_endpoint.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json new file mode 100644 index 00000000..6bcc6aa7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc-peering-connection.json @@ -0,0 +1,25 @@ +{ + "type": "ec2-vpc-peering-connection", + "category": 3, + "potentialLinks": ["ec2-vpc"], + "descriptiveName": "VPC Peering Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC Peering Connection by ID", + "list": true, + "listDescription": "List all VPC Peering Connections", + "search": true, + "searchDescription": "Search for VPC Peering Connections by their ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc_peering_connection.id" + }, + { + "terraformQueryMap": "aws_vpc_peering_connection_accepter.id" + }, + { + "terraformQueryMap": "aws_vpc_peering_connection_options.vpc_peering_connection_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json new file mode 100644 index 00000000..3df7e1e8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-vpc.json @@ -0,0 +1,16 @@ +{ + "type": "ec2-vpc", + "category": 3, + "descriptiveName": "VPC", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VPC by ID", + "list": true, + "listDescription": "List all VPCs" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_vpc.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json b/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json new file mode 100644 index 00000000..6c44da49 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-capacity-provider.json @@ -0,0 +1,20 @@ +{ + "type": "ecs-capacity-provider", + "category": 7, + "potentialLinks": ["autoscaling-auto-scaling-group"], + "descriptiveName": "Capacity Provider", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a capacity provider by its short name or full Amazon Resource Name (ARN).", + "list": true, + "listDescription": "List capacity providers.", + "search": true, + "searchDescription": "Search capacity providers by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_capacity_provider.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json b/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json new file mode 100644 index 00000000..968ac8ec --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-cluster.json @@ -0,0 +1,25 @@ +{ + "type": "ecs-cluster", + "category": 1, + "potentialLinks": [ + "ecs-container-instance", + "ecs-service", + "ecs-task", + "ecs-capacity-provider" + ], + "descriptiveName": "ECS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a cluster by name", + "list": true, + "listDescription": "List all clusters", + "search": true, + "searchDescription": "Search for a cluster by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_cluster.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json b/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json new file mode 100644 index 00000000..918fcd86 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-container-instance.json @@ -0,0 +1,12 @@ +{ + "type": "ecs-container-instance", + "category": 1, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "Container Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a container instance by ID which consists of {clusterName}/{id}", + "search": true, + "searchDescription": "Search for container instances by cluster" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-service.json b/docs.overmind.tech/docs/sources/aws/data/ecs-service.json new file mode 100644 index 00000000..2023b25a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-service.json @@ -0,0 +1,27 @@ +{ + "type": "ecs-service", + "category": 1, + "potentialLinks": [ + "ecs-cluster", + "elbv2-target-group", + "servicediscovery-service", + "ecs-task-definition", + "ecs-capacity-provider", + "ec2-subnet", + "ecs-security-group", + "dns" + ], + "descriptiveName": "ECS Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ECS service by full name ({clusterName}/{id})", + "search": true, + "searchDescription": "Search for ECS services by cluster" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ecs_service.cluster_name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json b/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json new file mode 100644 index 00000000..76223f75 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-task-definition.json @@ -0,0 +1,19 @@ +{ + "type": "ecs-task-definition", + "category": 1, + "potentialLinks": ["iam-role", "secretsmanager-secret", "ssm-parameter"], + "descriptiveName": "Task Definition", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a task definition by revision name ({family}:{revision})", + "list": true, + "listDescription": "List all task definitions", + "search": true, + "searchDescription": "Search for task definitions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ecs_task_definition.family" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ecs-task.json b/docs.overmind.tech/docs/sources/aws/data/ecs-task.json new file mode 100644 index 00000000..4563d299 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ecs-task.json @@ -0,0 +1,18 @@ +{ + "type": "ecs-task", + "category": 1, + "potentialLinks": [ + "ecs-cluster", + "ecs-container-instance", + "ecs-task-definition", + "ec2-network-interface", + "ip" + ], + "descriptiveName": "ECS Task", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ECS task by ID", + "search": true, + "searchDescription": "Search for ECS tasks by cluster" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json b/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json new file mode 100644 index 00000000..f52ca656 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-access-point.json @@ -0,0 +1,18 @@ +{ + "type": "efs-access-point", + "category": 3, + "descriptiveName": "EFS Access Point", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an access point by ID", + "list": true, + "listDescription": "List all access points", + "search": true, + "searchDescription": "Search for an access point by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_access_point.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json b/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json new file mode 100644 index 00000000..41acfabb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-backup-policy.json @@ -0,0 +1,16 @@ +{ + "type": "efs-backup-policy", + "category": 2, + "descriptiveName": "EFS Backup Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an Backup Policy by file system ID", + "search": true, + "searchDescription": "Search for an Backup Policy by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_backup_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json b/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json new file mode 100644 index 00000000..69da6ccb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-file-system.json @@ -0,0 +1,18 @@ +{ + "type": "efs-file-system", + "category": 2, + "descriptiveName": "EFS File System", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a file system by ID", + "list": true, + "listDescription": "List file systems", + "search": true, + "searchDescription": "Search file systems by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_file_system.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json b/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json new file mode 100644 index 00000000..ae47e2a4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-mount-target.json @@ -0,0 +1,16 @@ +{ + "type": "efs-mount-target", + "category": 2, + "descriptiveName": "EFS Mount Target", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an mount target by ID", + "search": true, + "searchDescription": "Search for mount targets by file system ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_mount_target.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json b/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json new file mode 100644 index 00000000..9d790dba --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/efs-replication-configuration.json @@ -0,0 +1,18 @@ +{ + "type": "efs-replication-configuration", + "category": 2, + "descriptiveName": "EFS Replication Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a replication configuration by file system ID", + "list": true, + "listDescription": "List all replication configurations", + "search": true, + "searchDescription": "Search for a replication configuration by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_efs_replication_configuration.source_file_system_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-addon.json b/docs.overmind.tech/docs/sources/aws/data/eks-addon.json new file mode 100644 index 00000000..f453ac10 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-addon.json @@ -0,0 +1,16 @@ +{ + "type": "eks-addon", + "category": 1, + "descriptiveName": "EKS Addon", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an addon by unique name ({clusterName}:{addonName})", + "search": true, + "searchDescription": "Search addons by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_addon.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json b/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json new file mode 100644 index 00000000..e59349e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-cluster.json @@ -0,0 +1,19 @@ +{ + "type": "eks-cluster", + "category": 1, + "descriptiveName": "EKS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a cluster by name", + "list": true, + "listDescription": "List all clusters", + "search": true, + "searchDescription": "Search for clusters by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_eks_cluster.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json b/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json new file mode 100644 index 00000000..29112a7b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-fargate-profile.json @@ -0,0 +1,17 @@ +{ + "type": "eks-fargate-profile", + "category": 7, + "potentialLinks": ["iam-role", "ec2-subnet"], + "descriptiveName": "Fargate Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a fargate profile by unique name ({clusterName}:{FargateProfileName})", + "search": true, + "searchDescription": "Search for fargate profiles by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_fargate_profile.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json b/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json new file mode 100644 index 00000000..514116c5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/eks-nodegroup.json @@ -0,0 +1,23 @@ +{ + "type": "eks-nodegroup", + "category": 1, + "potentialLinks": [ + "ec2-key-pair", + "ec2-security-group", + "ec2-subnet", + "autoscaling-auto-scaling-group", + "ec2-launch-template" + ], + "descriptiveName": "EKS Nodegroup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a node group by unique name ({clusterName}:{NodegroupName})", + "search": true, + "searchDescription": "Search for node groups by cluster name" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_eks_node_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json b/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json new file mode 100644 index 00000000..f38a9830 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elb-instance-health.json @@ -0,0 +1,12 @@ +{ + "type": "elb-instance-health", + "category": 5, + "potentialLinks": ["ec2-instance"], + "descriptiveName": "ELB Instance Health", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get instance health by ID ({LoadBalancerName}/{InstanceId})", + "list": true, + "listDescription": "List all instance healths" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json b/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json new file mode 100644 index 00000000..bbd6805a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elb-load-balancer.json @@ -0,0 +1,28 @@ +{ + "type": "elb-load-balancer", + "category": 3, + "potentialLinks": [ + "dns", + "route53-hosted-zone", + "ec2-subnet", + "ec2-vpc", + "ec2-instance", + "elb-instance-health", + "ec2-security-group" + ], + "descriptiveName": "Classic Load Balancer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a classic load balancer by name", + "list": true, + "listDescription": "List all classic load balancers", + "search": true, + "searchDescription": "Search for classic load balancers by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_elb.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json new file mode 100644 index 00000000..5ae2f13c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-listener.json @@ -0,0 +1,27 @@ +{ + "type": "elbv2-listener", + "category": 3, + "potentialLinks": [ + "elbv2-load-balancer", + "acm-certificate", + "elbv2-rule", + "cognito-idp-user-pool", + "http", + "elbv2-target-group" + ], + "descriptiveName": "ELB Listener", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ELB listener by ARN", + "search": true, + "searchDescription": "Search for ELB listeners by load balancer ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_alb_listener.arn" + }, + { + "terraformQueryMap": "aws_lb_listener.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json new file mode 100644 index 00000000..a43cb811 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-load-balancer.json @@ -0,0 +1,34 @@ +{ + "type": "elbv2-load-balancer", + "category": 3, + "potentialLinks": [ + "elbv2-target-group", + "elbv2-listener", + "dns", + "route53-hosted-zone", + "ec2-vpc", + "ec2-subnet", + "ec2-address", + "ip", + "ec2-security-group", + "ec2-coip-pool" + ], + "descriptiveName": "Elastic Load Balancer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ELB by name", + "list": true, + "listDescription": "List all ELBs", + "search": true, + "searchDescription": "Search for ELBs by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lb.arn" + }, + { + "terraformQueryMap": "aws_lb.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json new file mode 100644 index 00000000..02271e06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-rule.json @@ -0,0 +1,19 @@ +{ + "type": "elbv2-rule", + "category": 7, + "descriptiveName": "ELB Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a rule by ARN", + "search": true, + "searchDescription": "Search for rules by listener ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_alb_listener_rule.arn" + }, + { + "terraformQueryMap": "aws_lb_listener_rule.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json new file mode 100644 index 00000000..d98f3986 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-group.json @@ -0,0 +1,24 @@ +{ + "type": "elbv2-target-group", + "category": 3, + "potentialLinks": ["ec2-vpc", "elbv2-load-balancer", "elbv2-target-health"], + "descriptiveName": "Target Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a target group by name", + "list": true, + "listDescription": "List all target groups", + "search": true, + "searchDescription": "Search for target groups by load balancer ARN or target group ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_alb_target_group.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lb_target_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json new file mode 100644 index 00000000..5b56610e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/elbv2-target-health.json @@ -0,0 +1,17 @@ +{ + "type": "elbv2-target-health", + "category": 5, + "potentialLinks": [ + "ec2-instance", + "lambda-function", + "ip", + "elbv2-load-balancer" + ], + "descriptiveName": "ELB Target Health", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get target health by unique ID ({TargetGroupArn}|{Id}|{AvailabilityZone}|{Port})", + "search": true, + "searchDescription": "Search for target health by target group ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-group.json b/docs.overmind.tech/docs/sources/aws/data/iam-group.json new file mode 100644 index 00000000..b29e1eee --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-group.json @@ -0,0 +1,19 @@ +{ + "type": "iam-group", + "category": 4, + "descriptiveName": "IAM Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a group by name", + "list": true, + "listDescription": "List all IAM groups", + "search": true, + "searchDescription": "Search for a group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json b/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json new file mode 100644 index 00000000..baf7d26c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-instance-profile.json @@ -0,0 +1,20 @@ +{ + "type": "iam-instance-profile", + "category": 4, + "potentialLinks": ["iam-role", "iam-policy"], + "descriptiveName": "IAM Instance Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM instance profile by name", + "list": true, + "listDescription": "List all IAM instance profiles", + "search": true, + "searchDescription": "Search IAM instance profiles by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_instance_profile.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-policy.json b/docs.overmind.tech/docs/sources/aws/data/iam-policy.json new file mode 100644 index 00000000..45065c35 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-policy.json @@ -0,0 +1,24 @@ +{ + "type": "iam-policy", + "category": 4, + "potentialLinks": ["iam-group", "iam-user", "iam-role"], + "descriptiveName": "IAM Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM policy by policyFullName ({path} + {policyName})", + "list": true, + "listDescription": "List all IAM policies", + "search": true, + "searchDescription": "Search for IAM policies by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_policy.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_user_policy_attachment.policy_arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-role.json b/docs.overmind.tech/docs/sources/aws/data/iam-role.json new file mode 100644 index 00000000..055312c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-role.json @@ -0,0 +1,20 @@ +{ + "type": "iam-role", + "category": 4, + "potentialLinks": ["iam-policy"], + "descriptiveName": "IAM Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM role by name", + "list": true, + "listDescription": "List all IAM roles", + "search": true, + "searchDescription": "Search for IAM roles by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_role.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/iam-user.json b/docs.overmind.tech/docs/sources/aws/data/iam-user.json new file mode 100644 index 00000000..ea0a8ab1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/iam-user.json @@ -0,0 +1,23 @@ +{ + "type": "iam-user", + "category": 4, + "potentialLinks": ["iam-group"], + "descriptiveName": "IAM User", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an IAM user by name", + "list": true, + "listDescription": "List all IAM users", + "search": true, + "searchDescription": "Search for IAM users by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_iam_user.arn" + }, + { + "terraformQueryMap": "aws_iam_user_group_membership.user" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-alias.json b/docs.overmind.tech/docs/sources/aws/data/kms-alias.json new file mode 100644 index 00000000..734fede8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-alias.json @@ -0,0 +1,19 @@ +{ + "type": "kms-alias", + "category": 4, + "potentialLinks": ["kms-key"], + "descriptiveName": "KMS Alias", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an alias by keyID/aliasName", + "list": true, + "listDescription": "List all aliases", + "search": true, + "searchDescription": "Search aliases by keyID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_alias.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json b/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json new file mode 100644 index 00000000..76550d54 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-custom-key-store.json @@ -0,0 +1,19 @@ +{ + "type": "kms-custom-key-store", + "category": 2, + "potentialLinks": ["cloudhsmv2-cluster", "ec2-vpc-endpoint-service"], + "descriptiveName": "Custom Key Store", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a custom key store by its ID", + "list": true, + "listDescription": "List all custom key stores", + "search": true, + "searchDescription": "Search custom key store by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_custom_key_store.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-grant.json b/docs.overmind.tech/docs/sources/aws/data/kms-grant.json new file mode 100644 index 00000000..0e29bf70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-grant.json @@ -0,0 +1,17 @@ +{ + "type": "kms-grant", + "category": 4, + "potentialLinks": ["kms-key", "iam-user", "iam-role"], + "descriptiveName": "KMS Grant", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a grant by keyID/grantId", + "search": true, + "searchDescription": "Search grants by keyID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_grant.grant_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json b/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json new file mode 100644 index 00000000..74452c63 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-key-policy.json @@ -0,0 +1,17 @@ +{ + "type": "kms-key-policy", + "category": 4, + "potentialLinks": ["kms-key"], + "descriptiveName": "KMS Key Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a KMS key policy by its Key ID", + "search": true, + "searchDescription": "Search KMS key policies by Key ID" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_key_policy.key_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/kms-key.json b/docs.overmind.tech/docs/sources/aws/data/kms-key.json new file mode 100644 index 00000000..a292b5f9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/kms-key.json @@ -0,0 +1,19 @@ +{ + "type": "kms-key", + "category": 4, + "potentialLinks": ["kms-custom-key-store", "kms-key-policy", "kms-grant"], + "descriptiveName": "KMS Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a KMS Key by its ID", + "list": true, + "listDescription": "List all KMS Keys", + "search": true, + "searchDescription": "Search for KMS Keys by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_kms_key.key_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json b/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json new file mode 100644 index 00000000..ecf68cb5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-event-source-mapping.json @@ -0,0 +1,28 @@ +{ + "type": "lambda-event-source-mapping", + "category": 1, + "potentialLinks": [ + "lambda-function", + "dynamodb-table", + "kinesis-stream", + "sqs-queue", + "kafka-cluster", + "mq-broker", + "rds-db-cluster" + ], + "descriptiveName": "Lambda Event Source Mapping", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Lambda event source mapping by UUID", + "list": true, + "listDescription": "List all Lambda event source mappings", + "search": true, + "searchDescription": "Search for Lambda event source mappings by Event Source ARN (SQS, DynamoDB, Kinesis, etc.)" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_lambda_event_source_mapping.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-function.json b/docs.overmind.tech/docs/sources/aws/data/lambda-function.json new file mode 100644 index 00000000..2c0e8ca8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-function.json @@ -0,0 +1,35 @@ +{ + "type": "lambda-function", + "category": 1, + "potentialLinks": [ + "iam-role", + "s3-bucket", + "sns-topic", + "sqs-queue", + "lambda-function", + "events-event-bus", + "elbv2-target-group", + "vpc-lattice-target-group", + "logs-log-group" + ], + "descriptiveName": "Lambda Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a lambda function by name", + "list": true, + "listDescription": "List all lambda functions", + "search": true, + "searchDescription": "Search for lambda functions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_lambda_function.arn" + }, + { + "terraformQueryMap": "aws_lambda_function_event_invoke_config.id" + }, + { + "terraformQueryMap": "aws_lambda_function_url.function_arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json b/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json new file mode 100644 index 00000000..71ba69ab --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-layer-version.json @@ -0,0 +1,17 @@ +{ + "type": "lambda-layer-version", + "category": 1, + "potentialLinks": ["signer-signing-job", "signer-signing-profile"], + "descriptiveName": "Lambda Layer Version", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a layer version by full name ({layerName}:{versionNumber})", + "search": true, + "searchDescription": "Search for layer versions by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_lambda_layer_version.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json b/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json new file mode 100644 index 00000000..9be5fdb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/lambda-layer.json @@ -0,0 +1,10 @@ +{ + "type": "lambda-layer", + "category": 1, + "potentialLinks": ["lambda-layer-version"], + "descriptiveName": "Lambda Layer", + "supportedQueryMethods": { + "list": true, + "listDescription": "List all lambda layers" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json new file mode 100644 index 00000000..783c3d5d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall-policy.json @@ -0,0 +1,23 @@ +{ + "type": "network-firewall-firewall-policy", + "category": 3, + "potentialLinks": [ + "network-firewall-rule-group", + "network-firewall-tls-inspection-configuration", + "kms-key" + ], + "descriptiveName": "Network Firewall Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall Policy by name", + "list": true, + "listDescription": "List Network Firewall Policies", + "search": true, + "searchDescription": "Search for Network Firewall Policies by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_firewall_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json new file mode 100644 index 00000000..c140fdfc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-firewall.json @@ -0,0 +1,28 @@ +{ + "type": "network-firewall-firewall", + "category": 3, + "potentialLinks": [ + "network-firewall-firewall-policy", + "ec2-subnet", + "ec2-vpc", + "logs-log-group", + "s3-bucket", + "firehose-delivery-stream", + "iam-policy", + "kms-key" + ], + "descriptiveName": "Network Firewall", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall by name", + "list": true, + "listDescription": "List Network Firewalls", + "search": true, + "searchDescription": "Search for Network Firewalls by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_firewall.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json new file mode 100644 index 00000000..74559d23 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-rule-group.json @@ -0,0 +1,19 @@ +{ + "type": "network-firewall-rule-group", + "category": 4, + "potentialLinks": ["kms-key", "sns-topic", "network-firewall-rule-group"], + "descriptiveName": "Network Firewall Rule Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall Rule Group by name", + "list": true, + "listDescription": "List Network Firewall Rule Groups", + "search": true, + "searchDescription": "Search for Network Firewall Rule Groups by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkfirewall_rule_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json b/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json new file mode 100644 index 00000000..fa09ef24 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/network-firewall-tls-inspection-configuration.json @@ -0,0 +1,19 @@ +{ + "type": "network-firewall-tls-inspection-configuration", + "category": 7, + "potentialLinks": [ + "acm-certificate", + "acm-pca-certificate-authority", + "acm-pca-certificate-authority-certificate", + "network-firewall-encryption-configuration" + ], + "descriptiveName": "Network Firewall TLS Inspection Configuration", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Network Firewall TLS Inspection Configuration by name", + "list": true, + "listDescription": "List Network Firewall TLS Inspection Configurations", + "search": true, + "searchDescription": "Search for Network Firewall TLS Inspection Configurations by ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json new file mode 100644 index 00000000..b25960d6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-attachment.json @@ -0,0 +1,14 @@ +{ + "type": "networkmanager-connect-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager Connect Attachment", + "supportedQueryMethods": { + "get": true + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json new file mode 100644 index 00000000..439f70bc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer-association.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-connect-peer-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-connect-peer", + "networkmanager-device", + "networkmanager-link" + ], + "descriptiveName": "Networkmanager Connect Peer Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connect Peer Association", + "list": true, + "listDescription": "List all Networkmanager Connect Peer Associations", + "search": true, + "searchDescription": "Search for Networkmanager ConnectPeerAssociations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json new file mode 100644 index 00000000..9b92d159 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connect-peer.json @@ -0,0 +1,21 @@ +{ + "type": "networkmanager-connect-peer", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "networkmanager-connect-attachment", + "ip", + "rdap-asn", + "ec2-subnet" + ], + "descriptiveName": "Networkmanager Connect Peer", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connect Peer by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_connect_peer.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json new file mode 100644 index 00000000..f8e45be7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-connection.json @@ -0,0 +1,22 @@ +{ + "type": "networkmanager-connection", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager Connection", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Connection", + "search": true, + "searchDescription": "Search for Networkmanager Connections by GlobalNetworkId, Device ARN, or Connection ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_connection.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json new file mode 100644 index 00000000..922a72cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network-policy.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-core-network-policy", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager Core Network Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Core Network Policy by Core Network id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network_policy.core_network_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json new file mode 100644 index 00000000..701eb4f6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-core-network.json @@ -0,0 +1,18 @@ +{ + "type": "networkmanager-core-network", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network-policy", + "networkmanager-connect-peer" + ], + "descriptiveName": "Networkmanager Core Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Core Network by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_core_network.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json new file mode 100644 index 00000000..8a83b60c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-device.json @@ -0,0 +1,24 @@ +{ + "type": "networkmanager-device", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-site", + "networkmanager-link-association", + "networkmanager-connection", + "networkmanager-network-resource-relationship" + ], + "descriptiveName": "Networkmanager Device", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Device", + "search": true, + "searchDescription": "Search for Networkmanager Devices by GlobalNetworkId, {GlobalNetworkId|SiteId} or ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_device.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json new file mode 100644 index 00000000..d699bb33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-global-network.json @@ -0,0 +1,29 @@ +{ + "type": "networkmanager-global-network", + "category": 3, + "potentialLinks": [ + "networkmanager-site", + "networkmanager-transit-gateway-registration", + "networkmanager-connect-peer-association", + "networkmanager-transit-gateway-connect-peer-association", + "networkmanager-network-resource-relationship", + "networkmanager-link", + "networkmanager-device", + "networkmanager-connection" + ], + "descriptiveName": "Network Manager Global Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a global network by id", + "list": true, + "listDescription": "List all global networks", + "search": true, + "searchDescription": "Search for a global network by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_global_network.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json new file mode 100644 index 00000000..4a80e3fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link-association.json @@ -0,0 +1,16 @@ +{ + "type": "networkmanager-link-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager LinkAssociation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Link Association", + "search": true, + "searchDescription": "Search for Networkmanager Link Associations by GlobalNetworkId and DeviceId or LinkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json new file mode 100644 index 00000000..950eeb0e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-link.json @@ -0,0 +1,23 @@ +{ + "type": "networkmanager-link", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link-association", + "networkmanager-site", + "networkmanager-network-resource-relationship" + ], + "descriptiveName": "Networkmanager Link", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Link", + "search": true, + "searchDescription": "Search for Networkmanager Links by GlobalNetworkId, GlobalNetworkId with SiteId, or ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_link.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json new file mode 100644 index 00000000..b98c6c14 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-network-resource-relationship.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-network-resource-relationship", + "category": 3, + "potentialLinks": [ + "networkmanager-connection", + "networkmanager-device", + "networkmanager-link", + "networkmanager-site", + "directconnect-connection", + "directconnect-direct-connect-gateway", + "directconnect-virtual-interface", + "ec2-customer" + ], + "descriptiveName": "Networkmanager Network Resource Relationships", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for Networkmanager NetworkResourceRelationships by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json new file mode 100644 index 00000000..78a07ca5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site-to-site-vpn-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-site-to-site-vpn-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network", "ec2-vpn-connection"], + "descriptiveName": "Networkmanager Site To Site Vpn Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Site To Site Vpn Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_site_to_site_vpn_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json new file mode 100644 index 00000000..e34cb165 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-site.json @@ -0,0 +1,22 @@ +{ + "type": "networkmanager-site", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-link", + "networkmanager-device" + ], + "descriptiveName": "Networkmanager Site", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Site", + "search": true, + "searchDescription": "Search for Networkmanager Sites by GlobalNetworkId or Site ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_networkmanager_site.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json new file mode 100644 index 00000000..dc8fa0ca --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-connect-peer-association.json @@ -0,0 +1,18 @@ +{ + "type": "networkmanager-transit-gateway-connect-peer-association", + "category": 3, + "potentialLinks": [ + "networkmanager-global-network", + "networkmanager-device", + "networkmanager-link" + ], + "descriptiveName": "Networkmanager Transit Gateway Connect Peer Association", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Connect Peer Association by id", + "list": true, + "listDescription": "List all Networkmanager Transit Gateway Connect Peer Associations", + "search": true, + "searchDescription": "Search for Networkmanager Transit Gateway Connect Peer Associations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json new file mode 100644 index 00000000..1790a705 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-peering.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-transit-gateway-peering", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "ec2-transit-gateway-peering-attachment", + "ec2-transit-gateway" + ], + "descriptiveName": "Networkmanager Transit Gateway Peering", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Peering by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_transit_gateway_peering.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json new file mode 100644 index 00000000..9f38242b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-registration.json @@ -0,0 +1,14 @@ +{ + "type": "networkmanager-transit-gateway-registration", + "category": 3, + "potentialLinks": ["networkmanager-global-network", "ec2-transit-gateway"], + "descriptiveName": "Networkmanager Transit Gateway Registrations", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Registrations", + "list": true, + "listDescription": "List all Networkmanager Transit Gateway Registrations", + "search": true, + "searchDescription": "Search for Networkmanager Transit Gateway Registrations by GlobalNetworkId" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json new file mode 100644 index 00000000..a07ef503 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-transit-gateway-route-table-attachment.json @@ -0,0 +1,19 @@ +{ + "type": "networkmanager-transit-gateway-route-table-attachment", + "category": 3, + "potentialLinks": [ + "networkmanager-core-network", + "networkmanager-transit-gateway-peering", + "ec2-transit-gateway-route-table" + ], + "descriptiveName": "Networkmanager Transit Gateway Route Table Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager Transit Gateway Route Table Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_transit_gateway_route_table_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json b/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json new file mode 100644 index 00000000..c7ad8625 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/networkmanager-vpc-attachment.json @@ -0,0 +1,15 @@ +{ + "type": "networkmanager-vpc-attachment", + "category": 3, + "potentialLinks": ["networkmanager-core-network"], + "descriptiveName": "Networkmanager VPC Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Networkmanager VPC Attachment by id" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_networkmanager_vpc_attachment.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json new file mode 100644 index 00000000..31c354f1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster-parameter-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-db-cluster-parameter-group", + "category": 6, + "descriptiveName": "RDS Cluster Parameter Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all RDS parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_rds_cluster_parameter_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json new file mode 100644 index 00000000..6c88f5fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-cluster.json @@ -0,0 +1,30 @@ +{ + "type": "rds-db-cluster", + "category": 6, + "potentialLinks": [ + "rds-db-subnet-group", + "dns", + "rds-db-cluster", + "ec2-security-group", + "route53-hosted-zone", + "kms-key", + "kinesis-stream", + "rds-option-group", + "secretsmanager-secret", + "iam-role" + ], + "descriptiveName": "RDS Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all RDS parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_rds_cluster.cluster_identifier" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json new file mode 100644 index 00000000..0fd7c367 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-instance.json @@ -0,0 +1,37 @@ +{ + "type": "rds-db-instance", + "category": 6, + "potentialLinks": [ + "dns", + "route53-hosted-zone", + "ec2-security-group", + "rds-db-parameter-group", + "rds-db-subnet-group", + "rds-db-cluster", + "kms-key", + "logs-log-stream", + "iam-role", + "kinesis-stream", + "backup-recovery-point", + "iam-instance-profile", + "rds-db-instance-automated-backup", + "secretsmanager-secret" + ], + "descriptiveName": "RDS Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an instance by ID", + "list": true, + "listDescription": "List all instances", + "search": true, + "searchDescription": "Search for instances by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_db_instance.identifier" + }, + { + "terraformQueryMap": "aws_db_instance_role_association.db_instance_identifier" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json new file mode 100644 index 00000000..f386e986 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-parameter-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-db-parameter-group", + "category": 6, + "descriptiveName": "RDS Parameter Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a parameter group by name", + "list": true, + "listDescription": "List all parameter groups", + "search": true, + "searchDescription": "Search for a parameter group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_parameter_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json new file mode 100644 index 00000000..80a46185 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-db-subnet-group.json @@ -0,0 +1,20 @@ +{ + "type": "rds-db-subnet-group", + "category": 3, + "potentialLinks": ["ec2-vpc", "ec2-subnet", "outposts-outpost"], + "descriptiveName": "RDS Subnet Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a subnet group by name", + "list": true, + "listDescription": "List all subnet groups", + "search": true, + "searchDescription": "Search for subnet groups by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_subnet_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json b/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json new file mode 100644 index 00000000..e31b8431 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/rds-option-group.json @@ -0,0 +1,19 @@ +{ + "type": "rds-option-group", + "category": 6, + "descriptiveName": "RDS Option Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an option group by name", + "list": true, + "listDescription": "List all RDS option groups", + "search": true, + "searchDescription": "Search for an option group by ARN" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_db_option_group.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json b/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json new file mode 100644 index 00000000..a6a6f9d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-health-check.json @@ -0,0 +1,19 @@ +{ + "type": "route53-health-check", + "category": 5, + "potentialLinks": ["cloudwatch-alarm"], + "descriptiveName": "Route53 Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get health check by ID", + "list": true, + "listDescription": "List all health checks", + "search": true, + "searchDescription": "Search for health checks by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route53_health_check.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json b/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json new file mode 100644 index 00000000..d9d89b61 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-hosted-zone.json @@ -0,0 +1,25 @@ +{ + "type": "route53-hosted-zone", + "category": 3, + "potentialLinks": ["route53-resource-record-set"], + "descriptiveName": "Hosted Zone", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a hosted zone by ID", + "list": true, + "listDescription": "List all hosted zones", + "search": true, + "searchDescription": "Search for a hosted zone by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_route53_hosted_zone_dnssec.id" + }, + { + "terraformQueryMap": "aws_route53_zone.zone_id" + }, + { + "terraformQueryMap": "aws_route53_zone_association.zone_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json b/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json new file mode 100644 index 00000000..7243d98a --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/route53-resource-record-set.json @@ -0,0 +1,22 @@ +{ + "type": "route53-resource-record-set", + "category": 3, + "potentialLinks": ["dns", "route53-health-check"], + "descriptiveName": "Route53 Record Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Route53 record Set by name", + "search": true, + "searchDescription": "Search for a record set by hosted zone ID in the format \"/hostedzone/JJN928734JH7HV\" or \"JJN928734JH7HV\" or by terraform ID in the format \"{hostedZone}_{recordName}_{type}\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "aws_route53_record.arn" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_route53_record.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json b/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json new file mode 100644 index 00000000..cb01e849 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/s3-bucket.json @@ -0,0 +1,82 @@ +{ + "type": "s3-bucket", + "category": 2, + "potentialLinks": ["lambda-function", "sqs-queue", "sns-topic", "s3-bucket"], + "descriptiveName": "S3 Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an S3 bucket by name", + "list": true, + "listDescription": "List all S3 buckets", + "search": true, + "searchDescription": "Search for S3 buckets by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_s3_bucket_acl.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_analytics_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_cors_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_intelligent_tiering_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_inventory.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_lifecycle_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_logging.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_metric.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_notification.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_object_lock_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_object.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_ownership_controls.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_policy.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_public_access_block.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_replication_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_request_payment_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_server_side_encryption_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_versioning.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket_website_configuration.bucket" + }, + { + "terraformQueryMap": "aws_s3_bucket.id" + }, + { + "terraformQueryMap": "aws_s3_object_copy.bucket" + }, + { + "terraformQueryMap": "aws_s3_object.bucket" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json b/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json new file mode 100644 index 00000000..18221376 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-data-protection-policy.json @@ -0,0 +1,17 @@ +{ + "type": "sns-data-protection-policy", + "category": 7, + "potentialLinks": ["sns-topic"], + "descriptiveName": "SNS Data Protection Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS data protection policy by associated topic ARN", + "search": true, + "searchDescription": "Search SNS data protection policies by its ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic_data_protection_policy.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json b/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json new file mode 100644 index 00000000..eb5b8c4f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-endpoint.json @@ -0,0 +1,11 @@ +{ + "type": "sns-endpoint", + "category": 7, + "descriptiveName": "SNS Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS endpoint by its ARN", + "search": true, + "searchDescription": "Search SNS endpoints by associated Platform Application ARN" + } +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json b/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json new file mode 100644 index 00000000..cb45be1e --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-platform-application.json @@ -0,0 +1,19 @@ +{ + "type": "sns-platform-application", + "category": 7, + "potentialLinks": ["sns-endpoint"], + "descriptiveName": "SNS Platform Application", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS platform application by its ARN", + "list": true, + "listDescription": "List all SNS platform applications", + "search": true, + "searchDescription": "Search SNS platform applications by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_platform_application.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json b/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json new file mode 100644 index 00000000..fb347d40 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-subscription.json @@ -0,0 +1,19 @@ +{ + "type": "sns-subscription", + "category": 7, + "potentialLinks": ["sns-topic", "iam-role"], + "descriptiveName": "SNS Subscription", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS subscription by its ARN", + "list": true, + "listDescription": "List all SNS subscriptions", + "search": true, + "searchDescription": "Search SNS subscription by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic_subscription.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sns-topic.json b/docs.overmind.tech/docs/sources/aws/data/sns-topic.json new file mode 100644 index 00000000..ae9e460b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sns-topic.json @@ -0,0 +1,19 @@ +{ + "type": "sns-topic", + "category": 7, + "potentialLinks": ["kms-key"], + "descriptiveName": "SNS Topic", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SNS topic by its ARN", + "list": true, + "listDescription": "List all SNS topics", + "search": true, + "searchDescription": "Search SNS topic by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sns_topic.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json b/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json new file mode 100644 index 00000000..f21fdf16 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/sqs-queue.json @@ -0,0 +1,19 @@ +{ + "type": "sqs-queue", + "category": 1, + "potentialLinks": ["http", "lambda-event-source-mapping"], + "descriptiveName": "SQS Queue", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SQS queue attributes by its URL", + "list": true, + "listDescription": "List all SQS queue URLs", + "search": true, + "searchDescription": "Search SQS queue by ARN" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_sqs_queue.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json b/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json new file mode 100644 index 00000000..5d71df18 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ssm-parameter.json @@ -0,0 +1,23 @@ +{ + "type": "ssm-parameter", + "category": 7, + "potentialLinks": ["ip", "http", "dns"], + "descriptiveName": "SSM Parameter", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an SSM parameter by name", + "list": true, + "listDescription": "List all SSM parameters", + "search": true, + "searchDescription": "Search for SSM parameters by ARN. This supports ARNs from IAM policies that contain wildcards" + }, + "terraformMappings": [ + { + "terraformQueryMap": "aws_ssm_parameter.name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "aws_ssm_parameter.arn" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md new file mode 100644 index 00000000..64c40288 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md @@ -0,0 +1,177 @@ +--- +title: Update IAM Role for Enhanced Security +sidebar_position: 2 +--- + +# Updating Your AWS IAM Role for Enhanced Security + +Starting December 2025, we've enhanced how Overmind connects to your AWS infrastructure using **EKS Pod Identity**. This update improves security by using short-lived, automatically rotated credentials when accessing your AWS resources. + +## Why This Update is Important + +Previously, Overmind used static AWS credentials to assume the IAM role in your account. With EKS Pod Identity, we now use: + +- **Short-lived credentials** that are automatically rotated +- **Session tagging** for better auditability and tracing +- **Reduced attack surface** with no long-lived credentials + +To benefit from these security improvements, you need to update the IAM role trust policy in your AWS account to allow the `sts:TagSession` permission. + +## How to Check if You Need to Update + +You can check if your IAM role needs updating by looking at the version tag: + +1. Open the **AWS IAM Console** +2. Navigate to **Roles** and find your Overmind role (usually named "Overmind" or "overmind-read-only") +3. Click on the role and go to the **Tags** tab +4. Look for the `overmind.version` tag + +| Version Tag | Status | +|-------------|--------| +| `2025-12-01` or later | ✅ Up to date | +| `2023-03-14` or earlier | ⚠️ Update required | +| No tag | ⚠️ Update required | + +## Update Instructions + +### Option A: Update via CloudFormation (Recommended) + +If you originally created your IAM role using our CloudFormation template, follow these steps: + +#### Step 1: Open AWS CloudFormation Console + +Go to the [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation) in the region where you deployed the Overmind stack. + +#### Step 2: Select the Overmind Stack + +Find and select the CloudFormation stack named **"Overmind"** (or "OvermindDevelopment" for development environments). + +:::tip +Look for a stack named "Overmind" or "OvermindDevelopment" in the region where you originally deployed it. +::: + +#### Step 3: Update the Stack + +1. Click the **"Update"** button at the top of the page +2. Under "Prepare template", select **"Replace existing template"** +3. Under "Specify template", select **"Amazon S3 URL"** +4. Enter the template URL provided by Overmind (see below for how to find it) +5. Click **"Next"** + +![Screenshot of AWS CloudFormation Update stack wizard showing "Replace existing template" selected and the Amazon S3 URL input field](./cloudformation-update-stack.png) + +:::info Finding the CloudFormation Template URL +To get the latest CloudFormation template URL: +1. Go to [Overmind Settings > Sources](https://app.overmind.tech/settings/sources) +2. Click **Add Source > AWS** +3. Right-click the "Deploy" button and copy the link - the URL contains the `templateURL` parameter +::: + +#### Step 4: Review and Apply + +1. Keep the existing **External ID** parameter unchanged +2. Click **"Next"** through the configuration pages +3. On the review page, check the box acknowledging that CloudFormation might create IAM resources +4. Click **"Submit"** + +The update typically takes less than a minute to complete. + +### Option B: Manual Update + +If you prefer to update the IAM role manually, or if you created the role without CloudFormation: + +#### Step 1: Open IAM Console + +Go to the [AWS IAM Console](https://console.aws.amazon.com/iam) and navigate to **Roles**. + +#### Step 2: Find Your Overmind Role + +Search for and select your Overmind role (usually named "Overmind" or the name you specified during setup). + +#### Step 3: Edit the Trust Policy + +1. Go to the **Trust relationships** tab +2. Click **"Edit trust policy"** +3. Add the following statement to the `Statement` array: + +```json +{ + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:TagSession" +} +``` + +Your complete trust policy should look like this: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "YOUR-EXTERNAL-ID-HERE" + } + } + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:TagSession" + } + ] +} +``` + +4. Click **"Update policy"** + +#### Step 4: Update the Version Tag (Optional) + +To help track the version of your role configuration: + +1. Go to the **Tags** tab +2. Add or update the tag: + - **Key:** `overmind.version` + - **Value:** `2025-12-01` + +## Verification + +After updating, your existing AWS sources will continue to work without interruption. The enhanced security features will be automatically enabled within the next few minutes. + +You can verify the update was successful by: +1. Checking that your source shows a green status in [Overmind Settings > Sources](https://app.overmind.tech/settings/sources) +2. Verifying the role's `overmind.version` tag shows `2025-12-01` or later + +## Frequently Asked Questions + +### Will this cause any downtime? + +No. The update adds a new permission without removing any existing permissions. Your sources will continue to work throughout the update process. + +### What if I have multiple AWS accounts? + +You'll need to update the IAM role in each AWS account where you have an Overmind source configured. + +### What happens if I don't update? + +Your sources will continue to work, but won't benefit from the enhanced security features provided by EKS Pod Identity. We strongly recommend updating for improved security posture. + +### I'm using a different Overmind AWS account ID + +If you're on a dedicated or on-premises deployment, the AWS account ID in the trust policy may be different. Contact your Overmind administrator for the correct account ID. + +## Need Help? + +If you encounter any issues during the update: + +- Contact support at support@overmind.tech diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md new file mode 100644 index 00000000..58334249 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-batch-prediction-job.md @@ -0,0 +1,34 @@ +--- +title: GCP Ai Platform Batch Prediction Job +sidebar_label: gcp-ai-platform-batch-prediction-job +--- + +A GCP AI Platform (Vertex AI) Batch Prediction Job is a managed job that runs a trained model against a large, static dataset to generate predictions asynchronously. It allows you to score data stored in Cloud Storage or BigQuery and write the results back to either service, without having to manage your own compute infrastructure. For full details see the official documentation: https://docs.cloud.google.com/vertex-ai/docs/predictions/get-batch-predictions + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-batch-prediction-job by its "locations|batchPredictionJobs" +- ~~`LIST`~~ +- `SEARCH`: Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1' + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +The batch prediction job references a trained model that provides the prediction logic. The job cannot run without specifying this model. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Input data for a batch prediction can come from a BigQuery table, and the job can also write the prediction results to another BigQuery table. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Customer-managed encryption keys (CMEK) from Cloud KMS may be attached to the job to encrypt its output artefacts stored in Cloud Storage or BigQuery. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +The job is executed under a specific IAM service account, which grants it permissions to read inputs, write outputs, and access the model. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets are commonly used to supply the input files (in JSONL or CSV) and/or to store the prediction output files produced by the batch job. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md new file mode 100644 index 00000000..4a4c1935 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-custom-job.md @@ -0,0 +1,35 @@ +--- +title: GCP Ai Platform Custom Job +sidebar_label: gcp-ai-platform-custom-job +--- + +A GCP AI Platform Custom Job (now part of Vertex AI) is a fully-managed training workload that runs user-supplied code inside one or more container images on Google Cloud infrastructure. It allows you to specify machine types, accelerators, networking and encryption settings, then orchestrates the provisioning, execution and clean-up of the training cluster. Custom Jobs are typically used when pre-built AutoML options are insufficient and you need complete control over your training loop. +Official documentation: https://cloud.google.com/vertex-ai/docs/training/create-custom-job + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-custom-job by its "name" +- `LIST`: List all gcp-ai-platform-custom-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +A successful Custom Job can optionally upload the trained artefacts as a Vertex AI Model resource; if that happens, the job will reference (and be referenced by) the resulting `gcp-ai-platform-model`. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Custom Jobs support customer-managed encryption keys (CMEK). When a CMEK is specified, the job resource, its logs and any artefacts it creates are encrypted with the referenced `gcp-cloud-kms-crypto-key`. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +You can run Custom Jobs inside a specific VPC network to reach private data sources or to avoid egress to the public internet. In that case the job is linked to the chosen `gcp-compute-network`. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Execution of a Custom Job occurs under a user-specified service account, which determines the permissions the training containers possess. The job therefore has a direct relationship to a `gcp-iam-service-account`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Training code commonly reads data from, and writes checkpoints or model artefacts to, Cloud Storage. The buckets used for staging, input or output will be surfaced as linked `gcp-storage-bucket` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md new file mode 100644 index 00000000..bcbe74fe --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-endpoint.md @@ -0,0 +1,35 @@ +--- +title: GCP Ai Platform Endpoint +sidebar_label: gcp-ai-platform-endpoint +--- + +A Vertex AI (formerly AI Platform) **Endpoint** is a regional resource that serves as an entry-point for online prediction requests in Google Cloud. One or more trained **Models** can be deployed to an Endpoint, after which client applications invoke the Endpoint’s HTTPS URL (or Private Service Connect address) to obtain real-time predictions. The resource stores configuration such as traffic splitting between models, logging settings, encryption settings and the VPC network to be used for private access. +Official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.endpoints + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-endpoint by its "name" +- `LIST`: List all gcp-ai-platform-endpoint +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +An Endpoint may contain one or many `deployedModel` blocks, each of which references a separate Model resource. Overmind links the Endpoint to every Model that is currently deployed or that has traffic allocated to it. + +### [`gcp-ai-platform-model-deployment-monitoring-job`](/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job) + +If model-deployment monitoring has been enabled, the monitoring job resource records statistics and drift detection for a specific Endpoint. Overmind links the Endpoint to all monitoring jobs that target it. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Prediction logging and monitoring can be configured to write request/response data into BigQuery tables. Those tables are therefore linked to the Endpoint that produced the records. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Endpoints can be created with a Customer-Managed Encryption Key (CMEK) via the `encryptionSpec.kmsKeyName` field. Overmind links the Endpoint to the specific Cloud KMS CryptoKey it uses for at-rest encryption. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When an Endpoint is set up for private predictions, it must specify a VPC network (`network` field) that will be used for Private Service Connect. This creates a relationship between the Endpoint and the referenced Compute Network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md new file mode 100644 index 00000000..69cb6b26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job.md @@ -0,0 +1,32 @@ +--- +title: GCP Ai Platform Model Deployment Monitoring Job +sidebar_label: gcp-ai-platform-model-deployment-monitoring-job +--- + +A Model Deployment Monitoring Job in Vertex AI (formerly AI Platform) performs continuous evaluation of a model that has been deployed to an endpoint. The job collects prediction requests and responses, analyses them for data drift, feature skew, and other anomalies, and can raise alerts when thresholds are exceeded. This enables teams to detect issues in production models early and take corrective action before business impact occurs. + +Official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.modelDeploymentMonitoringJobs + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-model-deployment-monitoring-job by its "locations|modelDeploymentMonitoringJobs" +- ~~`LIST`~~ +- `SEARCH`: Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1' + +## Possible Links + +### [`gcp-ai-platform-endpoint`](/sources/gcp/Types/gcp-ai-platform-endpoint) + +A Model Deployment Monitoring Job is always attached to a specific Vertex AI endpoint; it monitors one or more model deployments that live on that endpoint. The link represents the `endpoint` field inside the job resource. + +### [`gcp-ai-platform-model`](/sources/gcp/Types/gcp-ai-platform-model) + +Within `modelDeploymentMonitoringObjectiveConfigs`, the job specifies the deployed model(s) it should watch. This link captures that relationship between the monitoring job and the underlying Vertex AI model resources. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the job is created with `encryptionSpec`, it uses a customer-managed Cloud KMS key to encrypt monitoring logs and metadata. The linked Crypto Key represents that key. + +### [`gcp-monitoring-notification-channel`](/sources/gcp/Types/gcp-monitoring-notification-channel) + +Alerting for drift or skew relies on Cloud Monitoring notification channels listed in the job’s `alertConfig.notificationChannels`. This link connects the monitoring job to those channels so users can trace how alerts will be delivered. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md new file mode 100644 index 00000000..6e685c19 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-model.md @@ -0,0 +1,31 @@ +--- +title: GCP Ai Platform Model +sidebar_label: gcp-ai-platform-model +--- + +A **GCP AI Platform Model** (now part of Vertex AI) is a top-level resource that represents a machine-learning model and its metadata. It groups together one or more model versions (or “Model resources” in Vertex AI terminology), defines the serving container, encryption settings and access controls, and can be deployed to online prediction endpoints or used by batch prediction jobs. +For full details, see the official documentation: https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.models + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-model by its "name" +- `LIST`: List all gcp-ai-platform-model +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-ai-platform-endpoint`](/sources/gcp/Types/gcp-ai-platform-endpoint) + +An AI Platform Model can be deployed to one or more endpoints. When Overmind detects that a model has been deployed, it links the model to the corresponding `gcp-ai-platform-endpoint` resource so that you can see where the model is serving traffic. + +### [`gcp-ai-platform-pipeline-job`](/sources/gcp/Types/gcp-ai-platform-pipeline-job) + +Vertex AI Pipeline Jobs often produce models as artefacts at the end of a training pipeline. Overmind links a `gcp-ai-platform-pipeline-job` to the `gcp-ai-platform-model` it created (or updated) so you can trace the provenance of a model back to the pipeline run that generated it. + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +Models use a container image for prediction service. If that container image is stored in Artifact Registry, Overmind establishes a link between the model and the `gcp-artifact-registry-docker-image` representing the serving container. This highlights dependencies on specific container images and versions. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the model, the model resource references the Cloud KMS Crypto Key used to encrypt the model data at rest. Overmind links the model to the `gcp-cloud-kms-crypto-key` to surface encryption dependencies and potential key-rotation risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md new file mode 100644 index 00000000..d15fdf9f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-ai-platform-pipeline-job.md @@ -0,0 +1,31 @@ +--- +title: GCP Ai Platform Pipeline Job +sidebar_label: gcp-ai-platform-pipeline-job +--- + +A **GCP AI Platform Pipeline Job** (now part of Vertex AI Pipelines) represents a managed execution of a Kubeflow pipeline on Google Cloud. It orchestrates a series of container-based tasks—such as data preprocessing, model training, and deployment—into a reproducible workflow that runs on Google-managed infrastructure. Each job stores its metadata, intermediate artefacts and logs in Google-hosted services, and can be monitored, retried or version-controlled through the Vertex AI console or API. +For full details, see the official documentation: [Vertex AI Pipelines – Run pipeline jobs](https://docs.cloud.google.com/vertex-ai/docs/pipelines/run-pipeline). + +## Supported Methods + +- `GET`: Get a gcp-ai-platform-pipeline-job by its "name" +- `LIST`: List all gcp-ai-platform-pipeline-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A pipeline job can be configured to use customer-managed encryption keys (CMEK) so that all intermediate artefacts and metadata produced by the pipeline are encrypted with a specific Cloud KMS crypto key. Overmind therefore surfaces a link to the `gcp-cloud-kms-crypto-key` that protects the job’s resources. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Pipeline components often run on GKE clusters or custom training/serving services that are attached to a VPC network. When a job specifies a `network` or `privateClusterConfig`, Overmind links the job to the corresponding `gcp-compute-network`, highlighting network-level exposure or egress restrictions that may affect the pipeline. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every pipeline job executes under a service account whose IAM permissions determine which Google Cloud resources the job can access (e.g. storage buckets, BigQuery datasets). Overmind connects the job to that `gcp-iam-service-account` so that permission scopes and potential privilege escalations can be inspected. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Pipeline jobs read from and write to Cloud Storage for dataset ingestion, model artefact output and pipeline metadata storage. Any bucket referenced in the job’s `pipeline_root`, component arguments or logging configuration is linked here, allowing visibility into data residency, ACLs and lifecycle policies relevant to the pipeline’s operation. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md new file mode 100644 index 00000000..ce46b462 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-artifact-registry-docker-image.md @@ -0,0 +1,17 @@ +--- +title: GCP Artifact Registry Docker Image +sidebar_label: gcp-artifact-registry-docker-image +--- + +A GCP Artifact Registry Docker Image resource represents a single immutable image stored in Google Cloud’s Artifact Registry. It contains metadata such as the image digest, tags, size and creation timestamp, and can be queried to understand exactly which layers and versions are about to be deployed. Managing this resource allows you to verify provenance, scan for vulnerabilities and enforce policies before the image ever reaches production. +For a full description of the REST resource, see Google’s official documentation: https://cloud.google.com/artifact-registry/docs/reference/rest/v1/projects.locations.repositories.dockerImages + +**Terrafrom Mappings:** + +- `google_artifact_registry_docker_image.name` + +## Supported Methods + +- `GET`: Get a gcp-artifact-registry-docker-image by its "locations|repositories|dockerImages" +- ~~`LIST`~~ +- `SEARCH`: Search for Docker images in Artifact Registry. Use the format "location|repository_id" or "projects/[project]/locations/[location]/repository/[repository_id]/dockerImages/[docker_image]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md new file mode 100644 index 00000000..14a1cebc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-data-transfer-transfer-config.md @@ -0,0 +1,31 @@ +--- +title: GCP Big Query Data Transfer Transfer Config +sidebar_label: gcp-big-query-data-transfer-transfer-config +--- + +The BigQuery Data Transfer Service Transfer Config defines a scheduled data-transfer job in Google Cloud. It specifies where the data comes from (for example Google Ads, YouTube or an external Cloud Storage bucket), the destination BigQuery dataset, the refresh window, schedule, run-options, encryption settings and notification preferences. In essence, it is the canonical object that tells BigQuery Data Transfer Service what to move, when to move it and how to handle the resulting tables. +Official documentation: https://docs.cloud.google.com/bigquery/docs/working-with-transfers + +**Terrafrom Mappings:** + +- `google_bigquery_data_transfer_config.id` + +## Supported Methods + +- `GET`: Get a gcp-big-query-data-transfer-transfer-config by its "locations|transferConfigs" +- ~~`LIST`~~ +- `SEARCH`: Search for BigQuery Data Transfer transfer configs in a location. Use the format "location" or "projects/project_id/locations/location/transferConfigs/transfer_config_id" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +The transfer config’s `destinationDatasetId` points to the BigQuery dataset that will receive the imported data, so the config depends on – and is intrinsically linked to – that dataset. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption is enabled, the transfer config references a Cloud KMS CryptoKey that is used to encrypt the tables created by the transfer, creating a dependency on the key. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Through the `notificationPubsubTopic` field, the transfer config can publish status and error messages about individual transfer runs to a Pub/Sub topic, establishing an outgoing link to that topic. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md new file mode 100644 index 00000000..cccc66c4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-dataset.md @@ -0,0 +1,39 @@ +--- +title: GCP Big Query Dataset +sidebar_label: gcp-big-query-dataset +--- + +A BigQuery Dataset is a top-level container that holds BigQuery tables, views, models and routines, and defines the geographic location where that data is stored. It also acts as the unit for access control, default encryption configuration and data lifecycle policies. +For full details see the Google Cloud documentation: https://cloud.google.com/bigquery/docs/datasets + +**Terrafrom Mappings:** + +- `google_bigquery_dataset.dataset_id` + +## Supported Methods + +- `GET`: Get GCP Big Query Dataset by "gcp-big-query-dataset-id" +- `LIST`: List all GCP Big Query Dataset items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A dataset can reference other datasets via authorised views or cross-dataset access entries. Those referenced datasets will be linked to the current item. + +### [`gcp-big-query-model`](/sources/gcp/Types/gcp-big-query-model) + +Every BigQuery ML model belongs to exactly one dataset. All models whose `dataset_id` matches this dataset will be linked. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +Tables and views are stored inside a dataset. All tables whose `dataset_id` equals this dataset will be linked. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the dataset is encrypted with a customer-managed key, the KMS Crypto Key used for default encryption will be linked here. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Service accounts that appear in the dataset’s IAM policy (for example as editors, owners, readers or custom roles) will be linked to show who can access or manage the dataset. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md new file mode 100644 index 00000000..d570b152 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md @@ -0,0 +1,26 @@ +--- +title: GCP Big Query Model +sidebar_label: gcp-big-query-model +--- + +A BigQuery Model is a logical resource that stores the metadata and artefacts produced by BigQuery ML when you train a machine-learning model. It lives inside a BigQuery dataset and can subsequently be queried, evaluated, exported or further trained. For a full description see the official Google Cloud documentation: https://cloud.google.com/bigquery/docs/reference/rest/v2/models + +## Supported Methods + +- `GET`: Get GCP Big Query Model by "gcp-big-query-dataset-id|gcp-big-query-model-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Model by "gcp-big-query-model-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +Each model is contained within exactly one BigQuery dataset. The link represents this parent–child relationship and allows Overmind to surface the impact of changes to the dataset on the model. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +A model is usually trained from, and may reference, one or more BigQuery tables (for example, the training, validation and prediction input tables). This link lets Overmind trace how alterations to those tables could affect the model’s behaviour or validity. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption keys (CMEK) are enabled, the model’s data is encrypted with a Cloud KMS crypto-key. Linking the model to the crypto-key allows Overmind to assess the consequences of key rotation, deletion or permission changes on the model’s availability. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md new file mode 100644 index 00000000..b3534ec1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-routine.md @@ -0,0 +1,22 @@ +--- +title: GCP Big Query Routine +sidebar_label: gcp-big-query-routine +--- + +A BigQuery Routine represents a user-defined piece of reusable logic—such as a stored procedure or user-defined function—that is stored inside a BigQuery dataset and can be invoked from SQL. Routines let teams encapsulate data-processing logic, share it across queries, and manage it with version control and Infrastructure-as-Code tools. For a full description of the capabilities and configuration options, see the Google Cloud documentation on routines (https://cloud.google.com/bigquery/docs/routines-intro). + +**Terrafrom Mappings:** + +- `google_bigquery_routine.routine_id` + +## Supported Methods + +- `GET`: Get GCP Big Query Routine by "gcp-big-query-dataset-id|gcp-big-query-routine-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Routine by "gcp-big-query-routine-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A routine is defined within a specific BigQuery dataset; the link shows the parent dataset that contains the routine. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md new file mode 100644 index 00000000..77f8bde5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-table.md @@ -0,0 +1,26 @@ +--- +title: GCP Big Query Table +sidebar_label: gcp-big-query-table +--- + +A BigQuery table is the fundamental unit of storage in Google Cloud BigQuery. It holds the rows of structured data that analysts query using SQL, and it defines the schema, partitioning, clustering, and encryption settings that govern how that data is stored and accessed. For a full description see the Google Cloud documentation: https://cloud.google.com/bigquery/docs/tables + +**Terrafrom Mappings:** + +- `google_bigquery_table.id` + +## Supported Methods + +- `GET`: Get GCP Big Query Table by "gcp-big-query-dataset-id|gcp-big-query-table-id" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Big Query Table by "gcp-big-query-dataset-id" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +Every BigQuery table is contained within exactly one dataset. This link represents that parent–child relationship, enabling Overmind to trace from a table back to the dataset that organises and administers it. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a BigQuery table is encrypted with a customer-managed encryption key (CMEK), this link points to the specific Cloud KMS crypto key in use. It allows Overmind to surface risks associated with key rotation, permissions, or key deletion that could affect the table’s availability or compliance posture. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md new file mode 100644 index 00000000..76c988b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-app-profile.md @@ -0,0 +1,27 @@ +--- +title: GCP Big Table Admin App Profile +sidebar_label: gcp-big-table-admin-app-profile +--- + +A Bigtable **App Profile** is a logical wrapper that tells Cloud Bigtable _how_ an application’s traffic should be routed, which clusters it can use, and what fail-over behaviour to apply. By creating multiple app profiles you can isolate workloads, direct different applications to specific clusters, or enable multi-cluster routing for higher availability. +For an in-depth explanation see the official documentation: https://cloud.google.com/bigtable/docs/app-profiles + +**Terrafrom Mappings:** + +- `google_bigtable_app_profile.id` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-app-profile by its "instances|appProfiles" +- ~~`LIST`~~ +- `SEARCH`: Search for BigTable App Profiles in an instance. Use the format "instance" or "projects/[project_id]/instances/[instance_name]/appProfiles/[app_profile_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +Every app profile specifies one or more clusters that client traffic may reach. Therefore an App Profile is directly linked to the Bigtable Cluster(s) it can route requests to. + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +An App Profile always belongs to exactly one Bigtable Instance; it cannot exist outside that instance’s administrative scope. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md new file mode 100644 index 00000000..58520f6c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-backup.md @@ -0,0 +1,27 @@ +--- +title: GCP Big Table Admin Backup +sidebar_label: gcp-big-table-admin-backup +--- + +A Cloud Bigtable Backup is a point-in-time copy of a Bigtable table that is managed by the Bigtable Admin API. It allows you to protect data against accidental deletion or corruption and to restore the table later, either in the same cluster or in a different one within the same instance. Each backup is stored in a specific cluster, retains the table’s schema and data as they existed at the moment the backup was taken, and can be kept for a user-defined retention period. +Official documentation: https://docs.cloud.google.com/bigtable/docs/backups + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-backup by its "instances|clusters|backups" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-big-table-admin-backup by its "instances|clusters" + +## Possible Links + +### [`gcp-big-table-admin-backup`](/sources/gcp/Types/gcp-big-table-admin-backup) + +The current item represents the Backup resource itself, containing metadata such as name, creation time, size, expiration time and the source table it protects. + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +Each backup is physically stored in exactly one Bigtable cluster; this link shows the parent cluster that owns and stores the backup. + +### [`gcp-big-table-admin-table`](/sources/gcp/Types/gcp-big-table-admin-table) + +A backup is created from a specific table; this link identifies that source table and allows you to see which tables can be restored from the backup. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md new file mode 100644 index 00000000..c0a71a23 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-cluster.md @@ -0,0 +1,23 @@ +--- +title: GCP Big Table Admin Cluster +sidebar_label: gcp-big-table-admin-cluster +--- + +A Cloud Bigtable cluster represents the set of serving and storage resources that handle all reads and writes for a Cloud Bigtable instance. Each cluster belongs to a single instance, lives in one Google Cloud zone, and is configured with a certain number of nodes and a specific storage type (SSD or HDD). Clusters can be added or removed to provide high availability, geographic redundancy, or additional throughput. With Overmind you can surface mis-configurations such as a single-zone deployment, inadequate node counts, or missing encryption settings before your change reaches production. +Official Google documentation: https://cloud.google.com/bigtable/docs/overview#clusters + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-cluster by its "instances|clusters" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-big-table-admin-cluster by its "instances" + +## Possible Links + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +Every cluster is a child resource of a Cloud Bigtable instance. Overmind links the cluster back to its parent instance so you can see which database workloads will be affected if you modify or delete the cluster. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +When customer-managed encryption keys (CMEK) are enabled for a Bigtable cluster, the cluster references a Cloud KMS crypto key. Overmind creates a link to that key so you can verify the key’s status, rotation schedule, and IAM policy before deploying changes to the cluster. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md new file mode 100644 index 00000000..7c8d7a88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-instance.md @@ -0,0 +1,24 @@ +--- +title: GCP Big Table Admin Instance +sidebar_label: gcp-big-table-admin-instance +--- + +Google Cloud Bigtable is Google’s fully managed, scalable NoSQL database service. +A Bigtable _instance_ is the administrative parent resource that defines the geographic placement, replication strategy, encryption settings and service-level configuration for the tables that will live inside it. Every instance contains one or more clusters, and each cluster in turn contains the nodes that serve user data. Creating or modifying an instance therefore determines where and how your Bigtable data will be stored and replicated. +For further details, refer to the official Google Cloud documentation: https://cloud.google.com/bigtable/docs/instances-clusters-nodes + +**Terrafrom Mappings:** + +- `google_bigtable_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-instance by its "name" +- `LIST`: List all gcp-big-table-admin-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) + +A Bigtable Admin Instance is the parent of one or more Bigtable Admin Clusters. Each cluster resource belongs to exactly one instance, inheriting its replication and localisation settings. When Overmind discovers or updates a gcp-big-table-admin-instance, it follows this relationship to enumerate the gcp-big-table-admin-cluster resources that compose the instance’s underlying serving infrastructure. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md new file mode 100644 index 00000000..a2196b32 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-table-admin-table.md @@ -0,0 +1,30 @@ +--- +title: GCP Big Table Admin Table +sidebar_label: gcp-big-table-admin-table +--- + +Google Cloud Bigtable tables are the primary data containers inside a Bigtable instance. A table holds rows of schemaless, wide-column data that can scale to petabytes while maintaining low-latency access. The Admin Table resource represents the configuration and lifecycle metadata for a table (for example, column families, garbage-collection rules, encryption settings and replication state). For a detailed explanation see the official documentation: https://docs.cloud.google.com/bigtable/docs/reference/admin/rpc. + +**Terrafrom Mappings:** + +- `google_bigtable_table.id` + +## Supported Methods + +- `GET`: Get a gcp-big-table-admin-table by its "instances|tables" +- ~~`LIST`~~ +- `SEARCH`: Search for BigTable tables in an instance. Use the format "instance_name" or "projects/[project_id]/instances/[instance_name]/tables/[table_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-big-table-admin-backup`](/sources/gcp/Types/gcp-big-table-admin-backup) + +A backup is a point-in-time snapshot that is created from a specific table. From a table resource you can enumerate the backups that protect it, or follow a backup back to the source table from which it was taken. + +### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) + +Every table belongs to exactly one Bigtable instance. The instance is the parent container that defines the clusters, replication topology and IAM policy under which the table operates. + +### [`gcp-big-table-admin-table`](/sources/gcp/Types/gcp-big-table-admin-table) + +Tables of the same type within the same project or instance can be cross-referenced for comparison, migration or restore operations (for example, when restoring a backup into a new table). Overmind links tables to other tables so you can trace relationships such as clone targets, restore destinations or sibling tables in the same instance. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md new file mode 100644 index 00000000..59bf138c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-billing-billing-info.md @@ -0,0 +1,20 @@ +--- +title: GCP Cloud Billing Billing Info +sidebar_label: gcp-cloud-billing-billing-info +--- + +`gcp-cloud-billing-billing-info` represents a Google Cloud **ProjectBillingInfo** resource, i.e. the object that records which Cloud Billing Account a particular GCP project is attached to and whether billing is currently enabled. +Knowing which Billing Account is used – and whether charges can actually accrue – is often vital when assessing the financial risk of a new deployment. +Official documentation: https://cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo + +## Supported Methods + +- `GET`: Get a gcp-cloud-billing-billing-info by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +Every ProjectBillingInfo belongs to exactly one Cloud project. Overmind therefore links the `gcp-cloud-billing-billing-info` item to the corresponding `gcp-cloud-resource-manager-project` item, allowing you to trace billing-account associations back to the project that will generate the spend. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md new file mode 100644 index 00000000..0ecba466 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-build-build.md @@ -0,0 +1,31 @@ +--- +title: GCP Cloud Build Build +sidebar_label: gcp-cloud-build-build +--- + +A GCP Cloud Build Build represents a single execution of Google Cloud Build, Google’s fully-managed continuous integration and delivery service. A build encapsulates the series of build steps, source code location, build artefacts, substitutions and metadata that are executed within an isolated builder environment. Each build is uniquely identified by its `name` (formatted as `projects/{projectId}/builds/{buildId}`) and records status, timing information, logs location and any images or other artefacts produced. +For full details see the official documentation: https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds + +## Supported Methods + +- `GET`: Get a gcp-cloud-build-build by its "name" +- `LIST`: List all gcp-cloud-build-build +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +If the build definition contains a step that builds and pushes a Docker image, the resulting image is usually pushed to Artifact Registry. The build therefore produces — and is linked to — one or more `gcp-artifact-registry-docker-image` resources representing the images it published. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Build execution runs under a specific IAM service account (commonly the project-level Cloud Build service account or a custom account) which grants it permissions to fetch source, write logs and push artefacts. The build is thus associated with the `gcp-iam-service-account` used during its execution. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Cloud Build streams build logs to Cloud Logging; organisations often route these logs into dedicated Logging buckets for retention or analysis. When such routing is configured, the build’s log entries will appear in (and therefore relate to) the relevant `gcp-logging-bucket`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Source code for a build can be fetched from a Cloud Storage bucket, and build logs or artefact archives can also be stored in buckets created by Cloud Build (e.g. `gs://{projectId}_cloudbuild`). Consequently, a build may read from or write to one or more `gcp-storage-bucket` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md new file mode 100644 index 00000000..119b1da2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-functions-function.md @@ -0,0 +1,34 @@ +--- +title: GCP Cloud Functions Function +sidebar_label: gcp-cloud-functions-function +--- + +A Google Cloud Functions Function is a serverless, event-driven compute resource that executes user-supplied code in response to HTTP requests or a wide range of Google Cloud events. Because Google Cloud manages the underlying infrastructure, you only specify the code, runtime, memory, timeout, trigger and IAM policy, and you are billed solely for the resources actually consumed while the function is running. For more detail, see Google’s official documentation: https://cloud.google.com/functions/docs/concepts/overview. + +## Supported Methods + +- `GET`: Get a gcp-cloud-functions-function by its "locations|functions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-cloud-functions-function by its "locations" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled, the function’s source code, environment variables or secret volumes are encrypted with a Cloud KMS CryptoKey. Overmind links the function to any CryptoKey that protects its assets so you can assess key rotation or deletion risks. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Function runs as an IAM Service Account. The permissions granted to this account define what the function can read or modify at runtime. Overmind links the function to its execution service account, allowing you to evaluate privilege levels and potential lateral-movement paths. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +A function can be triggered by a Pub/Sub topic or publish messages to one. Overmind records these relationships so you can see which topics will invoke the function and what downstream systems might be affected if the function misbehaves. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +Second-generation Cloud Functions are deployed on Cloud Run. Overmind links the function to the underlying Cloud Run Service, exposing additional configuration such as VPC connectors, ingress settings and revision history that may introduce risk. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Functions often interact with Cloud Storage: source code may be stored in a staging bucket, and functions can be triggered by bucket events (e.g., object creation). Overmind links the function to any associated buckets, helping you identify data-exfiltration risks and unintended public access. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md new file mode 100644 index 00000000..64bb5960 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key-version.md @@ -0,0 +1,30 @@ +--- +title: GCP Cloud KMS Crypto Key Version +sidebar_label: gcp-cloud-kms-crypto-key-version +--- + +A CryptoKeyVersion represents an individual cryptographic key and its associated key material within a Cloud KMS CryptoKey. An ENABLED version can be used for cryptographic operations. Each CryptoKey can have multiple versions, allowing for key rotation. For security reasons, the raw cryptographic key material can never be viewed or exported - it can only be used to encrypt, decrypt, or sign data when an authorized user or application invokes Cloud KMS. For more information, refer to the [official documentation](https://docs.cloud.google.com/kms/docs/key-states). + +**Terraform Mappings:** + +- `google_kms_crypto_key_version.id` + +## Supported Methods + +- `GET`: Get GCP Cloud KMS Crypto Key Version by "location|keyRing|cryptoKey|version" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Cloud KMS Crypto Key Versions by "location|keyRing|cryptoKey" (returns all versions of the specified CryptoKey) + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A CryptoKeyVersion belongs to exactly one parent CryptoKey. The parent CryptoKey contains the version's configuration and purpose. Deleting the parent CryptoKey will delete all of its CryptoKeyVersions, but deleting a CryptoKeyVersion does not affect the parent key. + +### `gcp-cloudkms-importjob` + +If the key material was imported (rather than generated by KMS), the CryptoKeyVersion references the ImportJob that was used for the import operation. The ImportJob contains metadata about how the key material was imported. Deleting the ImportJob after a successful import does not affect the CryptoKeyVersion. + +### `gcp-cloudkms-ekmconnection` + +For CryptoKeyVersions with EXTERNAL_VPC protection level, the version links to an EKM (External Key Manager) connection that manages the external key material. This is used when keys are stored and operated on in an external key management system rather than within Google Cloud KMS. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md new file mode 100644 index 00000000..2813c0fb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-crypto-key.md @@ -0,0 +1,19 @@ +--- +title: GCP Cloud Kms Crypto Key +sidebar_label: gcp-cloud-kms-crypto-key +--- + +A Google Cloud KMS Crypto Key is a logical key resource that performs cryptographic operations such as encryption/de-encryption, signing, and message authentication. Each Crypto Key sits inside a Key Ring, which in turn lives in a specific GCP location (region). The key material for a Crypto Key can be rotated, versioned, and protected by Cloud KMS or by customer-managed hardware security modules, and it is referenced by other Google Cloud services whenever those services need to encrypt or sign data on your behalf. +Official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key + +## Supported Methods + +- `GET`: Get GCP Cloud Kms Crypto Key by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Cloud Kms Crypto Key by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name" + +## Possible Links + +### [`gcp-cloud-kms-key-ring`](/sources/gcp/Types/gcp-cloud-kms-key-ring) + +A Crypto Key is always a child resource of a Key Ring. The `gcp-cloud-kms-key-ring` link allows Overmind to trace from the key to its parent container, establishing the hierarchical relationship needed to understand inheritance of IAM policies, location constraints, and aggregated risk. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md new file mode 100644 index 00000000..5dd85555 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-kms-key-ring.md @@ -0,0 +1,22 @@ +--- +title: GCP Cloud Kms Key Ring +sidebar_label: gcp-cloud-kms-key-ring +--- + +A Cloud KMS Key Ring is a logical container used to group related customer-managed encryption keys within Google Cloud’s Key Management Service (KMS). All Crypto Keys created inside the same Key Ring share the same geographic location, and access control can be applied at the Key Ring level to govern every key it contains. For more information, refer to the [official documentation](https://cloud.google.com/kms/docs/create-key-ring). + +**Terrafrom Mappings:** + +- `google_kms_key_ring.name` + +## Supported Methods + +- `GET`: Get GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name" +- `LIST`: List all GCP Cloud Kms Key Rings across all locations in the project +- `SEARCH`: Search for GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Key Ring is the direct parent of one or more Crypto Keys. Every Crypto Key resource must belong to exactly one Key Ring, so Overmind creates this link to allow navigation from the Key Ring to all the keys it contains (and vice-versa), making it easier to assess the full cryptographic surface associated with a given deployment. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md new file mode 100644 index 00000000..19130df9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-project.md @@ -0,0 +1,20 @@ +--- +title: GCP Cloud Resource Manager Project +sidebar_label: gcp-cloud-resource-manager-project +--- + +A **Google Cloud Platform (GCP) Project** is the fundamental organising entity managed by the Cloud Resource Manager service. Every GCP workload—whether it is a single virtual machine or a complex, multi-region Kubernetes deployment—must reside inside a Project. The Project acts as a logical container for: + +- All GCP resources (compute, storage, networking, databases, etc.) +- Identity and Access Management (IAM) policies +- Billing configuration +- Quotas and limits +- Metadata such as labels and organisation/folder hierarchy + +Because policies and billing are enforced at the Project level, understanding the state of a Project is critical when assessing deployment risk. For detailed information, refer to the official Google documentation: https://cloud.google.com/resource-manager/docs/creating-managing-projects + +## Supported Methods + +- `GET`: Get a gcp-cloud-resource-manager-project by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md new file mode 100644 index 00000000..bcf456a7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-cloud-resource-manager-tag-value.md @@ -0,0 +1,16 @@ +--- +title: GCP Cloud Resource Manager Tag Value +sidebar_label: gcp-cloud-resource-manager-tag-value +--- + +A Tag Value is the value component of Google Cloud’s hierarchical tagging system, which allows you to attach fine-grained, policy-aware metadata to resources. Each Tag Value sits under a Tag Key and, together, the pair forms a tag that can be propagated across projects and folders within an organisation. Tags enable centralised governance, cost allocation, and conditional access control through IAM and Org Policy. For full details, see the official Google Cloud documentation: https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing#tag-values + +**Terrafrom Mappings:** + +- `google_tags_tag_value.name` + +## Supported Methods + +- `GET`: Get a gcp-cloud-resource-manager-tag-value by its "name" +- ~~`LIST`~~ +- `SEARCH`: Search for TagValues by TagKey. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md new file mode 100644 index 00000000..b9b328cd --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Address +sidebar_label: gcp-compute-address +--- + +A GCP Compute Address is a statically-reserved IPv4 or IPv6 address that can be assigned to Compute Engine resources such as virtual machine instances, forwarding rules, VPN gateways and load-balancers. Reserving the address stops it from changing when the attached resource is restarted and allows the address to be re-used on other resources later. Addresses may be global (for external HTTP(S) load-balancers) or regional (for most other use-cases), and internal addresses can be tied to a specific VPC network and sub-network. +For full details see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/addresses + +**Terrafrom Mappings:** + +- `google_compute_address.name` + +## Supported Methods + +- `GET`: Get GCP Compute Address by "gcp-compute-address-name" +- `LIST`: List all GCP Compute Address items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-address`](/sources/gcp/Types/gcp-compute-address) + +A self-link that allows Overmind to relate this address to other instances of the same type (for example, distinguishing between regional and global addresses with identical names). + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Internal (private) addresses are reserved within a specific VPC network, so an address will be linked to the `gcp-compute-network` that owns the IP range from which it is allocated. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +When an internal address is scoped to a particular sub-network, Overmind records this dependency by linking the address to the corresponding `gcp-compute-subnetwork`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md new file mode 100644 index 00000000..7d468e6d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Autoscaler +sidebar_label: gcp-compute-autoscaler +--- + +The Google Cloud Compute Autoscaler is a regional or zonal resource that automatically adds or removes VM instances from a Managed Instance Group in response to workload demand. By scaling on CPU utilisation, load-balancing capacity, Cloud Monitoring metrics, or pre-defined schedules, it helps keep applications responsive while keeping infrastructure spending under control. +For detailed information, consult the official documentation: https://cloud.google.com/compute/docs/autoscaler + +**Terrafrom Mappings:** + +- `google_compute_autoscaler.name` + +## Supported Methods + +- `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" +- `LIST`: List all GCP Compute Autoscaler items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md new file mode 100644 index 00000000..f295c81b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-backend-service.md @@ -0,0 +1,29 @@ +--- +title: GCP Compute Backend Service +sidebar_label: gcp-compute-backend-service +--- + +A GCP Compute Backend Service is the central configuration object that tells a Google Cloud load balancer where and how to send traffic. +It groups one or more back-end targets (for example instance groups, zonal NEG or serverless NEG), specifies the load-balancing scheme (internal or external), session affinity, health checks, protocol, timeout and (optionally) Cloud Armor security policies. +Because almost every Google Cloud load-balancing product routes traffic through a backend service, it is a critical part of any production deployment. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/backendServices + +**Terrafrom Mappings:** + +- `google_compute_backend_service.name` + +## Supported Methods + +- `GET`: Get GCP Compute Backend Service by "gcp-compute-backend-service-name" +- `LIST`: List all GCP Compute Backend Service items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A backend service implicitly belongs to the same VPC network as the back-end resources (instance groups or NEGs) it references. Consequently, the service’s reachability, IP ranges and firewall posture are constrained by that network, so Overmind creates a link to the corresponding `gcp-compute-network` to surface these dependencies. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +If Cloud Armor is enabled, the backend service contains a direct reference to a `securityPolicy`. This link allows Overmind to show how web-application-firewall rules and rate-limiting policies are applied to traffic flowing through the backend service. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md new file mode 100644 index 00000000..6b34dc83 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md @@ -0,0 +1,39 @@ +--- +title: GCP Compute Disk +sidebar_label: gcp-compute-disk +--- + +A GCP Compute Disk is a durable, high-performance block-storage volume that can be attached to one or more Compute Engine virtual machine instances. Persistent disks can act as boot devices or as additional data volumes, are automatically replicated within a zone or region, and can be backed up through snapshots or turned into custom images for rapid redeployment. +For full details see the official Google Cloud documentation: https://cloud.google.com/compute/docs/disks + +**Terrafrom Mappings:** + +- `google_compute_disk.name` + +## Supported Methods + +- `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" +- `LIST`: List all GCP Compute Disk items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +This link appears when one persistent disk has been cloned or recreated from another (for example, using the `--source-disk` flag), allowing Overmind to follow ancestry or duplication chains between disks. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +A custom image may have been created from the current disk, or conversely the disk may have been created from an image. Overmind records this link so you can see which images depend on, or are the origin of, a particular disk. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Virtual machine instances to which the disk is attached (either as a boot disk or as an additional mounted volume) are linked here. This allows you to view the blast-radius of any change to the disk in terms of running workloads. + +### [`gcp-compute-instant-snapshot`](/sources/gcp/Types/gcp-compute-instant-snapshot) + +If an instant snapshot has been taken from the disk, or if the disk has been created from an instant snapshot, Overmind records the relationship via this link. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Standard persistent disk snapshots derived from the disk, or snapshots that were used to create the disk, are linked here, enabling traceability between long-term backups and the live volume. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md new file mode 100644 index 00000000..5877253d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-external-vpn-gateway.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute External Vpn Gateway +sidebar_label: gcp-compute-external-vpn-gateway +--- + +A GCP Compute External VPN Gateway represents a VPN gateway device that resides outside of Google Cloud—typically an on-premises firewall, router or a third-party cloud appliance. In High-Availability VPN (HA VPN) configurations it is used to describe the peer gateway so that Cloud Router and HA VPN tunnels can be created and managed declaratively. Each external gateway resource records the device’s public IP addresses and routing style, allowing Google Cloud to treat the remote endpoint as a first-class object and to validate or reference it from other VPN and network resources. +For full details, see the official Google documentation: https://cloud.google.com/sdk/gcloud/reference/compute/external-vpn-gateways + +**Terrafrom Mappings:** + +- `google_compute_external_vpn_gateway.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-external-vpn-gateway by its "name" +- `LIST`: List all gcp-compute-external-vpn-gateway +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md new file mode 100644 index 00000000..074bee1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md @@ -0,0 +1,26 @@ +--- +title: GCP Compute Firewall +sidebar_label: gcp-compute-firewall +--- + +A GCP Compute Firewall is a set of rules that control incoming and outgoing network traffic to Virtual Machine (VM) instances within a Google Cloud Virtual Private Cloud (VPC) network. Each rule defines whether specific connections (identified by protocol, port, source, destination and direction) are allowed or denied, thereby providing network-level security and segmentation for workloads running on Google Cloud. + +**Terrafrom Mappings:** + +- `google_compute_firewall.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-firewall by its "name" +- `LIST`: List all gcp-compute-firewall +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A firewall rule is always created inside a single VPC network; that network determines the scope within which the rule is evaluated. Overmind therefore links a gcp-compute-firewall to the gcp-compute-network that owns it. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Firewall rules can specify target or source service accounts, allowing traffic to be filtered based on the workload identity running on a VM. Overmind links the firewall rule to any gcp-iam-service-account referenced in its `target_service_accounts` or `source_service_accounts` fields. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md new file mode 100644 index 00000000..9d622a78 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-forwarding-rule.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Forwarding Rule +sidebar_label: gcp-compute-forwarding-rule +--- + +A GCP Compute Forwarding Rule defines how incoming packets are directed within Google Cloud. It associates an IP address, protocol and port range with a specific target—such as a load-balancer target proxy, VPN gateway, or, for certain internal load-balancer variants, a backend service—so that traffic is forwarded correctly. Forwarding rules can be global or regional and, when internal, are bound to a particular VPC network (and optionally a subnetwork) to control the scope of traffic distribution. +For full details see the official documentation: https://docs.cloud.google.com/load-balancing/docs/forwarding-rule-concepts + +**Terrafrom Mappings:** + +- `google_compute_forwarding_rule.name` + +## Supported Methods + +- `GET`: Get GCP Compute Forwarding Rule by "gcp-compute-forwarding-rule-name" +- `LIST`: List all GCP Compute Forwarding Rule items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +For certain internal load balancers (e.g. Internal TCP/UDP Load Balancer), the forwarding rule points directly to a backend service. Overmind records this as a link so that any risk identified on the backend service can be surfaced when assessing the forwarding rule. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +An internal forwarding rule is created inside a specific VPC network; the rule determines how traffic is routed within that network. Linking the forwarding rule to its VPC allows Overmind to trace network-level misconfigurations that could affect traffic flow. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +When a regional internal forwarding rule is restricted to a particular subnetwork, the subnetwork is explicitly referenced. This link lets Overmind evaluate subnet-level controls (such as secondary ranges and IAM bindings) in the context of the forwarding rule’s traffic path. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md new file mode 100644 index 00000000..85788cc6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-address.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Global Address +sidebar_label: gcp-compute-global-address +--- + +A Compute Global Address is a static, reserved IP address that is accessible from any Google Cloud region. It can be either external (public) or internal, and is typically used by globally distributed resources such as HTTP(S) load balancers, Cloud Run services, or global internal load balancers. Once reserved, the address can be bound to forwarding rules or other network endpoints, ensuring that the same IP is advertised worldwide. +For full details, see the official documentation: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#global_addresses + +**Terrafrom Mappings:** + +- `google_compute_global_address.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-global-address by its "name" +- `LIST`: List all gcp-compute-global-address +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Global internal addresses must be created within a specific VPC network, and the `network` attribute on the address points to that VPC. Overmind therefore links a gcp-compute-global-address to the corresponding gcp-compute-network so that you can understand which network context the IP address belongs to and assess any related risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md new file mode 100644 index 00000000..fae8d4a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-global-forwarding-rule.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Global Forwarding Rule +sidebar_label: gcp-compute-global-forwarding-rule +--- + +A Google Compute Engine **Global Forwarding Rule** represents the externally-visible IP address and port(s) that receive traffic for a global load balancer. It defines where packets that enter on a particular protocol/port combination should be sent, pointing them at a target proxy (for HTTP(S), SSL or TCP Proxy load balancers) or target VPN gateway. In the case of Internal Global Load Balancing it may also specify the VPC network and subnetwork that own the virtual IP address. In short, the forwarding rule is the public (or internal) entry-point that maps client traffic to the load balancer’s control plane. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules + +**Terrafrom Mappings:** + +- `google_compute_global_forwarding_rule.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-global-forwarding-rule by its "name" +- `LIST`: List all gcp-compute-global-forwarding-rule +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +A global forwarding rule ultimately delivers traffic to one or more backend services via a chain of resources (target proxy → URL map → backend service). Overmind surfaces this indirect relationship so that you can trace the path from the exposed IP address all the way to the workloads that will handle the request. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When the forwarding rule is used for internal global load balancing, it contains a `network` field that points to the VPC network that owns the virtual IP address. This link allows Overmind to show which network the listener lives in and what other resources share that network. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Similar to the network link, internal forwarding rules may reference a specific `subnetwork`. Overmind records this connection so you can identify the exact IP range and region in which the internal load balancer’s virtual IP is allocated. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md new file mode 100644 index 00000000..61efaaff --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-health-check.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Health Check +sidebar_label: gcp-compute-health-check +--- + +A **GCP Compute Health Check** is a monitored probe that periodically tests the reachability and responsiveness of Google Cloud resources—such as VM instances, managed instance groups, or back-ends behind a load balancer—and reports their health status. These checks allow Google Cloud’s load balancers and auto-healing mechanisms to route traffic only to healthy instances, improving service reliability and availability. You can configure different protocols (HTTP, HTTPS, TCP, SSL, or HTTP/2), thresholds, and time-outs to suit your workload’s requirements. +For full details, see the official documentation: https://cloud.google.com/load-balancing/docs/health-checks + +**Terrafrom Mappings:** + +- `google_compute_health_check.name` + +## Supported Methods + +- `GET`: Get GCP Compute Health Check by "gcp-compute-health-check-name" +- `LIST`: List all GCP Compute Health Check items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md new file mode 100644 index 00000000..095d2e51 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-http-health-check.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Http Health Check +sidebar_label: gcp-compute-http-health-check +--- + +A **Google Cloud Compute HTTP Health Check** is a legacy, regional health-check resource that periodically issues HTTP `GET` requests to a specified path on your instances or load-balanced back-ends. If an instance responds with an acceptable status code (e.g. `200–299`) within the configured timeout for the required number of consecutive probes, it is marked healthy; otherwise, it is marked unhealthy. Load balancers and target pools use this signal to route traffic only to healthy instances, helping to maintain application availability. +Google now recommends the newer, unified _Health Check_ resource for most use-cases, but HTTP Health Checks remain fully supported and are still encountered in many estates. +For full details, see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/httpHealthChecks + +**Terrafrom Mappings:** + +- `google_compute_http_health_check.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-http-health-check by its "name" +- `LIST`: List all gcp-compute-http-health-check +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md new file mode 100644 index 00000000..77040607 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Image +sidebar_label: gcp-compute-image +--- + +A GCP Compute Image represents a bootable disk image in Google Compute Engine. Images capture the contents of a virtual machine’s root volume (operating system, installed packages, configuration files, etc.) and act as the template from which new persistent disks and VM instances are created. Teams use images to standardise the base operating-system layer across their fleet, speed up instance provisioning, and ensure consistency between environments. Modifying or deleting an image can therefore have an immediate impact on every workload that references it, including instance templates and managed instance groups. +Official documentation: https://cloud.google.com/compute/docs/images + +**Terrafrom Mappings:** + +- `google_compute_image.name` + +## Supported Methods + +- `GET`: Get GCP Compute Image by "gcp-compute-image-name" +- `LIST`: List all GCP Compute Image items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md new file mode 100644 index 00000000..bbe86356 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group-manager.md @@ -0,0 +1,34 @@ +--- +title: GCP Compute Instance Group Manager +sidebar_label: gcp-compute-instance-group-manager +--- + +A Google Cloud Compute Instance Group Manager is the control plane object that creates and maintains a Managed Instance Group (MIG). It provisions Virtual Machine (VM) instances from an Instance Template, keeps their number in line with the desired size, and automatically repairs or replaces unhealthy VMs to ensure uniformity across the group. In effect, it is the resource that makes a MIG self-healing and declarative. For full details see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceGroupManagers. + +**Terrafrom Mappings:** + +- `google_compute_instance_group_manager.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance Group Manager by "gcp-compute-instance-group-manager-name" +- `LIST`: List all GCP Compute Instance Group Manager items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-autoscaler`](/sources/gcp/Types/gcp-compute-autoscaler) + +An autoscaler resource can reference a particular Instance Group Manager and adjust the group’s target size according to load metrics. When a link exists, Overmind shows which autoscaler is controlling the scaling behaviour of the MIG managed by this Instance Group Manager. + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +The Instance Group Manager owns and controls a specific managed instance group. This link reveals the underlying Instance Group object that represents the collection of VMs created by the manager. + +### [`gcp-compute-instance-template`](/sources/gcp/Types/gcp-compute-instance-template) + +Every Instance Group Manager specifies an Instance Template that defines the configuration of the VMs it will create (machine type, disks, metadata, etc.). Overmind links the manager to its template so you can trace configuration drift risks back to the source template. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +When using legacy network load balancers, an Instance Group Manager may add its instances to one or more Target Pools. This link identifies the load-balancing back-ends that depend on the instances generated by the manager, helping to surface blast-radius considerations for networking changes. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md new file mode 100644 index 00000000..ee62276d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-group.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Instance Group +sidebar_label: gcp-compute-instance-group +--- + +A Google Cloud Compute Instance Group is a logical collection of virtual machine (VM) instances that you manage as a single entity. Instance groups can be either managed (where the group is tied to an instance template and can perform auto-healing, autoscaling and rolling updates) or unmanaged (a simple grouping of individually created VMs). They are commonly used to distribute traffic across identical instances and to simplify operational tasks such as scaling and updates. +For an in-depth explanation, refer to the official documentation: https://cloud.google.com/compute/docs/instance-groups + +**Terrafrom Mappings:** + +- `google_compute_instance_group.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance Group by "gcp-compute-instance-group-name" +- `LIST`: List all GCP Compute Instance Group items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each VM contained in the instance group is attached to a specific VPC network. Consequently, the instance group inherits a dependency on that GCP Compute Network; changes to the network (e.g., firewall rules, routing) can directly impact the availability or behaviour of all instances in the group. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within its parent VPC network, every instance is placed in a particular subnetwork. Therefore, the instance group is transitively linked to the associated GCP Compute Subnetwork. Subnetwork configuration—such as IP ranges or regional placement—affects how the grouped instances communicate internally and with external resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md new file mode 100644 index 00000000..fd742bd5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance-template.md @@ -0,0 +1,63 @@ +--- +title: GCP Compute Instance Template +sidebar_label: gcp-compute-instance-template +--- + +A Compute Engine instance template is a reusable blueprint that captures almost all of the configuration needed to launch a Virtual Machine (VM) instance in Google Cloud: machine type, boot image, attached disks, network interfaces, metadata, service accounts, shielded-VM options and more. Templates allow you to create individual VM instances consistently or serve as the basis for managed instance groups that can scale automatically. +Official documentation: https://cloud.google.com/compute/docs/instance-templates + +**Terrafrom Mappings:** + +- `google_compute_instance_template.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-instance-template by its "name" +- `LIST`: List all gcp-compute-instance-template +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If customer-managed encryption keys (CMEK) are specified in the template, they reference a Cloud KMS crypto-key that will be used to encrypt the boot or data disks of any VM created from the template. + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +The template can define additional persistent disks to be auto-created and attached, or it can attach existing disks in read-only or read-write mode. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +The boot disk section of the template points to a Compute Engine image that is cloned each time a new VM is launched. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +When a user or an autoscaler instantiates the template, it materialises as one or more Compute Engine instances that inherit every property defined in the template. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every network interface defined in the template must belong to a VPC network, so the template contains links to the relevant network resources. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If the template targets sole-tenant nodes, it can specify a node group affinity so that all created VMs land on a particular node group. + +### [`gcp-compute-reservation`](/sources/gcp/Types/gcp-compute-reservation) + +Templates may be configured to consume capacity from an existing reservation, ensuring launched VMs fit within reserved resources. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +Tags or service-account settings in the template can cause the resulting instances to match Cloud Armor security policies applied at the project or network level. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Instead of an image, the template can build new disks from a snapshot, linking the template to that snapshot resource. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +For networks that are in auto or custom subnet mode, the template points to the exact subnetwork each NIC should join. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +The template includes a service account and its OAuth scopes; the created VMs will assume that service account’s identity and permissions. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md new file mode 100644 index 00000000..ffa3f837 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md @@ -0,0 +1,30 @@ +--- +title: GCP Compute Instance +sidebar_label: gcp-compute-instance +--- + +A GCP Compute Instance is a virtual machine (VM) hosted on Google Cloud’s Compute Engine service. It provides configurable CPU, memory, storage and operating-system options, enabling you to run anything from small test services to large-scale production workloads. Instances can be created from public images or custom images, can have one or more network interfaces, and can attach multiple persistent or ephemeral disks. For full details see the official documentation: https://cloud.google.com/compute/docs/instances + +**Terrafrom Mappings:** + +- `google_compute_instance.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" +- `LIST`: List all GCP Compute Instance items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +A Compute Instance normally boots from and/or mounts one or more persistent disks. Overmind links an instance to every `gcp-compute-disk` that is attached to it so you can assess the impact of changes to those disks on the VM. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every network interface on a Compute Instance is connected to a VPC network. Overmind records this relationship to show how altering a `gcp-compute-network` (for example, changing routing or firewall rules) could affect the instance’s connectivity. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within a VPC network, an interface resides in a specific subnetwork. Overmind links the instance to its `gcp-compute-subnetwork` so you can evaluate risks related to IP ranges, regional availability or subnet-level security policies that might influence the VM. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md new file mode 100644 index 00000000..b26e582d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instant-snapshot.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Instant Snapshot +sidebar_label: gcp-compute-instant-snapshot +--- + +A GCP Compute Instant Snapshot is a point-in-time, crash-consistent copy of a persistent disk that is captured almost immediately, irrespective of the size of the disk. It is stored in the same region as the source disk and is intended for rapid backup, testing, or disaster-recovery scenarios where minimal creation time is essential. Instant snapshots are ephemeral by design (they are automatically deleted after seven days unless converted to a regular snapshot) and incur lower network egress because the data never leaves the region. +For full details, refer to the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/instantSnapshots + +**Terrafrom Mappings:** + +- `google_compute_instant_snapshot.name` + +## Supported Methods + +- `GET`: Get GCP Compute Instant Snapshot by "gcp-compute-instant-snapshot-name" +- `LIST`: List all GCP Compute Instant Snapshot items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +An Instant Snapshot is created from a persistent disk. The snapshot’s `source_disk` field references the original `gcp-compute-disk`, and any restore or promotion operation will require access to that underlying disk or its region. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md new file mode 100644 index 00000000..a8df0527 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-machine-image.md @@ -0,0 +1,35 @@ +--- +title: GCP Compute Machine Image +sidebar_label: gcp-compute-machine-image +--- + +A Google Cloud Compute Engine **Machine Image** is a first-class resource that stores all the information required to recreate one or more identical virtual machine instances, including boot and data disks, instance metadata, machine type, service accounts, and network interface definitions. Machine images make it easy to version-control complete VM templates and roll them out across projects or organisations. +Official documentation: https://cloud.google.com/compute/docs/machine-images + +**Terrafrom Mappings:** + +- `google_compute_machine_image.name` + +## Supported Methods + +- `GET`: Get GCP Compute Machine Image by "gcp-compute-machine-image-name" +- `LIST`: List all GCP Compute Machine Image items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +The machine image contains snapshots of every persistent disk that was attached to the source VM. Linking a machine image to its underlying disks allows Overmind to surface risks such as outdated disk encryption keys or insufficient replication settings. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +A machine image is normally created from, or used to instantiate, Compute Engine instances. Tracking this relationship lets you see which VMs were the origin of the image and which new VMs will inherit its configuration or vulnerabilities. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Network interface settings embedded in the machine image reference specific VPC networks. Connecting the image to those networks helps identify issues like deprecated network configurations that new VMs would inherit. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Each network interface in the machine image also specifies a subnetwork. Mapping this linkage highlights potential problems such as subnet IP exhaustion or mismatched IAM policies that could affect any instance launched from the image. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md new file mode 100644 index 00000000..4377abfc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network-endpoint-group.md @@ -0,0 +1,34 @@ +--- +title: GCP Compute Network Endpoint Group +sidebar_label: gcp-compute-network-endpoint-group +--- + +A Google Cloud Compute Network Endpoint Group (NEG) is a collection of network endpoints—VM NICs, IP and port pairs, or fully-managed serverless targets such as Cloud Run and Cloud Functions—that you treat as a single backend for Google Cloud Load Balancing. By grouping endpoints into a NEG you can precisely steer traffic, perform health-checking, and scale back-end capacity without exposing individual resources. See the official documentation for full details: https://cloud.google.com/load-balancing/docs/negs/. + +**Terrafrom Mappings:** + +- `google_compute_network_endpoint_group.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-network-endpoint-group by its "name" +- `LIST`: List all gcp-compute-network-endpoint-group +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-functions-function`](/sources/gcp/Types/gcp-cloud-functions-function) + +Serverless NEGs can reference a Cloud Functions function as their target, allowing the function to serve as a backend to an HTTP(S) load balancer. Overmind links a NEG to the Cloud Functions function it fronts. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A VM-based or hybrid NEG is created inside a specific VPC network; all its endpoints must belong to that network. Overmind therefore relates the NEG to the corresponding `gcp-compute-network`. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +For regional VM NEGs, each endpoint is an interface on a VM residing in a particular subnetwork. Overmind surfaces this dependency by linking the NEG to each associated `gcp-compute-subnetwork`. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +When a Cloud Run service is exposed through an external HTTP(S) load balancer, Google automatically creates a serverless NEG representing that service. Overmind links the NEG back to its originating `gcp-run-service`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md new file mode 100644 index 00000000..368d7337 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Network +sidebar_label: gcp-compute-network +--- + +A Google Cloud VPC (Virtual Private Cloud) network is a global, logically-isolated network that spans all regions within a Google Cloud project. It defines the IP address space, routing tables, firewall rules and connectivity options (for example, VPN, Cloud Interconnect and peering) for the resources that are attached to it. Each VPC network can contain one or more regional subnetworks that allocate IP addresses to individual resources. +For a full description see the official Google Cloud documentation: https://cloud.google.com/vpc/docs/vpc. + +**Terrafrom Mappings:** + +- `google_compute_network.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-network by its "name" +- `LIST`: List all gcp-compute-network +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +A gcp-compute-network can be linked to another gcp-compute-network when the two are connected using VPC Network Peering. This relationship allows traffic to flow privately between the two VPC networks and is modelled in Overmind as a link between the respective network resources. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Each gcp-compute-network contains one or more gcp-compute-subnetwork resources. Overmind links a network to all of its subnetworks to show the hierarchy and to surface any risks that originate in the subnetwork configuration. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md new file mode 100644 index 00000000..a5493f53 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-group.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Node Group +sidebar_label: gcp-compute-node-group +--- + +A **Google Cloud Compute Node Group** is a logical grouping of one or more sole-tenant nodes – dedicated physical Compute Engine servers that are exclusively reserved for your projects. Node groups let you manage the life-cycle, scheduling policies and placement of these nodes as a single resource. They are typically used when you need hardware isolation for licensing or security reasons, or when you require predictable performance unaffected by noisy neighbours. Each node in the group is created from a Node Template that defines the machine type, CPU platform, labels and maintenance behaviour for the nodes. +Official documentation: https://cloud.google.com/compute/docs/nodes/sole-tenant-nodes + +**Terrafrom Mappings:** + +- `google_compute_node_group.name` +- `google_compute_node_template.name` + +## Supported Methods + +- `GET`: Get GCP Compute Node Group by "gcp-compute-node-group-name" +- `LIST`: List all GCP Compute Node Group items +- `SEARCH`: Search for GCP Compute Node Group by "gcp-compute-node-group-nodeTemplateName" diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md new file mode 100644 index 00000000..11c1ab06 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Project +sidebar_label: gcp-compute-project +--- + +A Google Cloud project is the top-level, logical container for every resource you create in Google Cloud. It stores metadata such as billing configuration, IAM policy, APIs that are enabled, default network settings and quotas, and it provides an isolated namespace for resource names. In the context of Compute Engine, the project determines which VM instances, disks, firewalls and other compute resources can interact, and it is the unit against which most permissions and quotas are enforced. +Official documentation: https://cloud.google.com/resource-manager/docs/creating-managing-projects + +## Supported Methods + +- `GET`: Get a gcp-compute-project by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every service account is created inside a single project and inherits that project’s IAM policy unless overridden. Overmind links a `gcp-compute-project` to the `gcp-iam-service-account` resources it owns so that you can trace how credentials and permissions propagate within the project. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets live inside a project and consume that project’s quotas and billing account. Linking a `gcp-compute-project` to its `gcp-storage-bucket` resources lets you see which data stores are affected by changes to project-wide settings such as IAM roles or organisation policies. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md new file mode 100644 index 00000000..32dd6369 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-public-delegated-prefix.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Public Delegated Prefix +sidebar_label: gcp-compute-public-delegated-prefix +--- + +A Google Cloud Compute Public Delegated Prefix represents a block of publicly-routable IPv4 or IPv6 addresses that Google has reserved and delegated to your project in a given region. Once the prefix exists you can further subdivide it into smaller delegated prefixes or assign individual addresses to resources such as VM instances, forwarding rules, or load balancers. Public Delegated Prefixes enable you to bring your own IP space, ensure predictable address allocation and control how traffic enters your network. +Official documentation: https://docs.cloud.google.com/vpc/docs/create-pdp + +**Terrafrom Mappings:** + +- `google_compute_public_delegated_prefix.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-public-delegated-prefix by its "name" +- `LIST`: List all gcp-compute-public-delegated-prefix +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/publicDelegatedPrefixes/[name] (used for terraform mapping). + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +A Public Delegated Prefix is created within, and therefore belongs to, a specific Cloud Resource Manager project. The project provides billing, IAM, and quota context for the prefix. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +A Public Delegated Prefix can itself be the parent of smaller delegated prefixes; these child prefixes are represented by additional `gcp-compute-public-delegated-prefix` resources that reference the parent block. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md new file mode 100644 index 00000000..4c64e0d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Region Backend Service +sidebar_label: gcp-compute-region-backend-service +--- + +A **GCP Compute Region Backend Service** is a regional load-balancing resource that defines how traffic is distributed to one or more back-end targets (such as Managed Instance Groups or Network Endpoint Groups) that all live in the same Google Cloud region. The service specifies settings such as the load-balancing protocol (HTTP, HTTPS, TCP, SSL etc.), session affinity, connection draining, health checks, fail-over behaviour and (optionally) Cloud Armor security policies. Regional backend services are used by Internal HTTP(S) Load Balancers, Internal TCP/UDP Load Balancers and several other Google Cloud load-balancing products. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/regionBackendServices + +**Terrafrom Mappings:** + +- `google_compute_region_backend_service.name` + +## Supported Methods + +- `GET`: Get GCP Compute Region Backend Service by "gcp-compute-region-backend-service-name" +- `LIST`: List all GCP Compute Region Backend Service items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +A region backend service lists one or more Managed Instance Groups (or unmanaged instance groups) as its back-ends; the load balancer distributes traffic across the VMs contained in these instance groups. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +For internal load balancing, the region backend service is tied to a specific VPC network. All back-ends must reside in subnets that belong to this network and traffic from the forwarding rule is delivered through it. + +### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) + +A backend service can optionally reference a Cloud Armor security policy. When attached, that policy governs and filters incoming requests before they reach the back-end targets. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md new file mode 100644 index 00000000..345b2e62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-commitment.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Region Commitment +sidebar_label: gcp-compute-region-commitment +--- + +A GCP Compute Region Commitment is an agreement in which you purchase a predefined amount of vCPU, memory or GPU capacity in a specific region for a fixed term (one or three years) in return for a reduced hourly price. Commitments are applied automatically to matching usage within the chosen region, helping to lower running costs while guaranteeing a baseline level of capacity. For a detailed explanation of the feature, see the official documentation: https://docs.cloud.google.com/compute/docs/reference/rest/v1/regionCommitments/list. + +**Terrafrom Mappings:** + +- `google_compute_region_commitment.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-region-commitment by its "name" +- `LIST`: List all gcp-compute-region-commitment +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-reservation`](/sources/gcp/Types/gcp-compute-reservation) + +A region commitment can be consumed by one or more compute reservations in the same region. When a reservation launches virtual machine instances, the resources they use are first drawn from any applicable commitments so that the discounted commitment pricing is applied automatically. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md new file mode 100644 index 00000000..446cca00 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Reservation +sidebar_label: gcp-compute-reservation +--- + +A GCP Compute Reservation is a zonal reservation of Compute Engine capacity that guarantees the availability of a specific machine type (and, optionally, attached GPUs, local SSDs, etc.) for when you later launch virtual machine (VM) instances. By pre-allocating vCPU and memory resources, reservations help you avoid capacity-related scheduling failures in busy zones and can be shared across projects inside the same organisation if desired. See the official documentation for full details: https://docs.cloud.google.com/compute/docs/instances/reservations-overview. + +**Terrafrom Mappings:** + +- `google_compute_reservation.name` + +## Supported Methods + +- `GET`: Get GCP Compute Reservation by "gcp-compute-reservation-name" +- `LIST`: List all GCP Compute Reservation items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-region-commitment`](/sources/gcp/Types/gcp-compute-region-commitment) + +Capacity held by a reservation counts against any existing regional commitment in the same region. By linking a reservation to its corresponding `gcp-compute-region-commitment`, you can see whether the reserved resources are already discounted or whether additional commitments may be required. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md new file mode 100644 index 00000000..3cbe6af4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Route +sidebar_label: gcp-compute-route +--- + +A GCP Compute Route is an entry in the routing table of a Google Cloud VPC network that determines how packets are forwarded from its subnets. Each route specifies a destination CIDR block and a next hop (for example, an instance, VPN tunnel, gateway, or peered network). Custom routes can be created to direct traffic through specific appliances, across VPNs, or towards on-premises networks, while system-generated routes provide default Internet and subnet behaviour. +See the official documentation for full details: https://cloud.google.com/vpc/docs/routes + +**Terrafrom Mappings:** + +- `google_compute_route.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-route by its "name" +- `LIST`: List all gcp-compute-route +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +If `next_hop_instance` is set, the route forwards matching traffic to the specified VM instance. Overmind therefore links the route to that Compute Instance, as deleting or modifying the instance will break the route. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every route belongs to exactly one VPC network, referenced in the `network` field. The network’s routing table is the context in which the route operates, so Overmind links the route to its parent network. + +### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) + +When `next_hop_vpn_tunnel` is used, the route sends traffic into a specific VPN tunnel. This dependency is captured by linking the route to the corresponding Compute VPN Tunnel, since changes to the tunnel affect the route’s viability. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md new file mode 100644 index 00000000..107de7f2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Router +sidebar_label: gcp-compute-router +--- + +A Google Cloud **Compute Router** is a regional, fully distributed control-plane resource that learns and exchanges dynamic routes between your Virtual Private Cloud (VPC) network and on-premises or partner networks. It implements the Border Gateway Protocol (BGP) on your behalf, allowing Cloud VPN tunnels and Cloud Interconnect attachments (VLANs) to advertise and receive custom routes without manual updates. Compute Routers are attached to a specific VPC network and region, but they propagate learned routes across the entire VPC through Google’s global backbone. +For a comprehensive overview, refer to the official Google Cloud documentation: https://cloud.google.com/network-connectivity/docs/router/how-to/creating-routers + +**Terrafrom Mappings:** + +- `google_compute_router.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-router by its "name" +- `LIST`: List all gcp-compute-router +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/routers/[router] (used for terraform mapping). + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every Compute Router is created inside a particular VPC network; the router exchanges routes on behalf of that network. Therefore, a gcp-compute-router will always have an owning gcp-compute-network. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Subnets define the IP ranges that the Compute Router ultimately advertises (or learns routes for) within the VPC. Routes learned or propagated by the router directly affect traffic flowing to and from gcp-compute-subnetwork resources. + +### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) + +Compute Routers terminate the BGP sessions used by Cloud VPN (HA VPN) tunnels. Each gcp-compute-vpn-tunnel can be configured to peer with a Compute Router interface, enabling dynamic route exchange between the tunnel and the VPC. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md new file mode 100644 index 00000000..e4013647 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-security-policy.md @@ -0,0 +1,18 @@ +--- +title: GCP Compute Security Policy +sidebar_label: gcp-compute-security-policy +--- + +A GCP Compute Security Policy represents a Cloud Armor security policy. It contains an ordered set of layer-7 filtering rules that allow, deny, or rate-limit traffic directed at a load balancer or backend service. By attaching a security policy you can enforce web-application-firewall (WAF) protections, mitigate DDoS attacks, and define custom match conditions—all without changing your application code. Overmind ingests these resources so you can understand how proposed changes will affect the exposure and resilience of your workloads before you deploy them. + +For full details see the official Google Cloud documentation: https://cloud.google.com/armor/docs/security-policy-concepts + +**Terrafrom Mappings:** + +- `google_compute_security_policy.name` + +## Supported Methods + +- `GET`: Get GCP Compute Security Policy by "gcp-compute-security-policy-name" +- `LIST`: List all GCP Compute Security Policy items +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md new file mode 100644 index 00000000..2dba8fa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md @@ -0,0 +1,27 @@ +--- +title: GCP Compute Snapshot +sidebar_label: gcp-compute-snapshot +--- + +A GCP Compute Snapshot is a point-in-time, incremental backup of a Compute Engine persistent disk. Snapshots allow you to restore data following accidental deletion, corruption, or regional outage, and can also be used to create new disks in the same or a different project/region. Because snapshots are incremental, only the blocks that have changed since the last snapshot are stored, reducing cost and network egress. Snapshots can be scheduled, encrypted with customer-managed keys, and shared across projects through Cloud Storage-backed snapshot storage. +Official documentation: https://cloud.google.com/compute/docs/disks/snapshots + +**Terrafrom Mappings:** + +- `google_compute_snapshot.name` + +## Supported Methods + +- `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" +- `LIST`: List all GCP Compute Snapshot items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +A snapshot is created from a specific persistent disk; the link lets you trace a snapshot back to the disk it protects, or discover all snapshots derived from that disk. + +### [`gcp-compute-instant-snapshot`](/sources/gcp/Types/gcp-compute-instant-snapshot) + +An instant snapshot can later be converted into a standard snapshot, or serve as an intermediary during a snapshot operation. This link shows lineage between an instant snapshot and the resulting persistent snapshot resource. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md new file mode 100644 index 00000000..350c08be --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-certificate.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Ssl Certificate +sidebar_label: gcp-compute-ssl-certificate +--- + +A GCP Compute SSL Certificate is a regional resource that stores the public and private key material required to terminate TLS for Google Cloud load balancers and proxy targets. Once created, the certificate can be attached to target HTTPS proxies (for external HTTP(S) Load Balancing) or target SSL proxies (for SSL Proxy Load Balancing) so that incoming connections can be securely encrypted in transit. Certificate data is provided by the user (self-managed) and can later be rotated or deleted as required. +For full details see the Google Cloud documentation: https://cloud.google.com/compute/docs/reference/rest/v1/sslCertificates + +**Terrafrom Mappings:** + +- `google_compute_ssl_certificate.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-ssl-certificate by its "name" +- `LIST`: List all gcp-compute-ssl-certificate +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md new file mode 100644 index 00000000..020ac502 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-ssl-policy.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Ssl Policy +sidebar_label: gcp-compute-ssl-policy +--- + +Google Cloud SSL policies allow you to define which TLS protocol versions and cipher suites can be used when clients negotiate secure connections with Google Cloud load balancers. By attaching an SSL policy to an HTTPS, SSL, or TCP proxy load balancer, you can enforce modern cryptographic standards, disable deprecated protocols, or maintain compatibility with legacy clients, thereby controlling the security posture of your services. Overmind can surface potential risks—such as the continued availability of weak ciphers—before you deploy. +For more information, see the official Google Cloud documentation: [SSL policies overview](https://cloud.google.com/compute/docs/reference/rest/v1/sslPolicies/get). + +**Terrafrom Mappings:** + +- `google_compute_ssl_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-ssl-policy by its "name" +- `LIST`: List all gcp-compute-ssl-policy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md new file mode 100644 index 00000000..00e54145 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md @@ -0,0 +1,22 @@ +--- +title: GCP Compute Subnetwork +sidebar_label: gcp-compute-subnetwork +--- + +A GCP Compute Subnetwork is a regional segment of a Virtual Private Cloud (VPC) network that defines an IP address range from which resources such as VM instances, GKE nodes, and internal load balancers receive their internal IP addresses. Each subnetwork is bound to a single region, can be configured for automatic or custom IP allocation, and supports features such as Private Google Access and flow logs. For full details see the official Google Cloud documentation: https://cloud.google.com/vpc/docs/subnets + +**Terrafrom Mappings:** + +- `google_compute_subnetwork.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-subnetwork by its "name" +- `LIST`: List all gcp-compute-subnetwork +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every subnetwork is a child resource of a VPC network. The `gcp-compute-network` item represents that parent VPC; a single network can contain multiple subnetworks, while each subnetwork is associated with exactly one network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md new file mode 100644 index 00000000..980032c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-http-proxy.md @@ -0,0 +1,17 @@ +--- +title: GCP Compute Target Http Proxy +sidebar_label: gcp-compute-target-http-proxy +--- + +A Google Cloud Compute Target HTTP Proxy acts as the intermediary between a forwarding rule and your defined URL map. When an incoming request reaches the load balancer, the proxy evaluates the host and path rules in the URL map and then forwards the request to the selected backend service. In essence, it is the control point that translates external client traffic into internal service calls, supporting features such as global anycast IPs, health-checking, and intelligent request routing for high-availability web applications. +For further information, see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/targetHttpProxies + +**Terrafrom Mappings:** + +- `google_compute_target_http_proxy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-http-proxy by its "name" +- `LIST`: List all gcp-compute-target-http-proxy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md new file mode 100644 index 00000000..1f9a673c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-https-proxy.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Target Https Proxy +sidebar_label: gcp-compute-target-https-proxy +--- + +A **Target HTTPS Proxy** is a global Google Cloud resource that terminates incoming HTTPS traffic and forwards the decrypted requests to the appropriate backend service according to a referenced URL map. It is a central component of the External HTTP(S) Load Balancer, holding one or more SSL certificates that are presented to clients during the TLS handshake and optionally enforcing an SSL policy that dictates the allowed protocol versions and cipher suites. +Official documentation: https://docs.cloud.google.com/sdk/gcloud/reference/compute/target-https-proxies + +**Terrafrom Mappings:** + +- `google_compute_target_https_proxy.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-https-proxy by its "name" +- `LIST`: List all gcp-compute-target-https-proxy +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-ssl-certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +The proxy references one or more SSL certificates that are served to clients when they initiate an HTTPS connection. These certificates are specified in the `ssl_certificates` field of the target HTTPS proxy. + +### [`gcp-compute-ssl-policy`](/sources/gcp/Types/gcp-compute-ssl-policy) + +An optional SSL policy can be attached to the proxy to control minimum TLS versions, allowed cipher suites, and other security settings. The policy is linked through the `ssl_policy` attribute. + +### [`gcp-compute-url-map`](/sources/gcp/Types/gcp-compute-url-map) + +Each target HTTPS proxy must reference exactly one URL map, which defines the routing rules that determine which backend service receives each request after SSL/TLS termination. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md new file mode 100644 index 00000000..b7b49d1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-target-pool.md @@ -0,0 +1,30 @@ +--- +title: GCP Compute Target Pool +sidebar_label: gcp-compute-target-pool +--- + +A Compute Target Pool is a regional resource that groups multiple VM instances so they can receive incoming traffic from legacy network TCP load balancers or be used as failover targets for forwarding rules. Target pools can also be linked to one or more Health Checks to determine the availability of their member instances. Official documentation: https://docs.cloud.google.com/load-balancing/docs/target-pools + +**Terrafrom Mappings:** + +- `google_compute_target_pool.id` + +## Supported Methods + +- `GET`: Get a gcp-compute-target-pool by its "name" +- `LIST`: List all gcp-compute-target-pool +- `SEARCH`: Search with full ID: projects/[project]/regions/[region]/targetPools/[name] (used for terraform mapping). + +## Possible Links + +### [`gcp-compute-health-check`](/sources/gcp/Types/gcp-compute-health-check) + +A target pool may reference one or more Health Checks. These checks are executed against each instance in the pool to decide whether the instance should receive traffic. Overmind links a target pool to any health check resources it is configured to use. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Member virtual machines are registered in the target pool. Overmind establishes links from the target pool to every compute instance that is currently part of the pool. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +Target pools can appear as dependencies of other target pools in scenarios such as cross-region failover configurations. Overmind represents these intra-type relationships with links between the relevant target pool resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md new file mode 100644 index 00000000..40025ed3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-url-map.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Url Map +sidebar_label: gcp-compute-url-map +--- + +A Google Cloud Platform (GCP) Compute URL Map is the routing table used by an External or Internal HTTP(S) Load Balancer. It evaluates the host and path of each incoming request and, according to the host rules and path matchers you configure, forwards that request to the appropriate backend service or backend bucket. In other words, the URL map determines “which traffic goes where” once it reaches the load balancer, making it a critical part of any web-facing deployment. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/urlMaps + +**Terrafrom Mappings:** + +- `google_compute_url_map.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-url-map by its "name" +- `LIST`: List all gcp-compute-url-map +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) + +Each URL map references one or more backend services in its path-matcher rules. Overmind therefore creates outbound links from a `gcp-compute-url-map` to every `gcp-compute-backend-service` that might receive traffic, allowing you to trace the full request path and identify downstream risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md new file mode 100644 index 00000000..b51f2540 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-gateway.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Vpn Gateway +sidebar_label: gcp-compute-vpn-gateway +--- + +A Google Cloud Compute VPN Gateway (specifically, the High-Availability VPN Gateway) provides a managed, highly available IPsec VPN endpoint that allows encrypted traffic to flow between a Google Cloud Virtual Private Cloud (VPC) network and an on-premises network or another cloud provider. By deploying a VPN Gateway you can create site-to-site tunnels that automatically scale their throughput and offer automatic fail-over across two interfaces in different zones within the same region. +For full details see the official documentation: https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview + +**Terrafrom Mappings:** + +- `google_compute_ha_vpn_gateway.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-vpn-gateway by its "name" +- `LIST`: List all gcp-compute-vpn-gateway +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +An HA VPN Gateway is created inside, and tightly bound to, a specific VPC network and region. It inherits the network’s subnet routes and advertises them across its VPN tunnels, and all incoming VPN traffic is delivered into that network. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md new file mode 100644 index 00000000..ddd9908b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-vpn-tunnel.md @@ -0,0 +1,31 @@ +--- +title: GCP Compute Vpn Tunnel +sidebar_label: gcp-compute-vpn-tunnel +--- + +A **GCP Compute VPN Tunnel** represents a single IPSec tunnel that is part of a Cloud VPN connection. It contains the parameters needed to establish and maintain the encrypted link – peer IP address, shared secret, IKE version, traffic selectors, and the attachment to either a Classic VPN gateway or an HA VPN gateway. In most deployments two or more tunnels are created for redundancy. +For the full specification see the official Google documentation: https://cloud.google.com/compute/docs/reference/rest/v1/vpnTunnels + +**Terrafrom Mappings:** + +- `google_compute_vpn_tunnel.name` + +## Supported Methods + +- `GET`: Get a gcp-compute-vpn-tunnel by its "name" +- `LIST`: List all gcp-compute-vpn-tunnel +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-external-vpn-gateway`](/sources/gcp/Types/gcp-compute-external-vpn-gateway) + +When the tunnel terminates on equipment outside Google Cloud, the `externalVpnGateway` field is set. This creates a relationship between the VPN tunnel and the corresponding External VPN Gateway resource. + +### [`gcp-compute-router`](/sources/gcp/Types/gcp-compute-router) + +If dynamic routing is enabled (HA VPN or dynamic Classic VPN), the tunnel is attached to a Cloud Router, which advertises and learns routes via BGP. The `router` field therefore links the VPN tunnel to a specific Cloud Router. + +### [`gcp-compute-vpn-gateway`](/sources/gcp/Types/gcp-compute-vpn-gateway) + +Every tunnel belongs to a Google-managed VPN gateway (`targetVpnGateway` for Classic VPN or `vpnGateway` for HA VPN). This link captures that parent-child relationship, allowing Overmind to evaluate the impact of gateway changes on its tunnels. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md new file mode 100644 index 00000000..eed12aa9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md @@ -0,0 +1,47 @@ +--- +title: GCP Container Cluster +sidebar_label: gcp-container-cluster +--- + +Google Kubernetes Engine (GKE) Container Clusters provide managed Kubernetes control-planes and node infrastructure on Google Cloud Platform. A cluster groups together one or more node pools running containerised workloads, and exposes both the Kubernetes API server and optional add-ons such as Cloud Monitoring, Cloud Logging, Workload Identity and Binary Authorisation. +For a full description of the service see the official Google documentation: https://cloud.google.com/kubernetes-engine/docs + +**Terrafrom Mappings:** + +- `google_container_cluster.id` + +## Supported Methods + +- `GET`: Get a gcp-container-cluster by its "locations|clusters" +- ~~`LIST`~~ +- `SEARCH`: Search for GKE clusters in a location. Use the format "location" or the full resource name supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A cluster can be configured to encrypt Kubernetes secrets and etcd data at rest using a customer-managed Cloud KMS crypto key. When customer-managed encryption is enabled, the cluster stores the resource ID of the key that protects its control-plane data, creating a link between the cluster and the KMS crypto key. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every GKE cluster is deployed into a VPC network. All control-plane and node traffic flows inside this network, and the cluster stores the name of the network it belongs to, creating a relationship with the corresponding gcp-compute-network resource. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If a node pool is configured to run on sole-tenant nodes, GKE provisions or attaches to Compute Engine node groups for placement. The cluster will therefore reference any node groups used by its node pools. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within the chosen VPC, a cluster is attached to one or more subnetworks to allocate IP ranges for nodes, pods and services. The subnetwork resource(s) appear in the cluster’s configuration and are linked to the cluster. + +### [`gcp-container-node-pool`](/sources/gcp/Types/gcp-container-node-pool) + +A cluster is composed of one or more node pools that provide the actual worker nodes. Each node pool references its parent cluster, and the cluster maintains a list of all associated node pools. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +GKE uses service accounts for both the control-plane (Google-managed) and the nodes (user-specified or default). Additionally, Workload Identity maps Kubernetes service accounts to IAM service accounts. Any service account configured for node pools, Workload Identity or authorised networks will be linked to the cluster. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Audit logs and event streams originating from a GKE cluster can be exported via Logging sinks to Pub/Sub topics for downstream processing. When such a sink targets a Pub/Sub topic, the cluster indirectly references that topic, creating a link captured by Overmind. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md new file mode 100644 index 00000000..dff4c40d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-node-pool.md @@ -0,0 +1,35 @@ +--- +title: GCP Container Node Pool +sidebar_label: gcp-container-node-pool +--- + +A Google Cloud Platform (GCP) Container Node Pool is a logical grouping of worker nodes within a Google Kubernetes Engine (GKE) cluster. All nodes in a pool share the same configuration (machine type, disk size, metadata, labels, etc.) and are managed as a single unit for operations such as upgrades, autoscaling and maintenance. Node pools allow you to mix and match node types inside a single cluster, enabling workload-specific optimisation, cost control and security hardening. +Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools + +**Terrafrom Mappings:** + +- `google_container_node_pool.id` + +## Supported Methods + +- `GET`: Get a gcp-container-node-pool by its "locations|clusters|nodePools" +- ~~`LIST`~~ +- `SEARCH`: Search GKE Node Pools within a cluster. Use "[location]|[cluster]" or the full resource name supported by Terraform mappings: "[project]/[location]/[cluster]/[node_pool_name]" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A node pool can be configured to use a Cloud KMS CryptoKey for at-rest encryption of node boot disks or customer-managed encryption keys (CMEK) for GKE secrets. Overmind links the node pool to the KMS key that protects its data, allowing you to trace encryption dependencies. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +When a node pool is created on sole-tenant nodes, GKE provisions the underlying Compute Engine Node Group that hosts those VMs. Linking highlights which Node Group provides the physical tenancy for the pool’s nodes. + +### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) + +Every node pool belongs to exactly one GKE cluster. This parent-child relationship is surfaced so you can quickly navigate from a pool to its cluster and understand cluster-level configuration and risk. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Each VM in a node pool runs as an IAM service account (often the “default” compute service account or a custom node service account). Overmind links the pool to that service account to expose permissions granted to workloads running on the nodes. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md new file mode 100644 index 00000000..dccaedcb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md @@ -0,0 +1,31 @@ +--- +title: GCP Dataform Repository +sidebar_label: gcp-dataform-repository +--- + +A GCP Dataform Repository is the top-level, version-controlled container that stores all the SQL workflow code, configuration files and commit history used by Dataform in Google Cloud. It functions much like a Git repository, allowing data teams to develop, test and deploy BigQuery pipelines through branches, pull requests and releases. Repositories live under a specific project and location and can be connected to Cloud Source Repositories or external Git providers. +Official documentation: https://cloud.google.com/dataform/docs/repositories + +**Terrafrom Mappings:** + +- `google_dataform_repository.id` + +## Supported Methods + +- `GET`: Get a gcp-dataform-repository by its "locations|repositories" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataform repositories in a location. Use the format "location" or "projects/[project_id]/locations/[location]/repositories/[repository_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the repository, it contains a reference to the Cloud KMS crypto key that encrypts its metadata. Overmind follows this link to verify key existence, rotation policy and wider blast radius. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Dataform executes queries and workflow steps using a service account specified in the repository or workspace settings. Linking to the IAM service account lets Overmind trace which identities can act on behalf of the repository and assess permission risks. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +A repository may reference secrets (such as connection strings or API tokens) stored in Secret Manager via environment variables or workflow configurations. Overmind links to these secrets to ensure they exist, are properly protected and are not about to be rotated or deleted. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md new file mode 100644 index 00000000..af6be780 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-aspect-type.md @@ -0,0 +1,17 @@ +--- +title: GCP Dataplex Aspect Type +sidebar_label: gcp-dataplex-aspect-type +--- + +A Google Cloud Dataplex Aspect Type is a reusable template that describes the structure and semantics of a particular piece of metadata—an _aspect_—that can later be attached to Dataplex assets, entries, or partitions. By defining aspect types centrally, an organisation can guarantee that the same metadata schema (for example, “Personally Identifiable Information classification” or “Data-quality score”) is applied consistently across lakes, zones, and assets, thereby strengthening governance, lineage, and discovery capabilities. +For further details, see the official Dataplex REST reference: https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.aspectTypes + +**Terrafrom Mappings:** + +- `google_dataplex_aspect_type.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-aspect-type by its "locations|aspectTypes" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex aspect types in a location. Use the format "location" or "projects/[project_id]/locations/[location]/aspectTypes/[aspect_type_id]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md new file mode 100644 index 00000000..a9caa00a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-data-scan.md @@ -0,0 +1,22 @@ +--- +title: GCP Dataplex Data Scan +sidebar_label: gcp-dataplex-data-scan +--- + +A Dataplex Data Scan is a managed resource that schedules and executes automated profiling or data-quality checks over data held in Google Cloud Platform (GCP) storage systems such as Cloud Storage and BigQuery. The scan stores its configuration, execution history and results, allowing teams to understand the structure, completeness and validity of their datasets before those datasets are used downstream. Full details can be found in the official Google Cloud documentation: https://docs.cloud.google.com/dataplex/docs/use-data-profiling + +**Terrafrom Mappings:** + +- `google_dataplex_datascan.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-data-scan by its "locations|dataScans" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex data scans in a location. Use the location name e.g., 'us-central1' or the format "projects/[project_id]/locations/[location]/dataScans/[data_scan_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +A Dataplex Data Scan can target objects stored in a Cloud Storage bucket for profiling or quality validation. Therefore, Overmind links the scan resource to the bucket that contains the underlying data being analysed, enabling a complete view of the data-quality pipeline and its dependencies. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md new file mode 100644 index 00000000..04d5e81f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataplex-entry-group.md @@ -0,0 +1,17 @@ +--- +title: GCP Dataplex Entry Group +sidebar_label: gcp-dataplex-entry-group +--- + +A Dataplex Entry Group is a logical container in Google Cloud that lives in the Data Catalog service and is used by Dataplex to organise metadata about datasets, tables and other data assets. By grouping related Data Catalog entries together, Entry Groups enable consistent discovery, governance and lineage tracking across lakes, zones and projects. Each Entry Group is created in a specific project and location and can be referenced by Dataplex jobs, policies and fine-grained IAM settings. +For full details see Google’s REST reference: https://cloud.google.com/data-catalog/docs/reference/rest/v1/projects.locations.entryGroups + +**Terrafrom Mappings:** + +- `google_dataplex_entry_group.id` + +## Supported Methods + +- `GET`: Get a gcp-dataplex-entry-group by its "locations|entryGroups" +- ~~`LIST`~~ +- `SEARCH`: Search for Dataplex entry groups in a location. Use the format "location" or "projects/[project_id]/locations/[location]/entryGroups/[entry_group_id]" which is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md new file mode 100644 index 00000000..9532af2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-autoscaling-policy.md @@ -0,0 +1,16 @@ +--- +title: GCP Dataproc Autoscaling Policy +sidebar_label: gcp-dataproc-autoscaling-policy +--- + +A Google Cloud Dataproc Autoscaling Policy defines how a Dataproc cluster should automatically grow or shrink its worker and secondary-worker (pre-emptible) node groups in response to load. Policies specify minimum and maximum instance counts, cooldown periods, and scaling rules based on YARN memory or CPU utilisation, allowing clusters to meet workload demand while controlling cost. Once created at the project or region level, a policy can be referenced by any Dataproc cluster in that location. For more detail see the official documentation: https://cloud.google.com/dataproc/docs/concepts/configuring-clusters/autoscaling. + +**Terrafrom Mappings:** + +- `google_dataproc_autoscaling_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-dataproc-autoscaling-policy by its "name" +- `LIST`: List all gcp-dataproc-autoscaling-policy +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md new file mode 100644 index 00000000..23805167 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md @@ -0,0 +1,54 @@ +--- +title: GCP Dataproc Cluster +sidebar_label: gcp-dataproc-cluster +--- + +A Google Cloud Dataproc Cluster is a managed cluster of Compute Engine virtual machines that runs open-source data-processing frameworks such as Apache Spark, Apache Hadoop, Presto and Trino. Dataproc handles the provisioning, configuration and ongoing management of the cluster, allowing you to submit jobs or create ephemeral clusters on demand while paying only for the compute you use. For full feature details see the official documentation: https://docs.cloud.google.com/dataproc/docs/concepts/overview. + +**Terrafrom Mappings:** + +- `google_dataproc_cluster.name` + +## Supported Methods + +- `GET`: Get a gcp-dataproc-cluster by its "name" +- `LIST`: List all gcp-dataproc-cluster +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Dataproc cluster can be configured to use a customer-managed encryption key (CMEK) from Cloud KMS to encrypt the persistent disks attached to its nodes as well as the cluster’s Cloud Storage staging bucket. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +Each Dataproc cluster is built from a specific Dataproc image (e.g., `2.1-debian11`). The image determines the operating system and the versions of Hadoop, Spark and other components installed on the VM instances. + +### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) + +Behind the scenes Dataproc creates managed instance groups for the primary, secondary and optional pre-emptible worker node pools. These MIGs handle instance creation, health-checking and replacement. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +The cluster’s VMs are attached to a specific VPC network, determining their routability and ability to reach other Google Cloud services or on-premises systems. + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +If you run Dataproc on sole-tenant nodes, the cluster associates each VM with a Compute Node Group to guarantee dedicated physical hardware. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Within the chosen VPC, the cluster can be pinned to a particular subnetwork to control IP address ranges, firewall rules and routing. + +### [`gcp-dataproc-autoscaling-policy`](/sources/gcp/Types/gcp-dataproc-autoscaling-policy) + +Clusters may reference an Autoscaling Policy that automatically adds or removes worker nodes based on YARN or Spark metrics, optimising performance and cost. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Dataproc node runs under a Compute Engine service account. This account’s IAM roles determine the cluster’s permission to read/write Cloud Storage, publish metrics, access BigQuery, etc. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Dataproc uses Cloud Storage buckets for staging job files, storing cluster logs and optionally as a default HDFS replacement via the `gcs://` connector. The cluster therefore references one or more buckets during its lifecycle. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md new file mode 100644 index 00000000..7f296db9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dns-managed-zone.md @@ -0,0 +1,27 @@ +--- +title: GCP Dns Managed Zone +sidebar_label: gcp-dns-managed-zone +--- + +A Cloud DNS Managed Zone is a logical container within Google Cloud that holds the DNS records for a particular namespace (for example, `example.com`). Each managed zone is served by a set of authoritative name servers and can be either public (resolvable on the public internet) or private (resolvable only from selected VPC networks). Managed zones let you create, update, and delete DNS resource-record sets using the Cloud DNS API, gcloud CLI, or Terraform. +For full details see Google’s documentation: https://docs.cloud.google.com/dns/docs/zones + +**Terrafrom Mappings:** + +- `google_dns_managed_zone.name` + +## Supported Methods + +- `GET`: Get a gcp-dns-managed-zone by its "name" +- `LIST`: List all gcp-dns-managed-zone +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Private managed zones can be attached to one or more VPC networks. When such a link exists, DNS queries originating from resources inside the referenced `gcp-compute-network` are resolved using the records defined in the managed zone. Overmind surfaces this relationship to show which networks will be affected by changes to the zone’s records or visibility settings. + +### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) + +Google Kubernetes Engine may automatically create or rely on Cloud DNS managed zones for features such as service discovery, Cloud DNS-based Pod/Service FQDN resolution, or workload identity federation. Linking a `gcp-dns-managed-zone` to a `gcp-container-cluster` allows Overmind to highlight how DNS adjustments could impact cluster-internal name resolution or ingress behaviour for that cluster. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md new file mode 100644 index 00000000..d3797d1d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-essential-contacts-contact.md @@ -0,0 +1,17 @@ +--- +title: GCP Essential Contacts Contact +sidebar_label: gcp-essential-contacts-contact +--- + +Google Cloud’s Essential Contacts service allows an organisation to register one or more e-mail addresses that will receive important operational and security notifications about a project, folder, or organisation. A “contact” resource represents a single recipient and records the e-mail address, preferred language and notification categories that the person should receive. More than one contact can be added so that the right teams are informed whenever Google issues mandatory or time-sensitive messages. +For a full description of the resource and its fields, refer to the official documentation: https://cloud.google.com/resource-manager/docs/managing-notification-contacts + +**Terrafrom Mappings:** + +- `google_essential_contacts_contact.id` + +## Supported Methods + +- `GET`: Get a gcp-essential-contacts-contact by its "name" +- `LIST`: List all gcp-essential-contacts-contact +- `SEARCH`: Search for contacts by their ID in the form of "projects/[project_id]/contacts/[contact_id]". diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md new file mode 100644 index 00000000..f9a224ba --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md @@ -0,0 +1,27 @@ +--- +title: GCP File Instance +sidebar_label: gcp-file-instance +--- + +A GCP Filestore instance is a fully-managed network file system that provides high-performance, scalable Network File System (NFS) shares to Google Cloud workloads. It allows you to mount POSIX-compliant file storage from Compute Engine VMs, GKE clusters and other services without having to provision or manage the underlying storage infrastructure yourself. Each instance resides in a specific region and VPC network, exposes one or more IP addresses, and can be encrypted with either Google-managed or customer-managed keys. +For full details, refer to the official documentation: https://cloud.google.com/filestore/docs. + +**Terrafrom Mappings:** + +- `google_filestore_instance.id` + +## Supported Methods + +- `GET`: Get a gcp-file-instance by its "locations|instances" +- ~~`LIST`~~ +- `SEARCH`: Search for Filestore instances in a location. Use the location string or the full resource name supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Filestore instance can be configured to use a customer-managed encryption key (CMEK) stored in Cloud KMS. When CMEK is enabled, the instance has a direct dependency on the specified `gcp-cloud-kms-crypto-key`, and loss or revocation of that key will render the file share inaccessible. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Every Filestore instance is attached to a single VPC network and is reachable through an internal IP address range that you specify. This link represents the network in which the instance’s NFS endpoints are published and through which client traffic must flow. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md new file mode 100644 index 00000000..0867be52 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md @@ -0,0 +1,13 @@ +--- +title: GCP Iam Role +sidebar_label: gcp-iam-role +--- + +Google Cloud Identity and Access Management (IAM) roles are collections of granular permissions that you grant to principals—such as users, groups or service accounts—so they can interact with Google Cloud resources. Roles come in three varieties (basic, predefined and custom) and are the chief mechanism for enforcing the principle of least privilege across your estate. Overmind represents each IAM role as an individual resource, enabling you to surface the blast-radius of creating, modifying or deleting a role before you commit the change. +For further details, refer to the official Google Cloud documentation: https://cloud.google.com/iam/docs/understanding-roles + +## Supported Methods + +- `GET`: Get a gcp-iam-role by its "name" +- `LIST`: List all gcp-iam-role +- ~~`SEARCH`~~ diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md new file mode 100644 index 00000000..79313f2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account-key.md @@ -0,0 +1,23 @@ +--- +title: GCP Iam Service Account Key +sidebar_label: gcp-iam-service-account-key +--- + +A GCP IAM Service Account Key is a cryptographic key-pair that allows code or users outside Google Cloud to authenticate as a specific service account. Each key consists of a public key stored by Google and a private key material that can be downloaded once and should be stored securely. Because anyone in possession of the private key can act with all the permissions of the associated service account, these keys are highly sensitive and should be rotated or disabled when no longer required. +For full details, see the official documentation: https://cloud.google.com/iam/docs/creating-managing-service-account-keys + +**Terrafrom Mappings:** + +- `google_service_account_key.id` + +## Supported Methods + +- `GET`: Get GCP Iam Service Account Key by "gcp-iam-service-account-email or unique_id|gcp-iam-service-account-key-name" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Iam Service Account Key by "gcp-iam-service-account-email or unique_id" + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Service Account Key is attached to exactly one Service Account; this link allows you to trace which principal will be able to use the key and to evaluate the permissions that could be exercised if the key were compromised. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md new file mode 100644 index 00000000..92c34a8d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-service-account.md @@ -0,0 +1,27 @@ +--- +title: GCP Iam Service Account +sidebar_label: gcp-iam-service-account +--- + +A GCP IAM Service Account is a non-human identity that represents a workload such as a VM, Cloud Function or CI/CD pipeline. It can be granted IAM roles and used to obtain access tokens for calling Google Cloud APIs, allowing software to authenticate securely without relying on end-user credentials. Each service account lives inside a single project (or, less commonly, an organisation or folder) and can be equipped with one or more private keys for external use. See the official documentation for further details: [Google Cloud – Service Accounts](https://cloud.google.com/iam/docs/service-accounts). + +**Terrafrom Mappings:** + +- `google_service_account.email` +- `google_service_account.unique_id` + +## Supported Methods + +- `GET`: Get GCP Iam Service Account by "gcp-iam-service-account-email or unique_id" +- `LIST`: List all GCP Iam Service Account items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +Every service account is created within exactly one Cloud Resource Manager project. Overmind links the service account to its parent project so that you can trace inheritance of IAM policies and understand the blast radius of changes to either resource. + +### [`gcp-iam-service-account-key`](/sources/gcp/Types/gcp-iam-service-account-key) + +A service account may have multiple keys (managed by Google or user-managed). These keys allow external systems to impersonate the service account. Overmind enumerates and links all keys associated with a service account, helping you identify stale or over-privileged credentials. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md new file mode 100644 index 00000000..9c101db9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md @@ -0,0 +1,23 @@ +--- +title: GCP Logging Bucket +sidebar_label: gcp-logging-bucket +--- + +A GCP Logging Bucket is a regional or multi-regional storage container within Cloud Logging that holds log entries for long-term retention, analysis and export. Buckets allow you to isolate logs by project, folder or organisation, set individual retention periods, and apply fine-grained IAM policies. They can be configured for customer-managed encryption and for log routing between projects or across the organisation. +For full details see the Google Cloud documentation: https://cloud.google.com/logging/docs/storage#buckets + +## Supported Methods + +- `GET`: Get a gcp-logging-bucket by its "locations|buckets" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-bucket by its "locations" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A logging bucket can be encrypted with a customer-managed encryption key (CMEK). When CMEK is enabled, the bucket stores the full resource name of the Cloud KMS crypto key that protects the log data, creating a dependency on that `gcp-cloud-kms-crypto-key` resource. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Writing, reading and routing logs rely on service accounts such as the Log Router and Google-managed writer accounts. These accounts appear in the bucket’s IAM policy and permissions, so the bucket is linked to the corresponding `gcp-iam-service-account` resources. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md new file mode 100644 index 00000000..e5e10117 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md @@ -0,0 +1,26 @@ +--- +title: GCP Logging Link +sidebar_label: gcp-logging-link +--- + +A GCP Logging Link is a Cloud Logging resource that connects a Log Bucket to an external analytics destination, currently a BigQuery dataset. Once the link is created, every entry that is written to the bucket is replicated to the linked BigQuery dataset in near real time, letting you query your logs with standard BigQuery SQL without having to configure or manage a separate Log Router sink. +Logging Links are created under the path +`projects/{project}/locations/{location}/buckets/{bucket}/links/{link}` and inherit the life-cycle and IAM policies of their parent bucket. They are regional, can optionally back-fill historical log data at creation time, and can be updated or deleted independently of the bucket or dataset. + +For more information see the official documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.locations.buckets.links + +## Supported Methods + +- `GET`: Get a gcp-logging-link by its "locations|buckets|links" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-link by its "locations|buckets" + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +A Logging Link points to the BigQuery dataset that serves as the analytics destination. The linked `gcp-big-query-dataset` receives a continuous copy of the logs contained in the parent bucket. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Every Logging Link is defined inside a specific `gcp-logging-bucket`. The bucket is the source of the log entries that are streamed to the linked BigQuery dataset. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md new file mode 100644 index 00000000..0502885d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-saved-query.md @@ -0,0 +1,13 @@ +--- +title: GCP Logging Saved Query +sidebar_label: gcp-logging-saved-query +--- + +A GCP Logging Saved Query is a reusable, shareable filter definition for Google Cloud Logging (Logs Explorer). It stores the log filter expression, as well as optional display preferences and metadata, so that complex queries can be rerun or shared without having to rewrite the filter each time. Saved queries can be created at the project, folder, billing-account or organisation level and are particularly useful for operational run-books, incident response and dashboards. +Official documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.locations.savedQueries + +## Supported Methods + +- `GET`: Get a gcp-logging-saved-query by its "locations|savedQueries" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-saved-query by its "locations" diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md new file mode 100644 index 00000000..b1e20cc0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md @@ -0,0 +1,31 @@ +--- +title: GCP Logging Sink +sidebar_label: gcp-logging-sink +--- + +A GCP Logging Sink is an export rule within Google Cloud Logging that continuously routes selected log entries to a destination such as BigQuery, Cloud Storage, Pub/Sub or another Logging bucket. Sinks allow you to retain logs for longer, perform analytics, or trigger near-real-time workflows outside Cloud Logging. Each sink is defined by three core elements: a filter that selects which log entries to export, a destination, and an IAM service account that is granted permission to write to that destination. +For full details see the official documentation: https://cloud.google.com/logging/docs/export/configure_export + +## Supported Methods + +- `GET`: Get GCP Logging Sink by "gcp-logging-sink-name" +- `LIST`: List all GCP Logging Sink items +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +If the sink’s destination is set to a BigQuery dataset, Overmind will create a link from the sink to that `gcp-big-query-dataset` resource because the sink writes log rows directly into the dataset’s `_TABLE_SUFFIX` sharded tables. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +A sink can either originate from a Logging bucket (when the sink is scoped to that bucket) or target a Logging bucket in another project or billing account. Overmind therefore links the sink to the relevant `gcp-logging-bucket` to show where logs are pulled from or pushed to. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +When a sink exports logs to Pub/Sub, it references a specific topic. Overmind links the sink to the corresponding `gcp-pub-sub-topic` so that users can trace event-driven pipelines or alerting mechanisms that rely on those published log messages. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +If the sink is configured to deliver logs to Cloud Storage, the destination bucket appears as a linked `gcp-storage-bucket`. This highlights where log files are archived and the IAM relationship required for the sink’s writer identity to upload objects. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md new file mode 100644 index 00000000..3521f688 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-alert-policy.md @@ -0,0 +1,23 @@ +--- +title: GCP Monitoring Alert Policy +sidebar_label: gcp-monitoring-alert-policy +--- + +A GCP Monitoring Alert Policy defines the conditions under which Google Cloud Monitoring should raise an alert and the actions that should be taken when those conditions are met. It lets you specify metrics to watch, threshold values, duration, notification channels, documentation for responders, and incident autoclose behaviour. Alert policies are a core part of Google Cloud’s observability suite, helping operations teams detect and respond to issues before they affect end-users. +For full details, see the official documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.alertPolicies#AlertPolicy + +**Terrafrom Mappings:** + +- `google_monitoring_alert_policy.id` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-alert-policy by its "name" +- `LIST`: List all gcp-monitoring-alert-policy +- `SEARCH`: Search by full resource name: projects/[project]/alertPolicies/[alert_policy_id] (used for terraform mapping). + +## Possible Links + +### [`gcp-monitoring-notification-channel`](/sources/gcp/Types/gcp-monitoring-notification-channel) + +An alert policy can reference one or more Notification Channels. These channels determine where alerts are delivered (e-mail, SMS, Pub/Sub, PagerDuty, etc.). Overmind therefore creates a link from each gcp-monitoring-alert-policy to the gcp-monitoring-notification-channel resources it targets, allowing you to understand which channels will be invoked when a policy fires. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md new file mode 100644 index 00000000..2e1aaa0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-custom-dashboard.md @@ -0,0 +1,17 @@ +--- +title: GCP Monitoring Custom Dashboard +sidebar_label: gcp-monitoring-custom-dashboard +--- + +A Google Cloud Monitoring Custom Dashboard is a user-defined workspace in which you can visualise metrics, logs-based metrics and alerting information collected from your Google Cloud resources and external services. By assembling charts, heatmaps, and scorecards that matter to your organisation, a custom dashboard lets you observe the real-time health and historical behaviour of your workloads, share operational insights with your team, and troubleshoot incidents more quickly. Dashboards are created and managed through the Cloud Monitoring API, the Google Cloud console, or declaratively via Terraform. +Official documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v1/projects.dashboards#Dashboard + +**Terrafrom Mappings:** + +- `google_monitoring_dashboard.id` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-custom-dashboard by its "name" +- `LIST`: List all gcp-monitoring-custom-dashboard +- `SEARCH`: Search for custom dashboards by their ID in the form of "projects/[project_id]/dashboards/[dashboard_id]". This is supported for terraform mappings. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md new file mode 100644 index 00000000..f29eba8b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-monitoring-notification-channel.md @@ -0,0 +1,17 @@ +--- +title: GCP Monitoring Notification Channel +sidebar_label: gcp-monitoring-notification-channel +--- + +A Google Cloud Monitoring Notification Channel is a resource that specifies where and how alerting notifications are delivered from Cloud Monitoring. Channels can point to many target types – e-mail, SMS, mobile push, Slack, PagerDuty, Pub/Sub, webhooks and more – and each channel stores the parameters (addresses, tokens, templates, etc.) required to reach that destination. Alerting policies reference one or more notification channels so that, when a policy is triggered, Cloud Monitoring automatically sends the message to the configured recipients. +For a full description of the resource and its schema, see the official Google Cloud documentation: https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.notificationChannels. + +**Terrafrom Mappings:** + +- `google_monitoring_notification_channel.name` + +## Supported Methods + +- `GET`: Get a gcp-monitoring-notification-channel by its "name" +- `LIST`: List all gcp-monitoring-notification-channel +- `SEARCH`: Search by full resource name: projects/[project]/notificationChannels/[notificationChannel] (used for terraform mapping). diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md new file mode 100644 index 00000000..c9c6788c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md @@ -0,0 +1,17 @@ +--- +title: GCP Orgpolicy Policy +sidebar_label: gcp-orgpolicy-policy +--- + +An Organisation Policy in Google Cloud Platform (GCP) lets administrators enforce or relax specific constraints on GCP resources across the organisation, folder, or project hierarchy. Each policy represents the chosen configuration for a single constraint (for example, restricting service account key creation or limiting the set of permitted VM regions) on a single resource node. By querying an Org Policy, Overmind can reveal whether pending changes will violate existing security or governance rules before deployment. +Official documentation: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints + +**Terrafrom Mappings:** + +- `google_org_policy_policy.name` + +## Supported Methods + +- `GET`: Get a gcp-orgpolicy-policy by its "name" +- `LIST`: List all gcp-orgpolicy-policy +- `SEARCH`: Search with the full policy name: projects/[project]/policies/[constraint] (used for terraform mapping). diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md new file mode 100644 index 00000000..16c2256e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-subscription.md @@ -0,0 +1,35 @@ +--- +title: GCP Pub Sub Subscription +sidebar_label: gcp-pub-sub-subscription +--- + +A Google Cloud Pub/Sub subscription represents a named endpoint that receives messages that are published to a specific Pub/Sub topic. Subscribers pull (or are pushed) messages from the subscription, acknowledge them, and thereby remove them from the backlog. Subscriptions can be configured for pull or push delivery, control message-retention, enforce acknowledgement deadlines, use filtering, dead-letter topics or BigQuery/Cloud Storage sinks. +For full details see the official documentation: https://cloud.google.com/pubsub/docs/subscriber + +**Terrafrom Mappings:** + +- `google_pubsub_subscription.name` + +## Supported Methods + +- `GET`: Get a gcp-pub-sub-subscription by its "name" +- `LIST`: List all gcp-pub-sub-subscription +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +A subscription can be of type “BigQuery subscription”, in which case Pub/Sub automatically streams all received messages into the linked BigQuery table. Overmind therefore links the subscription to the destination `gcp-big-query-table` so that you can see where your data will land. + +### [`gcp-pub-sub-subscription`](/sources/gcp/Types/gcp-pub-sub-subscription) + +Multiple subscriptions may exist on the same topic or share common dead-letter topics and filters. Overmind links related subscriptions together so you can understand fan-out patterns or duplicated consumption paths for the same data. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Every subscription is attached to exactly one topic, from which it receives messages. This parent–child relationship is surfaced by Overmind via a direct link to the source `gcp-pub-sub-topic`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets can emit object-change notifications to Pub/Sub topics. Subscriptions that listen to such topics are therefore operationally coupled to the originating bucket. Overmind links the subscription to the relevant `gcp-storage-bucket` so you can trace the flow of change events from storage to message consumers. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md new file mode 100644 index 00000000..92968f0f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-pub-sub-topic.md @@ -0,0 +1,26 @@ +--- +title: GCP Pub Sub Topic +sidebar_label: gcp-pub-sub-topic +--- + +A Google Cloud Pub/Sub Topic is a named message stream into which publishers send messages and from which subscribers receive them. Topics act as the core distribution point in the Pub/Sub service, decoupling producers and consumers and enabling asynchronous, scalable communication between systems. For full details see the official documentation: https://docs.cloud.google.com/pubsub/docs/create-topic. + +**Terrafrom Mappings:** + +- `google_pubsub_topic.name` + +## Supported Methods + +- `GET`: Get a gcp-pub-sub-topic by its "name" +- `LIST`: List all gcp-pub-sub-topic +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Pub/Sub Topic can be encrypted with a customer-managed Cloud KMS key. When such a key is specified, the topic will hold a reference to the corresponding `gcp-cloud-kms-crypto-key`, and Overmind will surface this dependency so you can assess the impact of key rotation or removal. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Storage buckets can be configured to send event notifications to a Pub/Sub Topic (for example, when objects are created or deleted). Overmind links the bucket to the topic so you can understand which storage resources rely on the topic and evaluate the blast radius of changes to either side. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md new file mode 100644 index 00000000..fe0ec384 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md @@ -0,0 +1,31 @@ +--- +title: GCP Redis Instance +sidebar_label: gcp-redis-instance +--- + +Cloud Memorystore for Redis provides a fully managed, in-memory, open-source Redis service on Google Cloud. It is commonly used for low-latency caching, session management, real-time analytics and message brokering. When you create an instance Google handles provisioning, patching, monitoring, fail-over and, if requested, TLS encryption and customer-managed encryption keys (CMEK). +More information can be found in the official documentation: https://cloud.google.com/memorystore/docs/redis + +**Terrafrom Mappings:** + +- `google_redis_instance.id` + +## Supported Methods + +- `GET`: Get a gcp-redis-instance by its "locations|instances" +- ~~`LIST`~~ +- `SEARCH`: Search Redis instances in a location. Use the format "location" or "projects/[project_id]/locations/[location]/instances/[instance_name]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If CMEK is enabled, the Redis instance is encrypted at rest using a Cloud KMS CryptoKey. Overmind links the instance to the crypto key so you can trace data-at-rest encryption dependencies and evaluate key rotation or IAM policies. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each Redis instance is created inside a specific VPC network and subnet. Linking to the compute network allows you to understand network reachability, firewall rules and peering arrangements that could affect the instance. + +### [`gcp-compute-ssl-certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +When TLS is enabled, Redis serves Google-managed certificates under the hood. Overmind associates these certificates (represented as Compute SSL Certificate resources) so that certificate expiry and chain of trust can be audited alongside the Redis service. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md new file mode 100644 index 00000000..5368eede --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md @@ -0,0 +1,46 @@ +--- +title: GCP Run Revision +sidebar_label: gcp-run-revision +--- + +A Cloud Run Revision represents an immutable snapshot of the code and configuration that Cloud Run executes. Every time you deploy a new container image or change the runtime configuration of a Cloud Run Service, a new Revision is created and given a unique name. The Revision stores details such as the container image reference, environment variables, scaling limits, traffic settings, networking options and the service account under which the workload runs. Official documentation: https://docs.cloud.google.com/run/docs/managing/revisions + +## Supported Methods + +- `GET`: Get a gcp-run-revision by its "locations|services|revisions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-revision by its "locations|services" + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +The Revision’s `container.image` field points to a Docker image that is normally stored in Artifact Registry (or the older Container Registry). Overmind therefore links the Revision to the exact image digest it deploys, so you can see what code is really running. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the Revision mounts secrets or other resources that are encrypted with Cloud KMS, those crypto-keys are surfaced as links. This helps you understand which keys would be required to decrypt data at runtime. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When a Revision is configured with a Serverless VPC Connector or egress settings that reference a particular VPC network, the corresponding `compute.network` is linked. This reveals the network perimeter through which outbound traffic may flow. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +Similarly, a Revision may target a specific sub-network (for example `vpcAccess.connectorSubnetwork`). Overmind links the Revision to that `compute.subnetwork` so you can trace which CIDR ranges and routes apply. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Each Revision runs with an IAM service account specified in its `serviceAccountName` field. Linking to the service account lets you inspect the permissions that the workload inherits. + +### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) + +A Revision belongs to exactly one Cloud Run Service. The link to the parent Service shows the traffic allocation, routing configuration and other higher-level settings that govern how the Revision is invoked. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +If the Revision’s metadata includes Cloud SQL connection strings (via the `cloudSqlInstances` setting), Overmind links to the referenced Cloud SQL instances, making database dependencies explicit. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Revisions can mount Cloud Storage buckets using Cloud Storage FUSE volumes or reference buckets through environment variables. When such configuration is detected, the corresponding buckets are linked so you can assess data-at-rest exposure. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md new file mode 100644 index 00000000..deff181d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md @@ -0,0 +1,54 @@ +--- +title: GCP Run Service +sidebar_label: gcp-run-service +--- + +Cloud Run Service is a fully-managed container execution environment that lets you run stateless HTTP containers on demand within Google Cloud. A Service represents the top-level Cloud Run resource, providing a stable URL, traffic splitting, configuration, and revision management for your containerised workload. For full details see the Google Cloud documentation: https://cloud.google.com/run/docs/reference/rest/v2/projects.locations.services + +**Terrafrom Mappings:** + +- `google_cloud_run_v2_service.id` + +## Supported Methods + +- `GET`: Get a gcp-run-service by its "locations|services" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-service by its "locations" + +## Possible Links + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +A Cloud Run Service pulls its container image from Artifact Registry (or Container Registry). The linked `gcp-artifact-registry-docker-image` represents the specific image digest or tag referenced in the Service spec. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the Service’s container image or any attached Secret Manager secret is encrypted with a customer-managed encryption key (CMEK), the Cloud Run Service will be linked to the corresponding `gcp-cloud-kms-crypto-key`. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When a Cloud Run Service is configured with a Serverless VPC Access connector, it attaches to a VPC network to reach private resources. That network is represented here as a `gcp-compute-network`. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +The Serverless VPC Access connector also lives on a particular subnetwork. The Cloud Run Service therefore relates to the `gcp-compute-subnetwork` used for outbound traffic. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every Cloud Run Service executes with an identity (the “service account” set in the Service’s `executionEnvironment` or `serviceAccount`). This runtime identity is captured as a link to `gcp-iam-service-account`. + +### [`gcp-run-revision`](/sources/gcp/Types/gcp-run-revision) + +Each deployment of a Cloud Run Service creates an immutable Revision. The Service maintains traffic routing rules among its Revisions, so it links to one or more `gcp-run-revision` resources. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +Environment variables or mounted volumes can reference secrets stored in Secret Manager. Any such secret referenced by the Service or its Revisions appears as a `gcp-secret-manager-secret` link. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +If the Service includes a Cloud SQL connection string (via the `cloudsql-instances` annotation), Overmind records a relationship to the corresponding `gcp-sql-admin-instance`. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud Run Services may interact with Cloud Storage—for example, by having a URL environment variable or event trigger configuration. Where such a bucket name is detected in the Service configuration, it is linked here as `gcp-storage-bucket`. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md new file mode 100644 index 00000000..be765812 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-secret-manager-secret.md @@ -0,0 +1,26 @@ +--- +title: GCP Secret Manager Secret +sidebar_label: gcp-secret-manager-secret +--- + +A Google Cloud Secret Manager Secret is the logical container for sensitive data such as API keys, passwords and certificates stored in Secret Manager. The secret resource defines metadata and access-control policies, while one or more numbered “versions” hold the actual payload, enabling safe rotation and roll-back. Secrets are encrypted at rest with Google-managed keys by default, or with a user-supplied Cloud KMS key, and access is governed through IAM. For further information see the official documentation: https://cloud.google.com/secret-manager/docs + +**Terrafrom Mappings:** + +- `google_secret_manager_secret.secret_id` + +## Supported Methods + +- `GET`: Get a gcp-secret-manager-secret by its "name" +- `LIST`: List all gcp-secret-manager-secret +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a customer-managed encryption key (CMEK) has been configured for this secret, the secret’s `kms_key_name` field will reference a Cloud KMS Crypto Key. Overmind surfaces that link so that you can trace how the secret is encrypted and assess key-management risks. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +Secret Manager can be set to publish notifications (e.g. when a new secret version is added or destroyed) to a Pub/Sub topic. When such a notification configuration exists, the secret will link to the relevant Pub/Sub topic, allowing you to review who can subscribe to, or forward, these events. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md new file mode 100644 index 00000000..32c701bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-security-center-management-security-center-service.md @@ -0,0 +1,13 @@ +--- +title: GCP Security Center Management Security Center Service +sidebar_label: gcp-security-center-management-security-center-service +--- + +A Security Center Service resource represents the activation and configuration of Google Cloud Security Command Center (SCC) for a particular location (for example `europe-west2`) within a project, folder, or organisation. It records whether SCC is enabled, the current service tier (Standard, Premium, or Enterprise), and other operational metadata such as activation time and billing status. Administrators use this resource to programme-matically enable or disable SCC, upgrade or downgrade the service tier, and verify the health of the service across all regions. +Official documentation: https://cloud.google.com/security-command-center/docs/reference/security-center-management/rest/v1/folders.locations.securityCenterServices#SecurityCenterService + +## Supported Methods + +- `GET`: Get a gcp-security-center-management-security-center-service by its "locations|securityCenterServices" +- ~~`LIST`~~ +- `SEARCH`: Search Security Center services in a location. Use the format "location". diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md new file mode 100644 index 00000000..bb6b4f89 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-directory-endpoint.md @@ -0,0 +1,23 @@ +--- +title: GCP Service Directory Endpoint +sidebar_label: gcp-service-directory-endpoint +--- + +A Service Directory Endpoint represents a concrete network destination that backs a Service Directory Service inside Google Cloud. Each endpoint records the IP address, port and (optionally) metadata that client workloads use to discover and call the service. Endpoints are created inside a hierarchy of **Project → Location → Namespace → Service → Endpoint** and are resolved at run-time through Service Directory’s DNS or HTTP APIs, allowing producers to register instances and consumers to discover them without hard-coding addresses. +Official documentation: https://cloud.google.com/service-directory/docs/reference/rest/v1/projects.locations.namespaces.services.endpoints + +**Terrafrom Mappings:** + +- `google_service_directory_endpoint.id` + +## Supported Methods + +- `GET`: Get a gcp-service-directory-endpoint by its "locations|namespaces|services|endpoints" +- ~~`LIST`~~ +- `SEARCH`: Search for endpoints by "location|namespace_id|service_id" or "projects/[project_id]/locations/[location]/namespaces/[namespace_id]/services/[service_id]/endpoints/[endpoint_id]" which is supported for terraform mappings. + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Each endpoint is associated with a specific VPC network; the `network` field determines from which network the endpoint can be reached and which clients can resolve it. When Overmind discovers a Service Directory Endpoint, it links the item to the corresponding gcp-compute-network so you can trace service discovery issues back to network configuration or segmentation problems. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md new file mode 100644 index 00000000..eb94c112 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-service-usage-service.md @@ -0,0 +1,20 @@ +--- +title: GCP Service Usage Service +sidebar_label: gcp-service-usage-service +--- + +Represents an individual Google Cloud API or service (for example, `pubsub.googleapis.com`, `compute.googleapis.com`) that can be enabled or disabled within a project or folder via the Service Usage API. +It holds metadata such as the service’s name, state (ENABLED, DISABLED, etc.), configuration and any consumer-specific settings. Managing this resource controls whether dependent resources in the project are allowed to operate. +Official documentation: https://cloud.google.com/service-usage/docs/overview + +## Supported Methods + +- `GET`: Get a gcp-service-usage-service by its "name" +- `LIST`: List all gcp-service-usage-service +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +A Pub/Sub topic can only exist and function if the `pubsub.googleapis.com` service is ENABLED in the same project. Overmind links a `gcp-service-usage-service` whose name is `pubsub.googleapis.com` to all `gcp-pub-sub-topic` resources in that project so that you can assess the blast radius of disabling the API. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md new file mode 100644 index 00000000..7206a904 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md @@ -0,0 +1,28 @@ +--- +title: GCP Spanner Database +sidebar_label: gcp-spanner-database +--- + +Google Cloud Spanner is Google Cloud’s fully-managed, horizontally-scalable, relational database service. +A Spanner **database** is the logical container that holds your tables, schema objects and data inside a Spanner instance. Each database inherits the instance’s compute and storage configuration and can be encrypted either with Google-managed keys or with a customer-managed key (CMEK). +For an overview of the service see the official documentation: https://cloud.google.com/spanner/docs/overview + +**Terrafrom Mappings:** + +- `google_spanner_database.name` + +## Supported Methods + +- `GET`: Get a gcp-spanner-database by its "instances|databases" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-spanner-database by its "instances" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Spanner database can be encrypted with a customer-managed encryption key (CMEK) stored in Cloud KMS. When CMEK is enabled, the database resource is linked to the specific `gcp-cloud-kms-crypto-key` that provides its encryption. + +### [`gcp-spanner-instance`](/sources/gcp/Types/gcp-spanner-instance) + +Every Spanner database lives inside a Spanner instance. The database inherits performance characteristics and regional configuration from its parent `gcp-spanner-instance`, making this a direct parent–child relationship. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md new file mode 100644 index 00000000..b3820f65 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md @@ -0,0 +1,23 @@ +--- +title: GCP Spanner Instance +sidebar_label: gcp-spanner-instance +--- + +A **Cloud Spanner instance** is the top-level container for Cloud Spanner resources in Google Cloud. It specifies the geographic placement of the underlying nodes, the amount of compute capacity allocated (measured in processing units), and the instance’s name and labels. All Cloud Spanner databases and their data live inside an instance, and the instance’s configuration determines their availability and latency characteristics. +For full details see the Google Cloud documentation: https://cloud.google.com/spanner/docs/instances + +**Terrafrom Mappings:** + +- `google_spanner_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-spanner-instance by its "name" +- `LIST`: List all gcp-spanner-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-spanner-database`](/sources/gcp/Types/gcp-spanner-database) + +A Cloud Spanner instance can host one or more Cloud Spanner databases. Each `gcp-spanner-database` discovered by Overmind will therefore be linked to the `gcp-spanner-instance` that contains it, allowing you to see which databases would be affected by changes to, or deletion of, the parent instance. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md new file mode 100644 index 00000000..7192c072 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup-run.md @@ -0,0 +1,22 @@ +--- +title: GCP Sql Admin Backup Run +sidebar_label: gcp-sql-admin-backup-run +--- + +A GCP SQL Admin Backup Run represents an individual on-demand or automatically-scheduled backup created for a Cloud SQL instance. Each backup run records metadata such as its status, start and end times, location, encryption information and size. Backup runs are managed through the Cloud SQL Admin API and can be listed, retrieved or deleted by project administrators. For full details see Google’s documentation: https://cloud.google.com/sql/docs/mysql/admin-api/rest/v1/backupRuns + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-backup-run by its "instances|backupRuns" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-sql-admin-backup-run by its "instances" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If a Cloud SQL instance is configured with customer-managed encryption keys (CMEK), the backup run is encrypted with the specified KMS CryptoKey. The backup run therefore references the CryptoKey used for encryption. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +Every backup run belongs to exactly one Cloud SQL instance; the instance is the parent resource under which the backup run is created. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md new file mode 100644 index 00000000..3c686d99 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-backup.md @@ -0,0 +1,28 @@ +--- +title: GCP Sql Admin Backup +sidebar_label: gcp-sql-admin-backup +--- + +A **GCP Sql Admin Backup** represents the backup configuration that protects a Cloud SQL instance. +The object contains the settings that determine when and how Google Cloud takes automatic or on-demand snapshots of the instance, including the backup window, retention period, and (when Customer-Managed Encryption Keys are used) the CryptoKey that encrypts the resulting files. +For a detailed description of Cloud SQL backups see the official documentation: https://cloud.google.com/sql/docs/mysql/backup-recovery/backups. + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-backup by its "name" +- `LIST`: List all gcp-sql-admin-backup +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the backup is encrypted with a Customer-Managed Encryption Key (CMEK), Overmind links the backup to the `gcp-cloud-kms-crypto-key` that holds the key material. Analysing this relationship lets you verify that the key exists, is in the correct state, and has the appropriate IAM policy. + +### [`gcp-sql-admin-backup-run`](/sources/gcp/Types/gcp-sql-admin-backup-run) + +Every time the backup configuration is executed it produces a Backup Run. This link connects the configuration to those individual `gcp-sql-admin-backup-run` objects, allowing you to trace whether recent runs succeeded and to inspect metadata such as the size and status of each run. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +The backup configuration belongs to a specific Cloud SQL instance. This link points from the backup resource to the parent `gcp-sql-admin-instance`, helping you understand which database workload the backup protects and enabling dependency traversal from the instance to its safety mechanisms. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md new file mode 100644 index 00000000..86a0993c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-sql-admin-instance.md @@ -0,0 +1,42 @@ +--- +title: GCP Sql Admin Instance +sidebar_label: gcp-sql-admin-instance +--- + +A GCP SQL Admin Instance represents a managed Cloud SQL database instance in Google Cloud Platform. It encapsulates the configuration of the database engine (MySQL, PostgreSQL or SQL Server), machine tier, storage, high-availability settings, networking and encryption options. The resource is managed through the Cloud SQL Admin API, which is documented here: https://cloud.google.com/sql/docs/mysql/admin-api/. Creating or modifying an instance via Terraform, the Cloud Console or gcloud ultimately results in API calls against this object. + +**Terrafrom Mappings:** + +- `google_sql_database_instance.name` + +## Supported Methods + +- `GET`: Get a gcp-sql-admin-instance by its "name" +- `LIST`: List all gcp-sql-admin-instance +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If Customer-Managed Encryption Keys (CMEK) are enabled for the instance, the instance is encrypted with a specific Cloud KMS Crypto Key. Overmind links the instance to the `gcp-cloud-kms-crypto-key` that provides its disk-level encryption key. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When an instance is configured for private IP or has authorised networks for public IP access, it attaches to one or more VPC networks. Overmind therefore links the instance to the `gcp-compute-network` resources that define those VPCs. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Cloud SQL automatically creates or uses service accounts to perform backups, replication and other administrative tasks. The instance is linked to the `gcp-iam-service-account` identities that act on its behalf, allowing you to trace permissions and potential privilege escalation paths. + +### [`gcp-sql-admin-backup-run`](/sources/gcp/Types/gcp-sql-admin-backup-run) + +Each automated or on-demand backup of an instance is represented by a Backup Run resource. Overmind links every `gcp-sql-admin-backup-run` to the parent instance so you can see the full backup history and retention compliance. + +### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) + +Instances may reference other instances when configured for read replicas, high-availability failover or cloning. Overmind links an instance to any peer `gcp-sql-admin-instance` that serves as its primary, replica or clone source/target. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Cloud SQL supports import/export of SQL dump files and automatic log exports to Cloud Storage. The instance is linked to any `gcp-storage-bucket` that it reads from or writes to during these operations, revealing data-exfiltration or retention risks. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md new file mode 100644 index 00000000..a4b4edde --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md @@ -0,0 +1,30 @@ +--- +title: GCP Storage Bucket +sidebar_label: gcp-storage-bucket +--- + +A GCP Storage Bucket is a logical container in Google Cloud Storage that holds your objects (blobs). Buckets provide globally-unique namespaces, configurable lifecycle policies, access controls, versioning, and encryption options, allowing organisations to store and serve unstructured data such as backups, media files, or static web assets. See the official documentation for full details: https://cloud.google.com/storage/docs/key-terms#buckets + +**Terrafrom Mappings:** + +- `google_storage_bucket.name` + +## Supported Methods + +- `GET`: Get a gcp-storage-bucket by its "name" +- `LIST`: List all gcp-storage-bucket +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Instances and other compute resources that run inside a VPC network often read from or write to a Storage Bucket. Additionally, when Private Google Access or VPC Service Controls are enabled, the bucket’s accessibility is governed by the associated compute network, creating a security dependency between the two resources. + +### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) + +Audit logs for a Storage Bucket can be routed into a Cloud Logging bucket, and Logging buckets can export their contents to a Storage Bucket. Either configuration establishes a link whereby changes to the Storage Bucket may affect log retention and compliance. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A Storage Bucket can be configured to use Customer-Managed Encryption Keys (CMEK). When this option is enabled, the bucket references a Cloud KMS CryptoKey for data-at-rest encryption, making the bucket’s availability and security reliant on the referenced key’s state and permissions. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md new file mode 100644 index 00000000..01476fc4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-transfer-transfer-job.md @@ -0,0 +1,34 @@ +--- +title: GCP Storage Transfer Transfer Job +sidebar_label: gcp-storage-transfer-transfer-job +--- + +A Storage Transfer Service Job represents a scheduled or on-demand operation that copies data between cloud storage systems or from on-premises sources into Google Cloud Storage. A job defines source and destination locations, transfer options (such as whether to delete objects after transfer), scheduling, and optional notifications. For full details see the official Google documentation: https://cloud.google.com/storage-transfer/docs/overview + +**Terrafrom Mappings:** + +- `google_storage_transfer_job.name` + +## Supported Methods + +- `GET`: Get a gcp-storage-transfer-transfer-job by its "name" +- `LIST`: List all gcp-storage-transfer-transfer-job +- ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Storage Transfer Service creates and utilises a dedicated service account to read from the source and write to the destination. The transfer job must have the correct IAM roles granted on this service account, making the two resources inherently linked. + +### [`gcp-pub-sub-subscription`](/sources/gcp/Types/gcp-pub-sub-subscription) + +If transfer job notifications are configured, the Storage Transfer Service publishes messages to a Pub/Sub topic. A subscription attached to that topic receives the events, so a job that emits notifications will be related to the downstream subscriptions. + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +The transfer job can be configured to send success, failure, or progress notifications to a specific Pub/Sub topic. That topic therefore has a direct relationship with the job. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +Buckets are commonly used as both sources and destinations for transfer jobs. Any bucket referenced in the `transferSpec` of a job (either as a source or destination) is linked to that job. diff --git a/docs.overmind.tech/docs/sources/gcp/_category_.json b/docs.overmind.tech/docs/sources/gcp/_category_.json new file mode 100644 index 00000000..80514572 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "GCP 🆕", + "position": 3, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your Google Cloud Platform" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/configuration.md b/docs.overmind.tech/docs/sources/gcp/configuration.md new file mode 100644 index 00000000..450d3b11 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/configuration.md @@ -0,0 +1,439 @@ +--- +title: GCP Configuration +sidebar_position: 1 +--- + +# GCP Configuration + +## Overview + +Overmind's GCP infrastructure discovery provides comprehensive visibility into your Google Cloud Platform resources through secure, read-only access using Google Cloud's native IAM system. + +Overmind supports two authentication methods: + +1. **Direct Access** (Default) - Grant permissions directly to the Overmind service account +2. **Service Account Impersonation** (Optional) - Create your own service account with permissions, then allow Overmind to impersonate it + +Both methods provide the same functionality and security. Choose the method that fits your organization's security policies. + +### Authentication Methods Comparison + +**Direct Access:** + +- Simplest setup - grant roles directly to Overmind's service account +- Best for quick setup and straightforward security requirements + +**Service Account Impersonation:** + +- Enhanced control - you create and manage your own service account +- Better for organizations requiring all service accounts to be internally managed +- Provides dual identity in audit logs (both Overmind's SA and your SA) +- Learn more: [GCP Service Account Impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) + +### Why Service Account-Based Access? + +Each customer receives a unique Overmind service account with minimal, read-only permissions. All access is logged through Google Cloud's audit system, giving you complete control with no shared credentials. This aligns with [Google Cloud's security best practices](https://cloud.google.com/security/best-practices). + +## Prerequisites + +Before beginning setup, ensure you have: + +- **GCP Resource Access**: Appropriate IAM admin permissions at the organization, folder, or project level to grant IAM roles (and create service accounts for impersonation) +- **Required Tools**: One of the following: + - [Google Cloud CLI (`gcloud`)](https://cloud.google.com/sdk/docs/install) installed and authenticated + - Terraform with the Google Cloud Provider configured +- **Parent Resource**: The parent resource ID where Overmind will discover resources. This can be: + - An organization: `organizations/123456789` + - A folder: `folders/987654321` + - A project: `projects/my-project-id` +- **Regional Scope**: List of GCP regions where your resources are located (mandatory for source configuration) + +### Authentication Setup + +Ensure your local environment is authenticated with Google Cloud: + +```bash +# Authenticate with Google Cloud +gcloud auth login + +# Set your default project (if using a project as parent) +gcloud config set project YOUR_PROJECT_ID + +# Verify authentication +gcloud auth list +``` + +For Terraform users, configure [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials): + +```bash +gcloud auth application-default login +``` + +## Quick Start + +### Step 1: Create Your Overmind GCP Source + +1. Navigate to **Settings** > **Sources** > **Add Source** > **GCP** in the Overmind application +2. Configure your source: + - **Parent ID**: The parent resource to discover from. Format: + - Organization: `organizations/123456789` + - Folder: `folders/987654321` + - Project: `projects/my-project-id` + - **Name**: A descriptive name for this source (optional) + - **Regions**: Select the regions where your resources are located (mandatory) + - **Impersonation** (optional): Toggle on to use service account impersonation + - Enter the email of the service account you'll create (e.g., `overmind-reader@your-project.iam.gserviceaccount.com`) + - Use any unique name for your service account +3. Click **Create Source** + +You'll be redirected to the source details page showing: + +- The Overmind service account email (e.g., `C-xxxxx@ovm-production.iam.gserviceaccount.com`) +- Configuration instructions customized for your setup +- Whether impersonation is enabled + +### Step 2: Grant Permissions + +The source details page provides customized scripts for your setup. These scripts automatically apply IAM permissions at the level you specified (organization, folder, or project). Permissions granted at a parent level are inherited by all child resources. + +Choose your preferred method: + +#### Option A: Cloud Shell (Easiest) + +Click the **"Open in Google Cloud Shell"** button shown on the source details page. This provides you with the scripts and guidance needed to complete the setup. Follow the instructions in Cloud Shell to run the appropriate setup script for your configuration. + +#### Option B: Manual Script + +Copy and run the bash script shown on the source details page. The script automatically detects whether you're using an organization, folder, or project parent and applies the correct `gcloud` commands. The script varies based on whether impersonation is enabled: + +**For Direct Access:** + +- Grants read-only roles directly to the Overmind service account at your specified parent level +- For project-level parents, also creates a custom role for additional permissions + +**For Impersonation:** + +- Grants read-only roles to your service account at your specified parent level (you must create the service account manually first) +- For project-level parents, also creates a custom role for additional permissions +- Grants Overmind's service account permission to impersonate yours (`roles/iam.serviceAccountTokenCreator`) + +#### Option C: Terraform + +Copy the Terraform configuration shown on the source details page and apply it: + +```bash +terraform init +terraform plan +terraform apply +``` + +### Step 3: Verify Source Status + +1. Navigate to **Settings** > **Sources** in the Overmind application +2. Verify your GCP source shows as **Healthy** + +## Required Permissions + +Overmind requires read-only IAM roles for infrastructure discovery. See the [Required GCP Roles Reference](#required-gcp-roles-reference) for the complete list. + +### Permission Flow + +Permissions can be applied at any level of the GCP resource hierarchy and are inherited by child resources: + +**Direct Access:** + +``` +Your GCP Organization/Folder/Project + └─ Overmind Service Account + └─ Granted: Viewer roles (+ custom role for project-level) + └─ Inherited by all child folders and projects +``` + +**Service Account Impersonation:** + +``` +Your GCP Organization/Folder/Project + ├─ Your Service Account + │ └─ Granted: Viewer roles (+ custom role for project-level) + │ └─ Inherited by all child folders and projects + └─ Overmind Service Account + └─ Granted: roles/iam.serviceAccountTokenCreator on Your Service Account +``` + +## Switching Between Authentication Methods + +### Enable Impersonation + +1. Create a service account in your GCP project (if you haven't already) +2. Grant it the required read-only roles and impersonation permission (use the scripts from the source details page - they handle both) +3. Edit your source in Overmind: enable **Impersonation** and enter your service account email +4. (Optional) Remove direct permissions from Overmind's service account + +### Disable Impersonation + +1. Edit your source in Overmind: disable **Impersonation** (this updates the scripts on the source details page) +2. Grant the required read-only roles directly to Overmind's service account (use the updated scripts from the source details page) +3. (Optional) Remove the impersonation permission and delete your service account + +## Validation + +### Verify IAM Permissions + +**Using Google Cloud Console:** + +1. Navigate to [IAM & Admin > IAM](https://console.cloud.google.com/iam-admin/iam) +2. Select your organization, folder, or project +3. Search for the service account (Overmind's or yours, depending on setup) +4. Verify all required roles are listed + +**Using Google Cloud CLI:** + +For direct access at organization level: + +```bash +gcloud organizations get-iam-policy YOUR_ORG_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For direct access at folder level: + +```bash +gcloud resource-manager folders get-iam-policy YOUR_FOLDER_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For direct access at project level: + +```bash +gcloud projects get-iam-policy YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +For impersonation (verify Overmind can impersonate your SA): + +```bash +gcloud iam service-accounts get-iam-policy YOUR_SA_EMAIL \ + --project=YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --format="table(bindings.role,bindings.members)" \ + --filter="bindings.members:serviceAccount:OVERMIND_SA_EMAIL" +``` + +### Test Source Discovery + +1. Navigate to **Explore** in the Overmind application +2. Run a query: GCP sources are prefixed with `gcp-` + - To list all VMs: `gcp-compute-instance` > `LIST` +3. Verify resources are being discovered + +### Validate Regional Coverage + +Review the **Regions** configuration in your source settings and verify discovered resources match your expected regional distribution. + +## Troubleshooting + +### Common Issues + +**"Insufficient Permissions" Error** + +Verify all required roles are assigned at the appropriate level: + +```bash +# For organization-level access +gcloud organizations get-iam-policy YOUR_ORG_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" + +# For folder-level access +gcloud resource-manager folders get-iam-policy YOUR_FOLDER_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" + +# For project-level access +gcloud projects get-iam-policy YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:serviceAccount:SA_EMAIL" +``` + +Re-run the setup script or check for organization-level policies restricting service account access. + +**No Resources Discovered** + +1. Verify regional configuration matches where your resources exist +2. For project-level parents, check that required GCP APIs are enabled: + ```bash + gcloud services list --enabled --project=YOUR_PROJECT_ID + ``` +3. For organization or folder-level parents, verify that you have the necessary permissions to list projects and that child projects have the required APIs enabled +4. Some resources may require additional permissions at different levels of the hierarchy + +**Service Account Impersonation Fails** + +1. Verify the impersonation permission is granted: + + ```bash + gcloud iam service-accounts get-iam-policy YOUR_SA_EMAIL --project=YOUR_PROJECT_ID + ``` + + You should see Overmind's service account with `roles/iam.serviceAccountTokenCreator`. + +2. Verify your service account exists and isn't disabled: + + ```bash + gcloud iam service-accounts describe YOUR_SA_EMAIL --project=YOUR_PROJECT_ID + ``` + +3. Ensure the service account email in Overmind matches exactly + +4. Wait for propagation: IAM policy changes can take a few minutes to propagate. Wait 2-5 minutes after granting permissions before testing. + +5. Check organization policies: Some organization policies may restrict service account impersonation. + +**Service Account Not Found** + +1. Verify you copied the correct email from the Overmind application +2. Ensure the email format is correct (ends with `.iam.gserviceaccount.com`) +3. For impersonation: verify your service account was created successfully +4. Contact [Overmind support](https://docs.overmind.tech/misc/support) if issues persist + +**Terraform Apply Failures** + +1. Verify authentication: `gcloud auth application-default print-access-token` +2. Ensure your credentials have necessary IAM permissions +3. For impersonation: ensure you have `iam.serviceAccounts.create` permission + +### Getting Help + +If you continue to experience issues, contact [Overmind support](https://docs.overmind.tech/misc/support) with: + +- Your GCP parent resource (organization/folder/project ID) +- The Overmind service account email +- Your service account email (if using impersonation) +- Whether you're using direct access or impersonation +- The parent level you're configuring (organization, folder, or project) +- Specific error messages and screenshots + +## Security Considerations + +### Principle of Least Privilege + +All roles are read-only and do not allow: + +- Resource modification or deletion +- Data access (beyond metadata) +- Configuration changes +- Administrative operations + +### Monitoring and Auditing + +1. Enable [Cloud Audit Logs](https://cloud.google.com/logging/docs/audit) for your project +2. Monitor service account activity in audit logs +3. Configure alerts for unusual behavior + +**Impersonation Audit Benefits:** +With impersonation, audit logs show both Overmind's identity and your service account identity, providing enhanced traceability. + +### Permission Management + +- **Regular Review**: Periodically review granted permissions +- **Revocation**: Remove access anytime: + - **Direct access**: Remove IAM bindings + - **Impersonation**: Remove `serviceAccountTokenCreator` role or disable/delete your service account + +## Required Permissions + +Overmind requires read-only access to discover and map your GCP infrastructure. The setup scripts provided in the Overmind application automatically grant all necessary permissions. + +### What Gets Configured + +**Essential role for resource discovery:** + +- `roles/browser` - Required for listing projects and navigating the resource hierarchy + +**Read-only viewer roles** for GCP services including: + +- Compute Engine, GKE, Cloud Run, Cloud Functions +- Cloud SQL, BigQuery, Spanner, Cloud Storage +- IAM, networking, monitoring, and logging resources +- And other GCP services + +**A custom role** with additional permissions for: + +- BigQuery data transfer configurations +- Spanner database details + +**Project-level-only roles** (only applied when using `projects/` parent): + +- `roles/iam.roleViewer` - View IAM roles +- `roles/iam.serviceAccountViewer` - View service accounts + +> **Note:** Some GCP IAM roles can only be granted at the project level, not at the organization or folder level. When configuring at the organization or folder level, these project-specific roles are automatically excluded. The custom role and project-level IAM roles are only created and assigned when using a project-level parent (e.g., `projects/my-project`). + +**For impersonation** (if enabled): + +- `roles/iam.serviceAccountTokenCreator` - Allows Overmind to impersonate your service account + +All permissions are read-only and do not allow resource modification, deletion, or access to data beyond metadata. + +The complete list of roles is included in the setup scripts shown in your source details page. These scripts are automatically updated as Overmind adds support for new GCP services and adapt based on whether you're configuring at the organization, folder, or project level. + +## Required GCP Roles Reference + +Here are all the predefined GCP roles that Overmind requires, plus the custom role for additional permissions: + +### Predefined Roles + +| Role | Purpose | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `roles/browser` | **Required:** List projects and navigate resource hierarchy [GCP Docs](https://cloud.google.com/iam/docs/understanding-roles#browser) | +| `roles/aiplatform.viewer` | AI Platform resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/aiplatform#aiplatform.viewer) | +| `roles/artifactregistry.reader` | Artifact Registry repository discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/artifactregistry#artifactregistry.reader) | +| `roles/bigquery.metadataViewer` | BigQuery metadata discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigquery#bigquery.metadataViewer) | +| `roles/bigquery.user` | BigQuery data transfer discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigquery#bigquery.user) | +| `roles/bigtable.viewer` | Cloud Bigtable resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/bigtable#bigtable.viewer) | +| `roles/cloudbuild.builds.viewer` | Cloud Build resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudbuild#cloudbuild.builds.viewer) | +| `roles/cloudfunctions.viewer` | Cloud Functions discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudfunctions#cloudfunctions.viewer) | +| `roles/cloudkms.viewer` | Cloud KMS resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudkms#cloudkms.viewer) | +| `roles/cloudsql.viewer` | Cloud SQL instance discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/cloudsql#cloudsql.viewer) | +| `roles/compute.viewer` | Compute Engine resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/compute#compute.viewer) | +| `roles/container.viewer` | GKE cluster and resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/container#container.viewer) | +| `roles/dataform.viewer` | Dataform resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataform#dataform.viewer) | +| `roles/dataplex.catalogViewer` | Dataplex catalog resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataplex#dataplex.catalogViewer) | +| `roles/dataplex.viewer` | Dataplex resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataplex#dataplex.viewer) | +| `roles/dataproc.viewer` | Dataproc cluster discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dataproc#dataproc.viewer) | +| `roles/dns.reader` | Cloud DNS resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/dns#dns.reader) | +| `roles/essentialcontacts.viewer` | Essential Contacts discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/essentialcontacts#essentialcontacts.viewer) | +| `roles/eventarc.viewer` | Eventarc trigger discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/eventarc#eventarc.viewer) | +| `roles/file.viewer` | Cloud Filestore discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/file#file.viewer) | +| `roles/iam.roleViewer` | **Project-level only:** IAM role discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/iam#iam.roleViewer) | +| `roles/iam.serviceAccountViewer` | **Project-level only:** IAM service account discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/iam#iam.serviceAccountViewer) | +| `roles/logging.viewer` | Cloud Logging resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/logging#logging.viewer) | +| `roles/monitoring.viewer` | Cloud Monitoring resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/monitoring#monitoring.viewer) | +| `roles/orgpolicy.policyViewer` | Organization Policy discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/orgpolicy#orgpolicy.policyViewer) | +| `roles/pubsub.viewer` | Pub/Sub resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/pubsub#pubsub.viewer) | +| `roles/redis.viewer` | Cloud Memorystore Redis discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/redis#redis.viewer) | +| `roles/resourcemanager.tagViewer` | Resource Manager tag discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/resourcemanager#resourcemanager.tagViewer) | +| `roles/run.viewer` | Cloud Run resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/run#run.viewer) | +| `roles/secretmanager.viewer` | Secret Manager secret discovery (metadata only) [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/secretmanager#secretmanager.viewer) | +| `roles/securitycentermanagement.viewer` | Security Center management discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/securitycentermanagement#securitycentermanagement.viewer) | +| `roles/servicedirectory.viewer` | Service Directory resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/servicedirectory#servicedirectory.viewer) | +| `roles/serviceusage.serviceUsageViewer` | Service Usage discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/serviceusage#serviceusage.serviceUsageViewer) | +| `roles/spanner.viewer` | Cloud Spanner resource discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/spanner#spanner.viewer) | +| `roles/storage.bucketViewer` | Cloud Storage bucket discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/storage#storage.bucketViewer) | +| `roles/storagetransfer.viewer` | Storage Transfer Service discovery [GCP Docs](https://cloud.google.com/iam/docs/roles-permissions/storagetransfer#storagetransfer.viewer) | + +### Custom Role + +| Role | Purpose | +| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `projects/{PROJECT_ID}/roles/overmindCustomRole` | Custom role for additional BigQuery and Spanner permissions **Permissions:** `bigquery.transfers.get` - BigQuery transfer configuration discovery, `spanner.databases.get` - Spanner database detail discovery, `spanner.databases.list` - Spanner database enumeration | + +All predefined roles provide read-only access and are sourced from Google Cloud's [predefined roles documentation](https://cloud.google.com/iam/docs/understanding-roles#predefined). + +**Project-Level Restrictions:** Some roles (`roles/iam.roleViewer` and `roles/iam.serviceAccountViewer`) can only be granted at the project level in GCP. When configuring at the organization or folder level, these roles are automatically excluded. The custom role is also only created and assigned when using a project-level parent (e.g., `projects/my-project`). diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json new file mode 100644 index 00000000..967b9b4b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-batch-prediction-job.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-batch-prediction-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Batch Prediction Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-batch-prediction-job by its \"locations|batchPredictionJobs\"", + "search": true, + "searchDescription": "Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1'" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json new file mode 100644 index 00000000..22003f70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-custom-job.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-custom-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Custom Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-custom-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-custom-job" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json new file mode 100644 index 00000000..67f77a9c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-ai-platform-endpoint", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-model", + "gcp-ai-platform-model-deployment-monitoring-job", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network" + ], + "descriptiveName": "GCP Ai Platform Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-endpoint by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-endpoint" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json new file mode 100644 index 00000000..5d1e6d04 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model-deployment-monitoring-job.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-model-deployment-monitoring-job", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-endpoint", + "gcp-ai-platform-model", + "gcp-cloud-kms-crypto-key", + "gcp-monitoring-notification-channel" + ], + "descriptiveName": "GCP Ai Platform Model Deployment Monitoring Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-model-deployment-monitoring-job by its \"locations|modelDeploymentMonitoringJobs\"", + "search": true, + "searchDescription": "Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1'" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json new file mode 100644 index 00000000..a0ccd633 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-model.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-model", + "category": 8, + "potentialLinks": [ + "gcp-ai-platform-endpoint", + "gcp-ai-platform-pipeline-job", + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Ai Platform Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-model by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-model" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json new file mode 100644 index 00000000..48d48bc7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-ai-platform-pipeline-job.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-ai-platform-pipeline-job", + "category": 8, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Ai Platform Pipeline Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-ai-platform-pipeline-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-ai-platform-pipeline-job" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json new file mode 100644 index 00000000..1c51945e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-artifact-registry-docker-image.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-artifact-registry-docker-image", + "category": 2, + "descriptiveName": "GCP Artifact Registry Docker Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-artifact-registry-docker-image by its \"locations|repositories|dockerImages\"", + "search": true, + "searchDescription": "Search for Docker images in Artifact Registry. Use the format \"location|repository_id\" or \"projects/[project]/locations/[location]/repository/[repository_id]/dockerImages/[docker_image]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_artifact_registry_docker_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json new file mode 100644 index 00000000..fa7b6a88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-data-transfer-transfer-config.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-big-query-data-transfer-transfer-config", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-cloud-kms-crypto-key", + "gcp-pub-sub-topic" + ], + "descriptiveName": "GCP Big Query Data Transfer Transfer Config", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-query-data-transfer-transfer-config by its \"locations|transferConfigs\"", + "search": true, + "searchDescription": "Search for BigQuery Data Transfer transfer configs in a location. Use the format \"location\" or \"projects/project_id/locations/location/transferConfigs/transfer_config_id\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_data_transfer_config.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json new file mode 100644 index 00000000..3efc5539 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-dataset.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-big-query-dataset", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-big-query-model", + "gcp-big-query-routine", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Big Query Dataset", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Dataset by \"gcp-big-query-dataset-id\"", + "list": true, + "listDescription": "List all GCP Big Query Dataset items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_dataset.dataset_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json new file mode 100644 index 00000000..be1751c4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-big-query-model", + "category": 6, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Big Query Model", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Model by \"gcp-big-query-dataset-id|gcp-big-query-model-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Model by \"gcp-big-query-model-id\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json new file mode 100644 index 00000000..b9704125 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-routine.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-query-routine", + "category": 6, + "potentialLinks": ["gcp-big-query-dataset"], + "descriptiveName": "GCP Big Query Routine", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Routine by \"gcp-big-query-dataset-id|gcp-big-query-routine-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Routine by \"gcp-big-query-routine-id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_routine.routine_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json new file mode 100644 index 00000000..6c9b01c6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-table.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-query-table", + "category": 6, + "potentialLinks": ["gcp-big-query-dataset", "gcp-cloud-kms-crypto-key"], + "descriptiveName": "GCP Big Query Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Big Query Table by \"gcp-big-query-dataset-id|gcp-big-query-table-id\"", + "search": true, + "searchDescription": "Search for GCP Big Query Table by \"gcp-big-query-dataset-id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigquery_table.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json new file mode 100644 index 00000000..4358f119 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-app-profile.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-big-table-admin-app-profile", + "category": 7, + "potentialLinks": [ + "gcp-big-table-admin-cluster", + "gcp-big-table-admin-instance" + ], + "descriptiveName": "GCP Big Table Admin App Profile", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-app-profile by its \"instances|appProfiles\"", + "search": true, + "searchDescription": "Search for BigTable App Profiles in an instance. Use the format \"instance\" or \"projects/[project_id]/instances/[instance_name]/appProfiles/[app_profile_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_app_profile.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json new file mode 100644 index 00000000..3f8518c8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-backup.json @@ -0,0 +1,15 @@ +{ + "type": "gcp-big-table-admin-backup", + "potentialLinks": [ + "gcp-big-table-admin-backup", + "gcp-big-table-admin-cluster", + "gcp-big-table-admin-table" + ], + "descriptiveName": "GCP Big Table Admin Backup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-backup by its \"instances|clusters|backups\"", + "search": true, + "searchDescription": "Search for gcp-big-table-admin-backup by its \"instances|clusters\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json new file mode 100644 index 00000000..bb3be7f6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-cluster.json @@ -0,0 +1,15 @@ +{ + "type": "gcp-big-table-admin-cluster", + "category": 7, + "potentialLinks": [ + "gcp-big-table-admin-instance", + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Big Table Admin Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-cluster by its \"instances|clusters\"", + "search": true, + "searchDescription": "Search for gcp-big-table-admin-cluster by its \"instances\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json new file mode 100644 index 00000000..fabaff46 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-instance.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-big-table-admin-instance", + "category": 7, + "potentialLinks": ["gcp-big-table-admin-cluster"], + "descriptiveName": "GCP Big Table Admin Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-big-table-admin-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_bigtable_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json new file mode 100644 index 00000000..f490a956 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-big-table-admin-table.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-big-table-admin-table", + "category": 6, + "potentialLinks": [ + "gcp-big-table-admin-backup", + "gcp-big-table-admin-instance", + "gcp-big-table-admin-table" + ], + "descriptiveName": "GCP Big Table Admin Table", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-big-table-admin-table by its \"instances|tables\"", + "search": true, + "searchDescription": "Search for BigTable tables in an instance. Use the format \"instance_name\" or \"projects/[project_id]/instances/[instance_name]/tables/[table_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_table.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json new file mode 100644 index 00000000..d34a018f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-billing-billing-info.json @@ -0,0 +1,10 @@ +{ + "type": "gcp-cloud-billing-billing-info", + "category": 7, + "potentialLinks": ["gcp-cloud-resource-manager-project"], + "descriptiveName": "GCP Cloud Billing Billing Info", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-billing-billing-info by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json new file mode 100644 index 00000000..234cabf0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-build-build.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-cloud-build-build", + "category": 7, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-iam-service-account", + "gcp-logging-bucket", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Cloud Build Build", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-build-build by its \"name\"", + "list": true, + "listDescription": "List all gcp-cloud-build-build" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json new file mode 100644 index 00000000..aa542ad7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-functions-function.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-cloud-functions-function", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-pub-sub-topic", + "gcp-run-service", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Cloud Functions Function", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-functions-function by its \"locations|functions\"", + "search": true, + "searchDescription": "Search for gcp-cloud-functions-function by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json new file mode 100644 index 00000000..c28ec0de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-cloud-kms-crypto-key", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-key-ring"], + "descriptiveName": "GCP Cloud Kms Crypto Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Cloud Kms Crypto Key by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name\"", + "search": true, + "searchDescription": "Search for GCP Cloud Kms Crypto Key by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json new file mode 100644 index 00000000..6eeaa47c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-key-ring.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-cloud-kms-key-ring", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-crypto-key"], + "descriptiveName": "GCP Cloud Kms Key Ring", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Cloud Kms Key Ring by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name\"", + "search": true, + "searchDescription": "Search for GCP Cloud Kms Key Ring by \"gcp-cloud-kms-key-ring-location\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_kms_key_ring.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json new file mode 100644 index 00000000..61a6eb0b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-project.json @@ -0,0 +1,9 @@ +{ + "type": "gcp-cloud-resource-manager-project", + "category": 7, + "descriptiveName": "GCP Cloud Resource Manager Project", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-resource-manager-project by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json new file mode 100644 index 00000000..48c3491c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-resource-manager-tag-value.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-cloud-resource-manager-tag-value", + "category": 7, + "descriptiveName": "GCP Cloud Resource Manager Tag Value", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-cloud-resource-manager-tag-value by its \"name\"", + "search": true, + "searchDescription": "Search for TagValues by TagKey." + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_tags_tag_value.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json new file mode 100644 index 00000000..53ebe1ef --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-address", + "category": 3, + "potentialLinks": [ + "gcp-compute-address", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Address by \"gcp-compute-address-name\"", + "list": true, + "listDescription": "List all GCP Compute Address items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_address.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json new file mode 100644 index 00000000..41effcaf --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-autoscaler", + "category": 7, + "potentialLinks": ["gcp-compute-instance-group-manager"], + "descriptiveName": "GCP Compute Autoscaler", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Autoscaler by \"gcp-compute-autoscaler-name\"", + "list": true, + "listDescription": "List all GCP Compute Autoscaler items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_autoscaler.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json new file mode 100644 index 00000000..c103843e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-backend-service.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-backend-service", + "category": 1, + "potentialLinks": ["gcp-compute-network", "gcp-compute-security-policy"], + "descriptiveName": "GCP Compute Backend Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Backend Service by \"gcp-compute-backend-service-name\"", + "list": true, + "listDescription": "List all GCP Compute Backend Service items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_backend_service.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json new file mode 100644 index 00000000..435b2c8c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-compute-disk", + "category": 2, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-image", + "gcp-compute-instance", + "gcp-compute-instant-snapshot", + "gcp-compute-snapshot" + ], + "descriptiveName": "GCP Compute Disk", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Disk by \"gcp-compute-disk-name\"", + "list": true, + "listDescription": "List all GCP Compute Disk items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_disk.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json new file mode 100644 index 00000000..2ee555ba --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-external-vpn-gateway.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-external-vpn-gateway", + "category": 3, + "descriptiveName": "GCP Compute External Vpn Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-external-vpn-gateway by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-external-vpn-gateway" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_external_vpn_gateway.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json new file mode 100644 index 00000000..6dd11c29 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-firewall", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-iam-service-account"], + "descriptiveName": "GCP Compute Firewall", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-firewall by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-firewall" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_firewall.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json new file mode 100644 index 00000000..bd2ce7de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-forwarding-rule.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-forwarding-rule", + "category": 3, + "potentialLinks": [ + "gcp-compute-backend-service", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Forwarding Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Forwarding Rule by \"gcp-compute-forwarding-rule-name\"", + "list": true, + "listDescription": "List all GCP Compute Forwarding Rule items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_forwarding_rule.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json new file mode 100644 index 00000000..84db32de --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-address.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-global-address", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Global Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-global-address by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-global-address" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_global_address.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json new file mode 100644 index 00000000..28c37fc0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-global-forwarding-rule.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-global-forwarding-rule", + "category": 3, + "potentialLinks": [ + "gcp-compute-backend-service", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Global Forwarding Rule", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-global-forwarding-rule by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-global-forwarding-rule" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_global_forwarding_rule.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json new file mode 100644 index 00000000..beda2c95 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-health-check.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-health-check", + "category": 7, + "descriptiveName": "GCP Compute Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Health Check by \"gcp-compute-health-check-name\"", + "list": true, + "listDescription": "List all GCP Compute Health Check items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_health_check.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json new file mode 100644 index 00000000..f5d21e72 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-http-health-check.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-http-health-check", + "category": 3, + "descriptiveName": "GCP Compute Http Health Check", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-http-health-check by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-http-health-check" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_http_health_check.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json new file mode 100644 index 00000000..acf4c7a2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-image", + "category": 1, + "descriptiveName": "GCP Compute Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Image by \"gcp-compute-image-name\"", + "list": true, + "listDescription": "List all GCP Compute Image items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json new file mode 100644 index 00000000..bc80be40 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group-manager.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-instance-group-manager", + "category": 1, + "potentialLinks": [ + "gcp-compute-autoscaler", + "gcp-compute-instance-group", + "gcp-compute-instance-template", + "gcp-compute-target-pool" + ], + "descriptiveName": "GCP Compute Instance Group Manager", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance Group Manager by \"gcp-compute-instance-group-manager-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance Group Manager items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_group_manager.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json new file mode 100644 index 00000000..0368fa88 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-group.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-instance-group", + "category": 1, + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "descriptiveName": "GCP Compute Instance Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance Group by \"gcp-compute-instance-group-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance Group items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json new file mode 100644 index 00000000..b56bff5c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance-template.json @@ -0,0 +1,29 @@ +{ + "type": "gcp-compute-instance-template", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-disk", + "gcp-compute-image", + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-reservation", + "gcp-compute-security-policy", + "gcp-compute-snapshot", + "gcp-compute-subnetwork", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Compute Instance Template", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-instance-template by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-instance-template" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance_template.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json new file mode 100644 index 00000000..b4700f94 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-instance", + "category": 1, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instance by \"gcp-compute-instance-name\"", + "list": true, + "listDescription": "List all GCP Compute Instance items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json new file mode 100644 index 00000000..a37e8bd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instant-snapshot.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-instant-snapshot", + "category": 2, + "potentialLinks": ["gcp-compute-disk"], + "descriptiveName": "GCP Compute Instant Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Instant Snapshot by \"gcp-compute-instant-snapshot-name\"", + "list": true, + "listDescription": "List all GCP Compute Instant Snapshot items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_instant_snapshot.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json new file mode 100644 index 00000000..72970664 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-machine-image.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-machine-image", + "category": 1, + "potentialLinks": [ + "gcp-compute-disk", + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-subnetwork" + ], + "descriptiveName": "GCP Compute Machine Image", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Machine Image by \"gcp-compute-machine-image-name\"", + "list": true, + "listDescription": "List all GCP Compute Machine Image items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_machine_image.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json new file mode 100644 index 00000000..cf4fa2ae --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network-endpoint-group.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-network-endpoint-group", + "category": 3, + "potentialLinks": [ + "gcp-cloud-functions-function", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-run-service" + ], + "descriptiveName": "GCP Compute Network Endpoint Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-network-endpoint-group by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-network-endpoint-group" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_network_endpoint_group.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json new file mode 100644 index 00000000..f4e5d730 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-network", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "descriptiveName": "GCP Compute Network", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-network by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-network" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_network.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json new file mode 100644 index 00000000..73f3c8c0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-group.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-compute-node-group", + "category": 1, + "descriptiveName": "GCP Compute Node Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Node Group by \"gcp-compute-node-group-name\"", + "list": true, + "listDescription": "List all GCP Compute Node Group items", + "search": true, + "searchDescription": "Search for GCP Compute Node Group by \"gcp-compute-node-group-nodeTemplateName\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_node_group.name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_node_template.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json new file mode 100644 index 00000000..b6c4fd2b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json @@ -0,0 +1,10 @@ +{ + "type": "gcp-compute-project", + "category": 7, + "potentialLinks": ["gcp-iam-service-account", "gcp-storage-bucket"], + "descriptiveName": "GCP Compute Project", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-project by its \"name\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json new file mode 100644 index 00000000..7348176c --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-public-delegated-prefix.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-compute-public-delegated-prefix", + "category": 3, + "potentialLinks": [ + "gcp-cloud-resource-manager-project", + "gcp-compute-public-delegated-prefix" + ], + "descriptiveName": "GCP Compute Public Delegated Prefix", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-public-delegated-prefix by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-public-delegated-prefix", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/publicDelegatedPrefixes/[name] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_public_delegated_prefix.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json new file mode 100644 index 00000000..81449263 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-region-backend-service", + "category": 1, + "potentialLinks": [ + "gcp-compute-instance-group", + "gcp-compute-network", + "gcp-compute-security-policy" + ], + "descriptiveName": "GCP Compute Region Backend Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Region Backend Service by \"gcp-compute-region-backend-service-name\"", + "list": true, + "listDescription": "List all GCP Compute Region Backend Service items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_region_backend_service.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json new file mode 100644 index 00000000..56244bf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-commitment.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-region-commitment", + "potentialLinks": ["gcp-compute-reservation"], + "descriptiveName": "GCP Compute Region Commitment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-region-commitment by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-region-commitment" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_region_commitment.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json new file mode 100644 index 00000000..6ea4f52b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-compute-reservation", + "category": 1, + "potentialLinks": [ + "gcp-compute-region-commitment" + ], + "descriptiveName": "GCP Compute Reservation", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Reservation by \"gcp-compute-reservation-name\"", + "list": true, + "listDescription": "List all GCP Compute Reservation items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_reservation.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json new file mode 100644 index 00000000..5a2ebefc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-route", + "category": 3, + "potentialLinks": [ + "gcp-compute-instance", + "gcp-compute-network", + "gcp-compute-vpn-tunnel" + ], + "descriptiveName": "GCP Compute Route", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-route by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-route" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_route.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json new file mode 100644 index 00000000..525bec37 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-compute-router", + "category": 3, + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-compute-vpn-tunnel" + ], + "descriptiveName": "GCP Compute Router", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-router by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-router", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/routers/[router] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_router.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json new file mode 100644 index 00000000..b2a5b7e5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-security-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-security-policy", + "category": 4, + "descriptiveName": "GCP Compute Security Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Security Policy by \"gcp-compute-security-policy-name\"", + "list": true, + "listDescription": "List all GCP Compute Security Policy items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_security_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json new file mode 100644 index 00000000..5b42d808 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-snapshot", + "category": 2, + "potentialLinks": ["gcp-compute-disk", "gcp-compute-instant-snapshot"], + "descriptiveName": "GCP Compute Snapshot", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Snapshot by \"gcp-compute-snapshot-name\"", + "list": true, + "listDescription": "List all GCP Compute Snapshot items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_snapshot.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json new file mode 100644 index 00000000..d2b248fc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-certificate.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-ssl-certificate", + "category": 7, + "descriptiveName": "GCP Compute Ssl Certificate", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-ssl-certificate by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-ssl-certificate" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ssl_certificate.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json new file mode 100644 index 00000000..37962175 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-ssl-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-compute-ssl-policy", + "category": 4, + "descriptiveName": "GCP Compute Ssl Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-ssl-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-ssl-policy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ssl_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json new file mode 100644 index 00000000..2af0ff8e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-subnetwork", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Subnetwork", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-subnetwork by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-subnetwork" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_subnetwork.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json new file mode 100644 index 00000000..866c2fe2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-http-proxy.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-target-http-proxy", + "category": 3, + "potentialLinks": ["gcp-compute-url-map"], + "descriptiveName": "GCP Compute Target Http Proxy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-http-proxy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-http-proxy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_target_http_proxy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json new file mode 100644 index 00000000..329ff140 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-https-proxy.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-target-https-proxy", + "category": 3, + "potentialLinks": [ + "gcp-compute-ssl-certificate", + "gcp-compute-ssl-policy", + "gcp-compute-url-map" + ], + "descriptiveName": "GCP Compute Target Https Proxy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-https-proxy by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-https-proxy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_target_https_proxy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json new file mode 100644 index 00000000..d4af55ad --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-target-pool.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-compute-target-pool", + "category": 3, + "potentialLinks": [ + "gcp-compute-health-check", + "gcp-compute-instance", + "gcp-compute-target-pool" + ], + "descriptiveName": "GCP Compute Target Pool", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-target-pool by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-target-pool", + "search": true, + "searchDescription": "Search with full ID: projects/[project]/regions/[region]/targetPools/[name] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_compute_target_pool.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json new file mode 100644 index 00000000..e5ae1cc4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-url-map.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-url-map", + "category": 3, + "potentialLinks": ["gcp-compute-backend-service"], + "descriptiveName": "GCP Compute Url Map", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-url-map by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-url-map" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_url_map.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json new file mode 100644 index 00000000..29de0dae --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-gateway.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-compute-vpn-gateway", + "category": 3, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Compute Vpn Gateway", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-vpn-gateway by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-vpn-gateway" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_ha_vpn_gateway.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json new file mode 100644 index 00000000..10d8198b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-vpn-tunnel.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-compute-vpn-tunnel", + "category": 3, + "potentialLinks": [ + "gcp-compute-external-vpn-gateway", + "gcp-compute-router", + "gcp-compute-vpn-gateway" + ], + "descriptiveName": "GCP Compute Vpn Tunnel", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-compute-vpn-tunnel by its \"name\"", + "list": true, + "listDescription": "List all gcp-compute-vpn-tunnel" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_vpn_tunnel.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json new file mode 100644 index 00000000..75607613 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json @@ -0,0 +1,26 @@ +{ + "type": "gcp-container-cluster", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-subnetwork", + "gcp-container-node-pool", + "gcp-iam-service-account", + "gcp-pub-sub-topic" + ], + "descriptiveName": "GCP Container Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-container-cluster by its \"locations|clusters\"", + "search": true, + "searchDescription": "Search for GKE clusters in a location. Use the format \"location\" or the full resource name supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_container_cluster.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json new file mode 100644 index 00000000..d14c9319 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-node-pool.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-container-node-pool", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-node-group", + "gcp-container-cluster", + "gcp-iam-service-account" + ], + "descriptiveName": "GCP Container Node Pool", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-container-node-pool by its \"locations|clusters|nodePools\"", + "search": true, + "searchDescription": "Search GKE Node Pools within a cluster. Use \"[location]|[cluster]\" or the full resource name supported by Terraform mappings: \"[project]/[location]/[cluster]/[node_pool_name]\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_container_node_pool.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json new file mode 100644 index 00000000..0513d8c6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-dataform-repository", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-secret-manager-secret" + ], + "descriptiveName": "GCP Dataform Repository", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataform-repository by its \"locations|repositories\"", + "search": true, + "searchDescription": "Search for Dataform repositories in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/repositories/[repository_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataform_repository.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json new file mode 100644 index 00000000..9d600015 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-aspect-type.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dataplex-aspect-type", + "category": 7, + "descriptiveName": "GCP Dataplex Aspect Type", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-aspect-type by its \"locations|aspectTypes\"", + "search": true, + "searchDescription": "Search for Dataplex aspect types in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/aspectTypes/[aspect_type_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_aspect_type.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json new file mode 100644 index 00000000..99ae75d0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-data-scan.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-dataplex-data-scan", + "category": 5, + "potentialLinks": ["gcp-storage-bucket"], + "descriptiveName": "GCP Dataplex Data Scan", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-data-scan by its \"locations|dataScans\"", + "search": true, + "searchDescription": "Search for Dataplex data scans in a location. Use the location name e.g., 'us-central1' or the format \"projects/[project_id]/locations/[location]/dataScans/[data_scan_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_datascan.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json new file mode 100644 index 00000000..22f88da0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataplex-entry-group.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dataplex-entry-group", + "category": 2, + "descriptiveName": "GCP Dataplex Entry Group", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataplex-entry-group by its \"locations|entryGroups\"", + "search": true, + "searchDescription": "Search for Dataplex entry groups in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/entryGroups/[entry_group_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_dataplex_entry_group.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json new file mode 100644 index 00000000..05e71e51 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-autoscaling-policy.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-dataproc-autoscaling-policy", + "category": 7, + "descriptiveName": "GCP Dataproc Autoscaling Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataproc-autoscaling-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-dataproc-autoscaling-policy" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dataproc_autoscaling_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json new file mode 100644 index 00000000..d1f19d26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json @@ -0,0 +1,27 @@ +{ + "type": "gcp-dataproc-cluster", + "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-image", + "gcp-compute-instance-group-manager", + "gcp-compute-network", + "gcp-compute-node-group", + "gcp-compute-subnetwork", + "gcp-dataproc-autoscaling-policy", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Dataproc Cluster", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dataproc-cluster by its \"name\"", + "list": true, + "listDescription": "List all gcp-dataproc-cluster" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dataproc_cluster.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json new file mode 100644 index 00000000..54e61a26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dns-managed-zone.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-dns-managed-zone", + "category": 3, + "potentialLinks": ["gcp-compute-network", "gcp-container-cluster"], + "descriptiveName": "GCP Dns Managed Zone", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-dns-managed-zone by its \"name\"", + "list": true, + "listDescription": "List all gcp-dns-managed-zone" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_dns_managed_zone.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json new file mode 100644 index 00000000..da7a9ff2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-essential-contacts-contact.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-essential-contacts-contact", + "descriptiveName": "GCP Essential Contacts Contact", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-essential-contacts-contact by its \"name\"", + "list": true, + "listDescription": "List all gcp-essential-contacts-contact", + "search": true, + "searchDescription": "Search for contacts by their ID in the form of \"projects/[project_id]/contacts/[contact_id]\"." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_essential_contacts_contact.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json new file mode 100644 index 00000000..1a538cc3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-file-instance", + "category": 2, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-compute-network"], + "descriptiveName": "GCP File Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-file-instance by its \"locations|instances\"", + "search": true, + "searchDescription": "Search for Filestore instances in a location. Use the location string or the full resource name supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_filestore_instance.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json new file mode 100644 index 00000000..1f4396c5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-iam-role", + "category": 4, + "descriptiveName": "GCP Iam Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-iam-role by its \"name\"", + "list": true, + "listDescription": "List all gcp-iam-role" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json new file mode 100644 index 00000000..07a30c6a --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account-key.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-iam-service-account-key", + "category": 4, + "potentialLinks": ["gcp-iam-service-account"], + "descriptiveName": "GCP Iam Service Account Key", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Iam Service Account Key by \"gcp-iam-service-account-email or unique_id|gcp-iam-service-account-key-name\"", + "search": true, + "searchDescription": "Search for GCP Iam Service Account Key by \"gcp-iam-service-account-email or unique_id\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_service_account_key.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json new file mode 100644 index 00000000..f4e8078e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-service-account.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-iam-service-account", + "category": 4, + "potentialLinks": [ + "gcp-cloud-resource-manager-project", + "gcp-iam-service-account-key" + ], + "descriptiveName": "GCP Iam Service Account", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Iam Service Account by \"gcp-iam-service-account-email or unique_id\"", + "list": true, + "listDescription": "List all GCP Iam Service Account items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_service_account.email" + }, + { + "terraformQueryMap": "google_service_account.unique_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json new file mode 100644 index 00000000..e63a32f8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-logging-bucket", + "category": 5, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-iam-service-account"], + "descriptiveName": "GCP Logging Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-bucket by its \"locations|buckets\"", + "search": true, + "searchDescription": "Search for gcp-logging-bucket by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json new file mode 100644 index 00000000..319a71a0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-logging-link", + "category": 5, + "potentialLinks": ["gcp-big-query-dataset", "gcp-logging-bucket"], + "descriptiveName": "GCP Logging Link", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-link by its \"locations|buckets|links\"", + "search": true, + "searchDescription": "Search for gcp-logging-link by its \"locations|buckets\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json new file mode 100644 index 00000000..f68dd359 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-saved-query.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-logging-saved-query", + "category": 5, + "descriptiveName": "GCP Logging Saved Query", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-logging-saved-query by its \"locations|savedQueries\"", + "search": true, + "searchDescription": "Search for gcp-logging-saved-query by its \"locations\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json new file mode 100644 index 00000000..013bcc3b --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-logging-sink", + "category": 7, + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-logging-bucket", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Logging Sink", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Logging Sink by \"gcp-logging-sink-name\"", + "list": true, + "listDescription": "List all GCP Logging Sink items" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json new file mode 100644 index 00000000..89297235 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-alert-policy.json @@ -0,0 +1,20 @@ +{ + "type": "gcp-monitoring-alert-policy", + "category": 5, + "potentialLinks": ["gcp-monitoring-notification-channel"], + "descriptiveName": "GCP Monitoring Alert Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-alert-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-alert-policy", + "search": true, + "searchDescription": "Search by full resource name: projects/[project]/alertPolicies/[alert_policy_id] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_monitoring_alert_policy.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json new file mode 100644 index 00000000..5dcb1ef2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-custom-dashboard.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-monitoring-custom-dashboard", + "category": 5, + "descriptiveName": "GCP Monitoring Custom Dashboard", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-custom-dashboard by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-custom-dashboard", + "search": true, + "searchDescription": "Search for custom dashboards by their ID in the form of \"projects/[project_id]/dashboards/[dashboard_id]\". This is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_monitoring_dashboard.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json new file mode 100644 index 00000000..9da3e095 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-monitoring-notification-channel.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-monitoring-notification-channel", + "category": 5, + "descriptiveName": "GCP Monitoring Notification Channel", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-monitoring-notification-channel by its \"name\"", + "list": true, + "listDescription": "List all gcp-monitoring-notification-channel", + "search": true, + "searchDescription": "Search by full resource name: projects/[project]/notificationChannels/[notificationChannel] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_monitoring_notification_channel.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json new file mode 100644 index 00000000..0126fcb8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-orgpolicy-policy", + "category": 7, + "descriptiveName": "GCP Orgpolicy Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-orgpolicy-policy by its \"name\"", + "list": true, + "listDescription": "List all gcp-orgpolicy-policy", + "search": true, + "searchDescription": "Search with the full policy name: projects/[project]/policies/[constraint] (used for terraform mapping)." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_org_policy_policy.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json new file mode 100644 index 00000000..35abc174 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-subscription.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-pub-sub-subscription", + "category": 7, + "potentialLinks": [ + "gcp-big-query-table", + "gcp-pub-sub-subscription", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Pub Sub Subscription", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-pub-sub-subscription by its \"name\"", + "list": true, + "listDescription": "List all gcp-pub-sub-subscription" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_pubsub_subscription.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json new file mode 100644 index 00000000..7f8a34b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-pub-sub-topic.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-pub-sub-topic", + "category": 7, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-storage-bucket"], + "descriptiveName": "GCP Pub Sub Topic", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-pub-sub-topic by its \"name\"", + "list": true, + "listDescription": "List all gcp-pub-sub-topic" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_pubsub_topic.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json new file mode 100644 index 00000000..96b1fcd6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-redis-instance", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-ssl-certificate" + ], + "descriptiveName": "GCP Redis Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-redis-instance by its \"locations|instances\"", + "search": true, + "searchDescription": "Search Redis instances in a location. Use the format \"location\" or \"projects/[project_id]/locations/[location]/instances/[instance_name]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_redis_instance.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json new file mode 100644 index 00000000..44d783b7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-run-revision", + "category": 7, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-iam-service-account", + "gcp-run-service", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Run Revision", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-run-revision by its \"locations|services|revisions\"", + "search": true, + "searchDescription": "Search for gcp-run-revision by its \"locations|services\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json new file mode 100644 index 00000000..74851f26 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json @@ -0,0 +1,28 @@ +{ + "type": "gcp-run-service", + "category": 1, + "potentialLinks": [ + "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-compute-subnetwork", + "gcp-iam-service-account", + "gcp-run-revision", + "gcp-secret-manager-secret", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Run Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-run-service by its \"locations|services\"", + "search": true, + "searchDescription": "Search for gcp-run-service by its \"locations\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_cloud_run_v2_service.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json new file mode 100644 index 00000000..c85bf1d8 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-secret-manager-secret.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-secret-manager-secret", + "category": 4, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-pub-sub-topic"], + "descriptiveName": "GCP Secret Manager Secret", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-secret-manager-secret by its \"name\"", + "list": true, + "listDescription": "List all gcp-secret-manager-secret" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_secret_manager_secret.secret_id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json new file mode 100644 index 00000000..f5c6408f --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-security-center-management-security-center-service.json @@ -0,0 +1,11 @@ +{ + "type": "gcp-security-center-management-security-center-service", + "category": 4, + "descriptiveName": "GCP Security Center Management Security Center Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-security-center-management-security-center-service by its \"locations|securityCenterServices\"", + "search": true, + "searchDescription": "Search Security Center services in a location. Use the format \"location\"." + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json new file mode 100644 index 00000000..83acd69d --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-directory-endpoint.json @@ -0,0 +1,18 @@ +{ + "type": "gcp-service-directory-endpoint", + "category": 7, + "potentialLinks": ["gcp-compute-network"], + "descriptiveName": "GCP Service Directory Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-service-directory-endpoint by its \"locations|namespaces|services|endpoints\"", + "search": true, + "searchDescription": "Search for endpoints by \"location|namespace_id|service_id\" or \"projects/[project_id]/locations/[location]/namespaces/[namespace_id]/services/[service_id]/endpoints/[endpoint_id]\" which is supported for terraform mappings." + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_service_directory_endpoint.id" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json new file mode 100644 index 00000000..8c15f219 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-service-usage-service.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-service-usage-service", + "category": 7, + "potentialLinks": ["gcp-pub-sub-topic"], + "descriptiveName": "GCP Service Usage Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-service-usage-service by its \"name\"", + "list": true, + "listDescription": "List all gcp-service-usage-service" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json new file mode 100644 index 00000000..f333a2dc --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-spanner-database", + "category": 6, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-spanner-instance"], + "descriptiveName": "GCP Spanner Database", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-spanner-database by its \"instances|databases\"", + "search": true, + "searchDescription": "Search for gcp-spanner-database by its \"instances\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_spanner_database.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json new file mode 100644 index 00000000..b3193800 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-spanner-instance", + "category": 6, + "potentialLinks": ["gcp-spanner-database"], + "descriptiveName": "GCP Spanner Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-spanner-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-spanner-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_spanner_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json new file mode 100644 index 00000000..1071747e --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup-run.json @@ -0,0 +1,12 @@ +{ + "type": "gcp-sql-admin-backup-run", + "category": 6, + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-sql-admin-instance"], + "descriptiveName": "GCP Sql Admin Backup Run", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-backup-run by its \"instances|backupRuns\"", + "search": true, + "searchDescription": "Search for gcp-sql-admin-backup-run by its \"instances\"" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json new file mode 100644 index 00000000..474f8076 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-backup.json @@ -0,0 +1,16 @@ +{ + "type": "gcp-sql-admin-backup", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-sql-admin-backup-run", + "gcp-sql-admin-instance" + ], + "descriptiveName": "GCP Sql Admin Backup", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-backup by its \"name\"", + "list": true, + "listDescription": "List all gcp-sql-admin-backup" + } +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json new file mode 100644 index 00000000..16d72392 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-sql-admin-instance.json @@ -0,0 +1,24 @@ +{ + "type": "gcp-sql-admin-instance", + "category": 6, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-iam-service-account", + "gcp-sql-admin-backup-run", + "gcp-sql-admin-instance", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Sql Admin Instance", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-sql-admin-instance by its \"name\"", + "list": true, + "listDescription": "List all gcp-sql-admin-instance" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_sql_database_instance.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json new file mode 100644 index 00000000..95730649 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json @@ -0,0 +1,21 @@ +{ + "type": "gcp-storage-bucket", + "category": 2, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network", + "gcp-logging-bucket" + ], + "descriptiveName": "GCP Storage Bucket", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-storage-bucket by its \"name\"", + "list": true, + "listDescription": "List all gcp-storage-bucket" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_storage_bucket.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json new file mode 100644 index 00000000..2b3bf896 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-transfer-transfer-job.json @@ -0,0 +1,22 @@ +{ + "type": "gcp-storage-transfer-transfer-job", + "category": 2, + "potentialLinks": [ + "gcp-iam-service-account", + "gcp-pub-sub-subscription", + "gcp-pub-sub-topic", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Storage Transfer Transfer Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a gcp-storage-transfer-transfer-job by its \"name\"", + "list": true, + "listDescription": "List all gcp-storage-transfer-transfer-job" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_storage_transfer_job.name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md new file mode 100644 index 00000000..cccc0bbc --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRole.md @@ -0,0 +1,17 @@ +--- +title: Cluster Role +sidebar_label: ClusterRole +--- + +A ClusterRole is a non-namespaced Kubernetes RBAC resource that groups together one or more policy rules, defining which verbs (such as `get`, `list`, `create`, `delete`) are allowed on which resources across the entire cluster. Because it is cluster-scoped, the permissions it grants apply to all namespaces. It can be referenced by a `RoleBinding` (to limit its scope to a single namespace) or by a `ClusterRoleBinding` (to apply it cluster-wide) and is commonly used to grant system-level or cross-namespace permissions to users, service accounts or other principals. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#clusterrole + +**Terrafrom Mappings:** + +- `kubernetes_cluster_role_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cluster Role by name +- `LIST`: List all Cluster Roles +- `SEARCH`: Search for a Cluster Role using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md new file mode 100644 index 00000000..946b9239 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ClusterRoleBinding.md @@ -0,0 +1,28 @@ +--- +title: Cluster Role Binding +sidebar_label: ClusterRoleBinding +--- + +A ClusterRoleBinding grants the permissions defined in a `ClusterRole` to one or more subjects (users, groups, or ServiceAccounts) across the entire Kubernetes cluster. Whereas a `RoleBinding` is namespace-scoped, a ClusterRoleBinding has cluster-wide effect, making it a critical component of RBAC configuration. +For further details, see the Kubernetes documentation: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding + +**Terrafrom Mappings:** + +- `kubernetes_cluster_role_binding_v1.metadata[0].name` +- `kubernetes_cluster_role_binding.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cluster Role Binding by name +- `LIST`: List all Cluster Role Bindings +- `SEARCH`: Search for a Cluster Role Binding using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ClusterRole`](/sources/k8s/Types/ClusterRole) + +The ClusterRoleBinding’s `roleRef` field points to the name of a `ClusterRole`. Overmind represents this relationship so you can trace which set of permissions (rules) is being granted cluster-wide. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +If a ClusterRoleBinding contains one or more ServiceAccounts in its `subjects` array, Overmind links the binding to those ServiceAccounts, allowing you to see exactly which workload identities receive the referenced cluster-level permissions. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md b/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md new file mode 100644 index 00000000..5dd4e332 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ConfigMap.md @@ -0,0 +1,17 @@ +--- +title: Config Map +sidebar_label: ConfigMap +--- + +A ConfigMap is a Kubernetes API object used to store non-confidential configuration data in key-value pairs. It allows you to decouple environment-specific configuration from your container images so that the same image can be reused in different environments with different settings. Pods and other Kubernetes workloads can consume the data held in a ConfigMap as environment variables, command-line arguments or configuration files mounted into a volume. For an in-depth overview, see the official documentation: https://kubernetes.io/docs/concepts/configuration/configmap/ + +**Terrafrom Mappings:** + +- `kubernetes_config_map_v1.metadata[0].name` +- `kubernetes_config_map.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Config Map by name +- `LIST`: List all Config Maps +- `SEARCH`: Search for a Config Map using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md b/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md new file mode 100644 index 00000000..7dc0e2d2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/CronJob.md @@ -0,0 +1,17 @@ +--- +title: Cron Job +sidebar_label: CronJob +--- + +A Kubernetes **CronJob** is a higher-level controller responsible for running a Job object on a repeating schedule, expressed in standard _cron_ syntax. It is typically used for routine, time-based tasks such as database backups, report generation, and regular housekeeping activities inside a cluster. The controller automatically creates the underlying Job at the scheduled time, monitors its execution and, depending on the configuration, retains or cleans up finished Jobs and their Pods. For a full description of the resource’s behaviour and available fields, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ + +**Terrafrom Mappings:** + +- `kubernetes_cron_job_v1.metadata[0].name` +- `kubernetes_cron_job.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Cron Job by name +- `LIST`: List all Cron Jobs +- `SEARCH`: Search for a Cron Job using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md b/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md new file mode 100644 index 00000000..b04661a4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/DaemonSet.md @@ -0,0 +1,18 @@ +--- +title: Daemon Set +sidebar_label: DaemonSet +--- + +A Kubernetes **DaemonSet** ensures that a copy of a specified Pod is running on every (or a selected subset of) node(s) in the cluster. It is commonly used for cluster-wide services such as log collectors, monitoring agents, or network proxies that must be present on each node. When nodes are added to the cluster, the DaemonSet automatically schedules the Pod on the new nodes; when nodes are removed, the Pods are garbage-collected. +For a full description, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ + +**Terrafrom Mappings:** + +- `kubernetes_daemon_set_v1.metadata[0].name` +- `kubernetes_daemonset.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Daemon Set by name +- `LIST`: List all Daemon Sets +- `SEARCH`: Search for a Daemon Set using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md b/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md new file mode 100644 index 00000000..a11e2ab6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Deployment.md @@ -0,0 +1,24 @@ +--- +title: Deployment +sidebar_label: Deployment +--- + +A Deployment is a higher-level Kubernetes workload resource that declaratively manages a set of identical Pods by creating and maintaining the appropriate number of ReplicaSets. With a Deployment you describe the desired state—such as how many replicas should be running or which Pod template to use—and the Kubernetes control plane continually works to bring the actual state in line with that specification. Deployments support rolling updates, rollbacks, and pausing/resuming of updates, making them the most common mechanism for managing stateless applications on Kubernetes clusters. +For the complete specification see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ + +**Terrafrom Mappings:** + +- `kubernetes_deployment_v1.metadata[0].name` +- `kubernetes_deployment.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Deployment by name +- `LIST`: List all Deployments +- `SEARCH`: Search for a Deployment using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ReplicaSet`](/sources/k8s/Types/ReplicaSet) + +Each Deployment automatically creates and owns one or more ReplicaSets. The ReplicaSet is responsible for keeping the specified number of Pod replicas running, while the Deployment supervises the ReplicaSets, deciding when to create new ones or scale them to facilitate updates or rollbacks. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md new file mode 100644 index 00000000..c8322f36 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md @@ -0,0 +1,36 @@ +--- +title: Endpoint Slice +sidebar_label: EndpointSlice +--- + +EndpointSlices provide a scalable and extensible way of tracking network endpoints that back a Kubernetes Service. Each slice contains a list of IP addresses and ports together with optional topology information such as the Node on which each endpoint is running. EndpointSlices replace the legacy Endpoints object for large clusters and are automatically created and managed by the control plane when a Service is defined. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/ + +**Terrafrom Mappings:** + +- `kubernetes_endpoints_slice_v1.metadata[0].name` +- `kubernetes_endpoints_slice.metadata[0].name` + +## Supported Methods + +- `GET`: Get a EndpointSlice by name +- `LIST`: List all EndpointSlices +- `SEARCH`: Search for a EndpointSlice using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Node`](/sources/k8s/Types/Node) + +Each endpoint within an EndpointSlice may include a `nodeName` or topology label indicating the Node that hosts the backing Pod. Overmind links the slice to those Nodes so you can see which machines will receive traffic for the Service. + +### [`Pod`](/sources/k8s/Types/Pod) + +Endpoints usually correspond to Pod IPs. By linking EndpointSlices to the underlying Pods, Overmind allows you to trace from a Service to the exact workloads that will handle requests. + +### [`dns`](/sources/stdlib/Types/dns) + +When Kubernetes populates cluster DNS (e.g. `my-service.my-namespace.svc.cluster.local`) it ultimately resolves to the addresses listed in the Service’s EndpointSlices. Linking to DNS records shows how a name queried by applications maps to concrete endpoints. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +EndpointSlices store one or more IPv4/IPv6 addresses for each endpoint. These addresses are linked so that you can follow a path from a Service to the raw IPs that will be contacted, helping to assess network-level reachability and risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md b/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md new file mode 100644 index 00000000..34e90580 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Endpoints.md @@ -0,0 +1,31 @@ +--- +title: Endpoints +sidebar_label: Endpoints +--- + +An Endpoint in Kubernetes represents the network locations (IP address + port) that actually implement a Service. While a Service is an abstract front-end, the corresponding Endpoints object keeps the ever-changing list of Pods that are ready to receive traffic. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/services-networking/service/#endpoints + +**Terrafrom Mappings:** + +- `kubernetes_endpoints.metadata[0].name` +- `kubernetes_endpoints_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Endpoints by name +- `LIST`: List all Endpointss +- `SEARCH`: Search for a Endpoints using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Node`](/sources/k8s/Types/Node) + +Each endpoint address can include a `nodeName` field indicating the Node on which the backing Pod is running. Overmind therefore links the Endpoints object to the Node(s) that currently host its backing Pods, helping you understand on which worker machines traffic will land. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Every endpoint entry exposes an IP address. Overmind extracts these IPs and links them, allowing you to trace the path from the abstract Service through the Endpoint to the concrete network address that will receive traffic. + +### [`Pod`](/sources/k8s/Types/Pod) + +Endpoint addresses typically contain a `targetRef` that points to the Pod providing the Service. Overmind links the Endpoints object to these Pods so you can quickly inspect the health, labels, and configuration of the workloads currently registered behind the Service. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md b/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md new file mode 100644 index 00000000..393d4521 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/HorizontalPodAutoscaler.md @@ -0,0 +1,16 @@ +--- +title: Horizontal Pod Autoscaler +sidebar_label: HorizontalPodAutoscaler +--- + +The Horizontal Pod Autoscaler (HPA) is a native Kubernetes controller that automatically increases or decreases the number of running Pods in a Deployment, ReplicaSet, StatefulSet, or other scalable resource so that observed resource consumption stays close to a user-defined target. It polls the Kubernetes Metrics Server (or a custom/external metrics API) at a regular interval, compares CPU, memory, or arbitrary custom metrics against the specified thresholds, and then adjusts the `spec.replicas` field of the target workload accordingly. This enables applications to meet fluctuating demand without manual intervention or unnecessary over-provisioning, while still preventing sudden traffic spikes from overwhelming the cluster. You can read the full upstream specification in the official Kubernetes documentation: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/. + +**Terrafrom Mappings:** + +- `kubernetes_horizontal_pod_autoscaler_v2.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Horizontal Pod Autoscaler by name +- `LIST`: List all Horizontal Pod Autoscalers +- `SEARCH`: Search for a Horizontal Pod Autoscaler using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md b/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md new file mode 100644 index 00000000..892c1746 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Ingress.md @@ -0,0 +1,27 @@ +--- +title: Ingress +sidebar_label: Ingress +--- + +An Ingress is a Kubernetes resource that manages external access to services within a cluster, typically HTTP and HTTPS traffic. It defines a set of routing rules that map incoming requests (based on hostnames and URL paths) to backend `Service` resources. By centralising traffic management, it allows fine-grained control over features such as virtual hosting, TLS termination and path-based routing without requiring each service to expose its own `Service` of type `LoadBalancer` or `NodePort`. +Official documentation: https://kubernetes.io/docs/concepts/services-networking/ingress/ + +**Terrafrom Mappings:** + +- `kubernetes_ingress_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get an Ingress by name +- `LIST`: List all Ingresses +- `SEARCH`: Search for an Ingress using the `ListOptions` JSON format, e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Service`](/sources/k8s/Types/Service) + +An Ingress routes external traffic to one or more backend `Service` objects. Each rule in the Ingress specification references a service name and port; therefore, Overmind links an Ingress to the `Service`(s) it targets so that you can trace how requests reach your application. + +### [`dns`](/sources/stdlib/Types/dns) + +The hostnames declared in an Ingress must be resolvable via DNS so that clients can reach the cluster’s ingress point. Overmind links these hostnames to their corresponding DNS records (A, AAAA or CNAME) to show whether the necessary records exist and to surface any misconfigurations. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Job.md b/docs.overmind.tech/docs/sources/k8s/Types/Job.md new file mode 100644 index 00000000..6917f44b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Job.md @@ -0,0 +1,23 @@ +--- +title: Job +sidebar_label: Job +--- + +A Kubernetes Job is a controller that runs one-off or batch tasks to completion. It creates one or more Pods and tracks their execution until the specified number have finished successfully. Jobs are ideal for database migrations, data processing, or any workload that needs to run to completion rather than persist indefinitely. A Job retries failed Pods according to its back-off policy and is marked as complete once all Pods exit successfully. For more details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/job/ + +**Terrafrom Mappings:** + +- `kubernetes_job.metadata[0].name` +- `kubernetes_job_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Job by name +- `LIST`: List all Jobs +- `SEARCH`: Search for a Job using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A Job spawns one or more Pods to run its workload; each Pod created by the Job is linked back to it via the Job’s `ownerReferences` metadata. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md b/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md new file mode 100644 index 00000000..0569cf33 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/LimitRange.md @@ -0,0 +1,17 @@ +--- +title: Limit Range +sidebar_label: LimitRange +--- + +A Kubernetes LimitRange is a namespace-level policy object that defines default, minimum, and maximum compute-resource constraints (such as CPU, memory, and ephemeral storage) that apply to Pods or individual Containers created in that namespace. By enforcing these boundaries, a LimitRange prevents a single workload from monopolising cluster resources and ensures that every workload has sensible defaults if the user omits explicit resource requests or limits. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/policy/limit-range/ + +**Terrafrom Mappings:** + +- `kubernetes_limit_range_v1.metadata[0].name` +- `kubernetes_limit_range.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Limit Range by name +- `LIST`: List all Limit Ranges +- `SEARCH`: Search for a Limit Range using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md b/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md new file mode 100644 index 00000000..d8270b02 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/NetworkPolicy.md @@ -0,0 +1,24 @@ +--- +title: Network Policy +sidebar_label: NetworkPolicy +--- + +A Kubernetes **NetworkPolicy** is a namespaced resource that controls how groups of Pods are allowed to communicate with each other and with other network endpoints. By defining ingress and/or egress rules that match Pods via label selectors, it provides fine-grained, declarative network segmentation inside the cluster, helping operators restrict unintended traffic and harden workloads. If no NetworkPolicy targets a Pod, that Pod is non-isolated and can both send and receive traffic to and from any source. +Official documentation: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + +**Terrafrom Mappings:** + +- `kubernetes_network_policy.metadata[0].name` +- `kubernetes_network_policy_v1.metadata[0].name` + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A NetworkPolicy selects one or more Pods (in the same namespace) through `podSelector` rules; therefore, each referenced Pod can be linked to the NetworkPolicy that governs its allowed ingress and egress traffic. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Node.md b/docs.overmind.tech/docs/sources/k8s/Types/Node.md new file mode 100644 index 00000000..0e85a370 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Node.md @@ -0,0 +1,30 @@ +--- +title: Node +sidebar_label: Node +--- + +A Kubernetes **Node** is a worker machine (virtual or physical) that runs the Pods making up a cluster’s workloads. Each Node contains the services necessary to run containers, including the container runtime, kubelet and kube-proxy, and is managed by the Kubernetes control plane. For more details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/architecture/nodes/ + +**Terrafrom Mappings:** + +- `kubernetes_node_taint.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Node by name +- `LIST`: List all Nodes +- `SEARCH`: Search for a Node using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +A Node is discoverable in the cluster via its DNS entry. Overmind links the Node to its corresponding DNS record(s) so you can trace how applications or services resolve to this worker machine. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Every Node advertises one or more internal and external IP addresses. Overmind establishes a link between the Node resource and these IP objects to surface network reachability or exposure risks. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +When Kubernetes is running on AWS, Nodes (EC2 instances) may have EBS volumes attached to provide persistent storage for Pods. Overmind links the Node to the `ec2-volume` resources it mounts, allowing you to evaluate storage-related blast radius or compliance concerns. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md new file mode 100644 index 00000000..e79d6569 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolume.md @@ -0,0 +1,32 @@ +--- +title: Persistent Volume +sidebar_label: PersistentVolume +--- + +A Kubernetes PersistentVolume (PV) is a cluster-wide object that represents a piece of storage that has been provisioned either statically by an administrator or dynamically via a StorageClass. Unlike ephemeral volumes that are tied to the lifetime of a Pod, a PV exists independently and can outlive any consumer Pods, enabling stateful workloads to retain data across rescheduling or restarts. Each PV encapsulates details such as capacity, access modes, reclaim policy and the specifics of the underlying storage medium (for example, AWS EBS, NFS, or a CSI-provisioned backend). +Official documentation: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ + +**Terrafrom Mappings:** + +- `kubernetes_persistent_volume.metadata[0].name` +- `kubernetes_persistent_volume_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PersistentVolume by name +- `LIST`: List all PersistentVolumes +- `SEARCH`: Search for a PersistentVolume using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +A PersistentVolume whose `spec.awsElasticBlockStore` (or CSI driver) references an AWS EBS disk ultimately maps to an EC2 volume. Overmind links the PV to the underlying `ec2-volume` so you can assess risks such as deletion protection, encryption status or capacity limits of the actual block device. + +### [`efs-access-point`](/sources/aws/Types/efs-access-point) + +When a PV is backed by Amazon EFS via the EFS CSI driver, it mounts the file system through a specific EFS Access Point. Linking the PV to the corresponding `efs-access-point` lets you trace permissions, throughput and network configurations that could affect the workload’s storage availability. + +### [`StorageClass`](/sources/k8s/Types/StorageClass) + +Most dynamically provisioned PVs include a `storageClassName` field that references the StorageClass used to create them. By linking to the `StorageClass`, Overmind shows the provisioning parameters, reclaim policy and allowed topologies that govern how this PV was created and how it behaves when released. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md new file mode 100644 index 00000000..c155e143 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PersistentVolumeClaim.md @@ -0,0 +1,24 @@ +--- +title: Persistent Volume Claim +sidebar_label: PersistentVolumeClaim +--- + +A PersistentVolumeClaim (PVC) in Kubernetes is a user-defined request for storage. Applications declare the amount of space, access mode and other requirements they need through a PVC, and Kubernetes finds (or waits for) a matching PersistentVolume (PV) to satisfy that request. Once bound, the PVC provides a stable, pod-agnostic handle for the underlying storage, meaning workloads can be rescheduled across nodes without losing data. +For a full explanation see the Kubernetes documentation: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + +**Terrafrom Mappings:** + +- `kubernetes_persistent_volume_claim.metadata[0].name` +- `kubernetes_persistent_volume_claim_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PersistentVolumeClaim by name +- `LIST`: List all PersistentVolumeClaims +- `SEARCH`: Search for a PersistentVolumeClaim using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`PersistentVolume`](/sources/k8s/Types/PersistentVolume) + +A PVC is bound to a PersistentVolume that satisfies its storage class, capacity and access-mode requirements. Overmind records this binding so that from a PVC you can quickly navigate to the backing PV and assess its characteristics and any associated risks. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Pod.md b/docs.overmind.tech/docs/sources/k8s/Types/Pod.md new file mode 100644 index 00000000..f0945bf7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Pod.md @@ -0,0 +1,51 @@ +--- +title: Pod +sidebar_label: Pod +--- + +A Kubernetes Pod is the smallest deployable unit in the Kubernetes object model. It represents one or more containers that share storage, network and a specification for how to run the containers. Pods are ephemeral and are usually created and managed by higher-level controllers such as Deployments or StatefulSets. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/concepts/workloads/pods/ + +**Terrafrom Mappings:** + +- `kubernetes_pod.metadata[0].name` +- `kubernetes_pod_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Pod by name +- `LIST`: List all Pods +- `SEARCH`: Search for a Pod using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`ConfigMap`](/sources/k8s/Types/ConfigMap) + +Pods can consume ConfigMaps as environment variables or mount them as files, allowing configuration data to be injected without rebuilding container images. + +### [`ec2-volume`](/sources/aws/Types/ec2-volume) + +When a Pod mounts a PersistentVolume backed by an AWS Elastic Block Store (EBS) volume, that underlying storage appears here as an `ec2-volume` link, connecting the workload to the physical disk resource in AWS. + +### [`dns`](/sources/stdlib/Types/dns) + +Each Pod receives an internal DNS entry (`..pod.cluster.local`) and may resolve or be resolved by other services; Overmind records this relationship so you can trace DNS dependencies. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +At runtime every Pod is assigned an IP address. This link surfaces the relationship between the Kubernetes object and the network IP resource managed by the underlying cloud networking layer. + +### [`PersistentVolumeClaim`](/sources/k8s/Types/PersistentVolumeClaim) + +Pods declare one or more PersistentVolumeClaims in their `volumes` section to obtain persistent storage. The link shows which claims are attached to the Pod. + +### [`PriorityClass`](/sources/k8s/Types/PriorityClass) + +A Pod may specify a `priorityClassName`; the associated PriorityClass influences scheduling order and pre-emption behaviour. This link ties the Pod to its scheduling priority. + +### [`Secret`](/sources/k8s/Types/Secret) + +Secrets can be mounted as files or injected as environment variables into a Pod, for example to provide credentials or TLS keys. This link identifies every Secret the Pod references. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +Each Pod runs under a ServiceAccount that defines its Kubernetes API permissions and, in many cases, its cloud IAM identity. The link shows the ServiceAccount used by the Pod. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md b/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md new file mode 100644 index 00000000..350f3102 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PodDisruptionBudget.md @@ -0,0 +1,23 @@ +--- +title: Pod Disruption Budget +sidebar_label: PodDisruptionBudget +--- + +A PodDisruptionBudget (PDB) is a Kubernetes policy object that limits the number of pods of a replicated application that can be unavailable during voluntary disruptions such as a node drain, cluster upgrade, or a user-initiated rolling update. By defining either a `minAvailable` or `maxUnavailable` threshold, it helps you maintain a desired level of service availability while still allowing the platform to carry out maintenance tasks. +See the official documentation for full details: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/ + +**Terrafrom Mappings:** + +- `kubernetes_pod_disruption_budget_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a PodDisruptionBudget by name +- `LIST`: List all PodDisruptionBudgets +- `SEARCH`: Search for a PodDisruptionBudget using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A PodDisruptionBudget references pods via a label selector defined in `spec.selector`. Any pod whose labels match this selector is governed by the PDB, meaning it counts towards the availability calculations and is protected from eviction when the defined disruption limits would be exceeded. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md b/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md new file mode 100644 index 00000000..51c473e4 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/PriorityClass.md @@ -0,0 +1,17 @@ +--- +title: Priority Class +sidebar_label: PriorityClass +--- + +A Kubernetes `PriorityClass` is a cluster-wide, non-namespaced resource that defines the relative importance of Pods. Each PriorityClass carries an integer value; the higher the value, the earlier the scheduler will try to place Pods that reference it. PriorityClasses are also used during pre-emption: when the cluster is under resource pressure, Pods with lower priority may be evicted in favour of higher-priority Pods. For full details, refer to the official Kubernetes documentation: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass + +**Terrafrom Mappings:** + +- `kubernetes_priority_class_v1.metadata[0].name` +- `kubernetes_priority_class.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Priority Class by name +- `LIST`: List all Priority Classs +- `SEARCH`: Search for a Priority Class using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md b/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md new file mode 100644 index 00000000..d5723a62 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ReplicaSet.md @@ -0,0 +1,19 @@ +--- +title: Replica Set +sidebar_label: ReplicaSet +--- + +A ReplicaSet is a Kubernetes controller whose purpose is to maintain a stable set of identical Pods running at any given time. By continuously watching the cluster state, it ensures that the desired number of Pod replicas are present: if one is deleted or becomes unhealthy, the ReplicaSet will automatically create a replacement. ReplicaSets are most commonly created implicitly by Deployments, but they can also be defined directly. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ + +## Supported Methods + +- `GET`: Get a ReplicaSet by name +- `LIST`: List all ReplicaSets +- `SEARCH`: Search for a ReplicaSet using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A ReplicaSet owns and manages a collection of Pods that match its selector. Each linked Pod represents one replica maintained by the ReplicaSet; scaling or health-checking operations performed by the ReplicaSet directly affect these Pods. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md b/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md new file mode 100644 index 00000000..3780e9de --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ReplicationController.md @@ -0,0 +1,23 @@ +--- +title: Replication Controller +sidebar_label: ReplicationController +--- + +A ReplicationController is a legacy Kubernetes workload controller whose job is to ensure that a specified number of pod replicas are running at any one time. If a pod crashes or is deleted, the ReplicationController creates a replacement; if too many exist, it deletes the excess. Although superseded by ReplicaSets and Deployments, ReplicationControllers are still respected by the Kubernetes API and may be encountered in older manifests. Further information can be found in the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/ + +**Terrafrom Mappings:** + +- `kubernetes_replication_controller.metadata[0].name` +- `kubernetes_replication_controller_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a ReplicationController by name +- `LIST`: List all ReplicationControllers +- `SEARCH`: Search for a ReplicationController using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A ReplicationController manages the lifecycle of a homogeneous set of Pods defined by its `spec.template`. Overmind links a ReplicationController to each Pod it owns via the `ownerReference`, enabling you to trace from controller to running workload (and vice-versa) when assessing deployment risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md b/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md new file mode 100644 index 00000000..5388ec70 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ResourceQuota.md @@ -0,0 +1,18 @@ +--- +title: Resource Quota +sidebar_label: ResourceQuota +--- + +A Kubernetes **ResourceQuota** object allows cluster administrators to limit the aggregate consumption of compute resources (such as CPU and memory), storage, and object counts (Pods, Services, PersistentVolumeClaims, etc.) within a namespace. By defining upper bounds, a ResourceQuota helps prevent any single team or workload from exhausting shared cluster capacity, and encourages fair usage across tenants. When a namespace has one or more quotas in place, resources are checked at creation or update time; if the requested amount would exceed the quota the operation is rejected. +Official documentation: https://kubernetes.io/docs/concepts/policy/resource-quotas/ + +**Terrafrom Mappings:** + +- `kubernetes_resource_quota_v1.metadata[0].name` +- `kubernetes_resource_quota.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Resource Quota by name +- `LIST`: List all Resource Quotas +- `SEARCH`: Search for a Resource Quota using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Role.md b/docs.overmind.tech/docs/sources/k8s/Types/Role.md new file mode 100644 index 00000000..38f16e56 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Role.md @@ -0,0 +1,18 @@ +--- +title: Role +sidebar_label: Role +--- + +A Kubernetes Role is an RBAC (Role-Based Access Control) resource that defines a set of permissions, expressed as rules, that apply within a single namespace. By binding a Role to a Subject (user, group, or service account) you control which verbs (get, list, create, delete, etc.) can be performed on which API resources inside that namespace. Roles are therefore central to enforcing the principle of least privilege in cluster security. +See the official Kubernetes documentation for full details: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole + +**Terrafrom Mappings:** + +- `kubernetes_role_v1.metadata[0].name` +- `kubernetes_role.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Role by name +- `LIST`: List all Roles +- `SEARCH`: Search for a Role using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md b/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md new file mode 100644 index 00000000..428b1dcf --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/RoleBinding.md @@ -0,0 +1,31 @@ +--- +title: Role Binding +sidebar_label: RoleBinding +--- + +A Kubernetes **RoleBinding** grants the permissions defined in a Role (or ClusterRole) to a set of subjects—users, groups or service accounts—within a single namespace. It is a cornerstone object in Kubernetes RBAC, controlling who can perform which actions on namespaced resources. See the official Kubernetes documentation for full details: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding + +**Terrafrom Mappings:** + +- `kubernetes_role_binding.metadata[0].name` +- `kubernetes_role_binding_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a RoleBinding by name +- `LIST`: List all RoleBindings +- `SEARCH`: Search for a RoleBinding using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Role`](/sources/k8s/Types/Role) + +The RoleBinding points to a Role via the `roleRef` field. This link lets Overmind trace which set of rules (verbs, resources, API groups) will be granted when the RoleBinding is applied. + +### [`ClusterRole`](/sources/k8s/Types/ClusterRole) + +Although scoped to a namespace, a RoleBinding can reference a ClusterRole instead of a Role. Overmind links the two so you can see when cluster-wide permission sets are being delegated into a namespace. + +### [`ServiceAccount`](/sources/k8s/Types/ServiceAccount) + +Service accounts commonly appear in the `subjects` list of a RoleBinding. Linking these enables Overmind to reveal which workloads (pods using the service account) will inherit the referenced permissions. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Secret.md b/docs.overmind.tech/docs/sources/k8s/Types/Secret.md new file mode 100644 index 00000000..1c504e2f --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Secret.md @@ -0,0 +1,17 @@ +--- +title: Secret +sidebar_label: Secret +--- + +A Kubernetes Secret is an object that holds a small amount of sensitive data—such as passwords, tokens, or keys—so that it can be used by Pods without being written to image or configuration files. Storing confidential information in a Secret allows you to keep it separate from application code and to control how and when it is exposed to the running workload. For a detailed overview, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/configuration/secret/ + +**Terrafrom Mappings:** + +- `kubernetes_secret_v1.metadata[0].name` +- `kubernetes_secret.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Secret by name +- `LIST`: List all Secrets +- `SEARCH`: Search for a Secret using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Service.md b/docs.overmind.tech/docs/sources/k8s/Types/Service.md new file mode 100644 index 00000000..039fe307 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/Service.md @@ -0,0 +1,32 @@ +--- +title: Service +sidebar_label: Service +--- + +A Kubernetes Service is an abstract resource that defines a logical set of Pods and the policy by which they can be accessed. It provides a stable virtual IP (ClusterIP), DNS entry and, depending on the type, can expose workloads internally within the cluster or externally to the Internet through NodePorts or cloud load-balancers. Services decouple network identity and discovery from the underlying Pods, allowing them to scale up, down, or be replaced without changing the connection endpoint. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/services-networking/service/ + +**Terrafrom Mappings:** + +- `kubernetes_service.metadata[0].name` +- `kubernetes_service_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Service by name +- `LIST`: List all Services +- `SEARCH`: Search for a Service using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Pod`](/sources/k8s/Types/Pod) + +A Service selects one or more Pods via label selectors and forwards traffic to them. Overmind links Services to the Pods that currently match their selector so you can see which workloads will receive traffic. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +Each Service is assigned one or more IP addresses (ClusterIP, ExternalIP, LoadBalancer IP). Overmind creates links to these IP resources to show the concrete network endpoints associated with the Service. + +### [`dns`](/sources/stdlib/Types/dns) + +Kubernetes automatically registers DNS records for every Service (e.g., `my-service.my-namespace.svc.cluster.local`). Overmind links Services to their corresponding DNS entries so you can trace name resolution to the backing workloads. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md b/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md new file mode 100644 index 00000000..5e2ed01b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/ServiceAccount.md @@ -0,0 +1,23 @@ +--- +title: Service Account +sidebar_label: ServiceAccount +--- + +A ServiceAccount is a Kubernetes resource that provides an identity to processes running inside Pods, allowing them to authenticate to the Kubernetes API and other services with the minimum privileges required. Each ServiceAccount can be linked to one or more Secrets that store its bearer token or image-pull credentials, and these Secrets are automatically mounted into Pods that specify the ServiceAccount. Further information can be found in the official Kubernetes documentation: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/. + +**Terrafrom Mappings:** + +- `kubernetes_service_account.metadata[0].name` +- `kubernetes_service_account_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a ServiceAccount by name +- `LIST`: List all ServiceAccounts +- `SEARCH`: Search for a ServiceAccount using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`Secret`](/sources/k8s/Types/Secret) + +A ServiceAccount is associated with Secrets that hold its authentication token or are referenced in `imagePullSecrets`. These Secrets determine how Pods using the ServiceAccount authenticate to the cluster or to external registries, making them critical for understanding access scopes and potential risk. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md b/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md new file mode 100644 index 00000000..e7dd27b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/StatefulSet.md @@ -0,0 +1,18 @@ +--- +title: Stateful Set +sidebar_label: StatefulSet +--- + +A StatefulSet is a Kubernetes workload controller that manages the deployment and scaling of a set of Pods, while guaranteeing the ordering and uniqueness of those Pods. Unlike Deployments, which are optimised for stateless services, StatefulSets are designed for applications that require stable network identities, stable persistent storage and ordered, graceful deployment and scaling. Typical use-cases include databases, distributed filesystems and clustered applications where each replica must be uniquely addressable. +For full details, see the official Kubernetes documentation: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ + +**Terrafrom Mappings:** + +- `kubernetes_stateful_set_v1.metadata[0].name` +- `kubernetes_stateful_set.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Stateful Set by name +- `LIST`: List all Stateful Sets +- `SEARCH`: Search for a Stateful Set using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md b/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md new file mode 100644 index 00000000..82202e32 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/StorageClass.md @@ -0,0 +1,18 @@ +--- +title: Storage Class +sidebar_label: StorageClass +--- + +A StorageClass is a cluster-wide Kubernetes resource that defines a “class” or tier of persistent storage that can be requested by workloads. Each StorageClass couples a provisioner (for example an AWS EBS driver, a CSI plug-in, or a Ceph back-end) with a set of parameters such as performance characteristics, encryption settings, reclaim policy, and mount options. When a user creates a PersistentVolumeClaim that references a particular `storageClassName`, Kubernetes dynamically provisions a matching PersistentVolume according to the rules in the StorageClass and binds it to the claim. This abstraction lets platform teams expose multiple quality-of-service levels while shielding application teams from underlying infrastructure details. +Official documentation: https://kubernetes.io/docs/concepts/storage/storage-classes/ + +**Terrafrom Mappings:** + +- `kubernetes_storage_class.metadata[0].name` +- `kubernetes_storage_class_v1.metadata[0].name` + +## Supported Methods + +- `GET`: Get a Storage Class by name +- `LIST`: List all Storage Classs +- `SEARCH`: Search for a Storage Class using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` diff --git a/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md b/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md new file mode 100644 index 00000000..62d7d4c3 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/Types/VolumeAttachment.md @@ -0,0 +1,23 @@ +--- +title: Volume Attachment +sidebar_label: VolumeAttachment +--- + +A Kubernetes `VolumeAttachment` represents the intent to attach (or detach) a PersistentVolume to a specific Node. It is created and managed automatically by the external CSI attacher or the in-tree volume controller whenever a Pod that uses a PersistentVolume is scheduled. Kubernetes will not make the volume available to the Pod until the corresponding `VolumeAttachment` reports that the attach operation has completed successfully. +For full details see the official Kubernetes documentation: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume-attachment-v1/ + +## Supported Methods + +- `GET`: Get a VolumeAttachment by name +- `LIST`: List all VolumeAttachments +- `SEARCH`: Search for a VolumeAttachment using the ListOptions JSON format e.g. `{"labelSelector": "app=wordpress"}` + +## Possible Links + +### [`PersistentVolume`](/sources/k8s/Types/PersistentVolume) + +`VolumeAttachment.spec.source.persistentVolumeName` holds the name of the PersistentVolume to be attached. Overmind links the `VolumeAttachment` to this `PersistentVolume` so you can trace which physical storage device is being mounted on which node. + +### [`Node`](/sources/k8s/Types/Node) + +`VolumeAttachment.spec.nodeName` identifies the Node where the volume should be attached. Linking `VolumeAttachment` to the `Node` lets you understand which worker machine will host the volume and helps assess the impact of node-specific storage operations. diff --git a/docs.overmind.tech/docs/sources/k8s/_category_.json b/docs.overmind.tech/docs/sources/k8s/_category_.json new file mode 100644 index 00000000..dd873fc2 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Kubernetes", + "position": 2, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your k8s cluster." + } +} diff --git a/docs.overmind.tech/docs/sources/k8s/account_settings.png b/docs.overmind.tech/docs/sources/k8s/account_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4e50b499f048c53aabd557cef52eeceaf5fcb30f GIT binary patch literal 49931 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 z zx;Y-CwCWWlrW7~%c-wJ4UlFHM*3h82?7PRVK^vLGXOd4NIhgQLCL$3m6(`6Qg|p1e zCTs&t9gk&0qeviXpJcPqP?dA}(>pGf7reO#^ROX05fq1H-7zoojUO*H`kuL+LH}+I`6g2jmUhXM3tF@rB=hZGM27 zS~MCls1s3S{hYM%g=oe7?a@6*G3T{g>7vgTZ0OGVgNtm-y{8Phlm>Nz(BUidFY>zc z7yY^CO7ISWEhQh9E5Bn;RZaK-`N=Pl7bmOAU{QLrm!#uMgjaYHXe({XdzSNGT2Ffa zTx?borH(A+;`Dea=JsvW5c{Q9EZ{vtE1*o9L&ro74=xBj`UDD0FFayPJB;{LVsKxp z-fr_=(?Carj)q_1_IY7pj;dE${2(Y<7Q&WdR|xu3UN0C){BDs>sf(1gb&O1L>JE}w zsNL=xR>_R19D{fJX|W~nfP1E$TUG3~LHKeb6*dX9p`Es}Z+gAsjFDHK|H%;Jz@N*l zg3PCCoC)Ltnj`iKzfSBr@thbJq6*I%#h&Gck&pkzvt_3=cOwi> z6n`{ef1%k~H|ryg^nnzf2;DJv@aK->0rI}19w$6owuWGMxzWNkPq3+`_wZsc^~4~B z-zkVM3drqSxL;u{H*+t|i^|QNd!u z`(EV7rHY1dCAbLh=4Mjn!C;Hu=TPT@q*aLXE-Q794!FTb3#-RVa-?H(YcsW2sfYi> zo+;30;4wAc8f?wGgYkK2wejyI%ZVmN+w{%>LjTTnD|5OeFiIe%XakScqI)*)m7E?| zQj?YQ)zaoNE}RPS7_!gFlXTgw?5~w%97pvS#GZFL=&cPm36A6 z7EBe~tiB+z0B4c#?!_)eA%tQkQnstWu_S~8fF8NFizf^hPmLsrG1M>;Sp+~#N7bC#>t=5y(kmG5Om*#2U$)C835-G6#4!OSJ@p1dp zljRRss$e3r)GwG3vg3YQp|Rm zR%q3A?du%$b>!+K-A+!h_!T6++SL`x=5K6jrIIyD$(6A$t1Msr*1hymqny)JE_OLY zAF6Iu+=W#pw>2u#i#4|q{#>RT#IY$?mX50xBnri@Rf$a0Z3)Gm#kmN7row54ypI0t zH`g28gOv`8&g(-V9#hI`_rpcKd_Y-pOMQGlzShUN)4JsZF2O-|`A!`b?iXjO}F`!P3*{kiL82mO-d0WH=DiUeK_x<2C1T zv{mBy>qYXDOLd+KS^4hiq@3xQ@B3Pg8~x=Z!_nCtCANXd`;)6eb5$PZbGCz?lPzlb z8#9=#sJ}6?ri$|mu{m1AYvlB3kgqExFsP;VEwS-pRXz-ow6Ob|Jr(+K_4sL-8`j#7 zK7URjaYK_T!)B+Y;U`i$bckP-dbG5%ahwak#8Ee4uz4-=qm3cP0tbgfGhzd{#Al{c zZlasq_Yuf2= z!0L%Abd9zUG70?cvz2Gvp5L^jBK6r!_od%?c};Wmf-Q#MpQR`8*>w?9)lN;cE>vSn z?d|n!E%-|(V#gbLY{hmGM>Y;42C?w)#V=>MD@2j*YikguhzElb_-ZadN-8Oi=Jx%n}?ksGNY6hlr=WtD#nVxe_?M# z?L;YFNI6##%J|`uYr}Rkm^>|+!rq^ltP|BlydNK&@-h8E>^AEti7^Lm(fyX7L-`Y3 z_32e`aG*r@K%0(yczkug!?~JuEl}(DoK+H~BStG*pFLO{`HGQ-dO!f;dgHrGN0(NfZ zhTCDORAUV15i{&g%n=6puQ{!*;`N=bquSTQa5S;ug?iD{&5{xN=b?C8yPZ>eYcUk` z^fSx|_0aV66md>W1T}*A_O)K={Y@<4SAW&rW-tSN1&RFmiR{UB+160#97ORN+%PkRxBg3}rQHK65cEbj5=u$$h&@l&&W?>{Y$@u~9tBuLolpWXMBVL< zLxBavsauJ{vSfLqr?O4pjG|trw*gR4H_OZYC5zmp`(&r6PWrQj$WDCwD45N(BR*N8 zE!Ccq-OzdxiPddoP_eDVlS?->*78O18_Ps}%!pTl<@h}(;mTp9efSG}E&dkMi^ zDtUr)R;dXxaMLVOvom0_4D#x7lrLHBOKKFlpKw}WeF9|^*4LdV%vChsd*%&kNBKq& z+3{4J8nMVv{k`UWGw!JwJ@2?v(<4#q)kHM*|7jiujkq0BTcrx zc>U+!P&$L3)~r868P3PU@M9ZHHBD!UoZ+RDf){d(Huu%g!4}u%+^4IW!qABE+dMzZ z!%bO<{iICbp~peP{ba4QxxO5t!_n~-4TYV+rH1H0>1V>!>0aY1aMkgPt^ z`&+c+>aDRWYQzLvf>B@S6V(0FZ57ajPf!*L64c7Fj|VFJjADHNHVSWJ5JJ!+CL3)Y zxgmb`J@(}LGM&_sdq^$UxTsTFmGg4G$+GChl9}oy4exz*9XZJDhisi#dL`i4=}vtZ zotE0veRgv)C7n8ritShYN{rTY^%@JSG%M0+J*0}NW#eVJ{Z7j3?5=)H7I(_mFV`3Q zmf=~N4_R{`FO-}N&X&}9YJ@m6?%CEE6qwU>eVhu$B15bYp!w7|3ayeu*M}BI>cL#~4V&1CPqC00nP<|3CCO1|05Yz5XzsXoVHABLqxrzyX zQt45Z$EDlVjeNRji1%^4{u^8^P!)CcIEA)t&tQC}QQK%L5@vBYAe=|1UQVeDddA+Fo|u zH$q0)Sh*cgig1k1ZX=08R`J?3PHpr(L#{>V*i!k5NVRd+?!K_jTVT;>HO(1bcF@x& zf{y}NH9xezlmt9{4fwE?k8aOhZpdIsbI2Z2;U+e-_8zQND=vdCO}Vn8S7RfPr5TB- zyBDIrI=-{Wd%3#PYc-u^y)&NrGyNyi5k0Ags_a*gT90*gRmAr}*_rL;mx_vtaRI&I zW7xb<00vyW@(dxG*q!T17n=~T$HUraZY2Y4XWRj!3f#YE%uAFrwI&Z{p~XE;V(%>+ zF}{9ZqMDK$ODhP}Wo!MKkmlrvnUP6PO=+eI73$9vJwK=yaaqL;K`}*hrtSPtTSv^OFD*zj>a-$y^5lBrr9Zp}<}jy_ z%8-qD-zG>(!JQZO`C{Q>eFGbqUo5}Nx{eGklwJ~kKGP$+)o=RAGQcWLSsLEoizKPC zwI1^9{Oz}2ueL$jMZIoiWmer4=+!0Y4tps4g4Iv^a%Qp6dLOd}Q&UaX$NLcw`G0;? zfN8W5c7f4@m5e2U3ta}R$j%8`QhE+CIHpx*PYRfEL@J{DS-{mbfw#W(T@=IACO;k% z(@+n@nE0dDJT7*699C#3?8g%NU_(>;&U$qXYi&3dmV2;D>iniCT$@>9X+L<+Ts<5b zl&G?|+>-mpwxz!P`FxI#CK9qi`AsNeFT0qOL z4^!NusJA>Gu(rH`u6%FPhm4H-<Wb5|4iA@l!3oA0B~)7mtMtl>`hV1&55*nYA77>=Mt|j2x)LhHhD`{sam| zic69oS_Jq(p0PV)3wUQy<+p3!7YUDl?7#EGI1l(@e^|G8 z)k)kDTekX|Dy?P}?FFy~plYdki(bU4AB^$MdSclX?PA+%SvN0Mt1TI&{S#@^jOnfX zX3Z>GE~w&joMu&>XBR5vl}>hg+w4g(Cu76Cwddsy)gccQ%qOQPkU{3Q^!BoMKQsx+ zkj{Rl+9fvfw6T6yzVE}8`im1B4v-!N1pzL}XR73JqPA^-B0&R1XDWPTdHUd&I{)QW zd|pR3*|-v>uIuOXJ(g(R+IdfSuA#Gljci&=76+7K8BtMugef%AlqdKt&T~>gOuk-B zduaadYw&Q~AK81~Et(=`jDwa&StD#3 zw_@in7Hn_ULQVZRdpg6&x{(W70e6S=;-cON1KBk*;xPM_t}N(zrcLG}k09to4V-A< z_Wp%Rmyle~mVir>9E$COS5;u+kk#@IK2@wWxKp>KAVpVgu4dDmPR4yU=)=w0u;W@W z&N#xq;FzCFf)^>)bqU_ReReehkyEnm47m1oV%H(YCaP&cG0usIh_F&A*kfk#N;MaM zscVXvh#hIwJa1R=nU(2a;TV?+XEmZ{5u}4LYuPc0A3IC++)?Un_al-DxyMkPYQp)k zp&@1+5m@k=zjk<&uPHsC03hyb1(WGdvI$A;>L6hYaBb_>~>H;sMrZ!7zRAj z(LdAU8jR=fG#`+PhBLtJ?zlY%ti4o`jHR=l73b&6FMl3B->>j;PP?(4wp`ngs5>|; zY;`t>NC4@-HqLSMlmGtd;{NfHPE+8r9Ew&*H(h1rDZPCRe&A&Q2U0het^2lg9N(VBdU3*}NtB~S zb7vk;905cLrJNoK^o8T3XV}&+MAgnfrP8wtZH1LbG_A0{OTKanx%K+{1Z?b8BW)hQ z3FQ5mBj5XI;E|B4Ukd=7zlPmMn+WQoH5m3kH6MJ>F{w-b_o5c-kl+?E!B@3Ou9dRBzbwV zFq~(^@%3X`S0*#!W^?S9Jk?hq#$-%Ee5$zYGx_G#TuQ;~^9L;A#$R-F0n#`nGufcs zC*I+V)hXVQVB(2R6H;ggfD{#EN6D%ZC_7Y1PCZf9`EA_pRq$}en}wv-{p4jLvqAI` zfWA&J<_=#xY3*}aiE%E)f(^bEc6SV8DU1B zzP8D9!hM6rP=3vVAm%V+0vr{o1M%ca2lI`onnD%S0OIroRl6F@j_{&oWgWSJgoqJN zF(cA=w2yFe#q{fK-(T3yPr30q;Ui|nnSd74VpC4m=?B{oP{^UJ0IPb``{1q3O@+S5 zo*dl|;e!@L04b-yP~&}UrU%%{FCW!{JA7>~;B$#l0axJs%*Jv*P@gF$&>`JW#3cimENSj_;nU0yg(H)O8>F1)jJgZ z37NbT;F$lcMvG4&S`zc@_3{IGB;@Bk3|;P@Yl&)JG)S1U6E9Jjkk3SXTAfl=B8t{v zK3CWCA`&l9OGN6?(bs2zpHP=(f^!;wU~Zpp}n7*)lY_qeJRy8 zk_i%!-<_!jp`oFDlX^e^Einj*h=wLhl;XeE)J)*)nB7qjJEMx2eD)emS+Ur!!&I-U zZBSntE6-<#{Mut$BHP!s+UC~_W!F3VcJhwUzxFMbO|~?uKFtL>;bRC9Epofx*@>wN zZa5{1jBy;2UL0x@K1$PU-#SMe`e!ewYGDe_;}IQEHfAG^{yl94 zTN@@)>IYUWO4$2J9rl>w!mq~Mk*3S9#pV)_tX$P_1)7V2c?qmeT?MpCLrYWx%ft77 zW``@@>vwELx-et7dkE4z)wO;s4G&_javH8?F^$DyRnU%aM<+y%ee8l?!&pnmmn?5m$s zQFc@}a1xQboH9i0BevKS@koLRF{I<8U@Q?~th<-pG*27`Ln(1KOZBr*pPRNr^LI~< zYl>E5qqLER2WXeg*vai%k%UDdNzy3ZN2K!+t9@eUEI0T(0C(vy2cUN$QZQ7628XdXaAX*f#Yuo6{P_+m9 z-)A3l%qbDbha*X&w|lW1;!CYk|8oBlE>Cl-a@KJy?8atk&KzbP(`YCRRsAc;jQBGp z0sU5+8MqCRk?QZ?^*e+F-F{Iu8nscnjy4$uv4p+W({%XwC@h@r-?jTG%o@>(D?+v- zPhx&%+su42s^5!k{1Bkpz>8+gD~Q}mJVN5sz@6SMfJ9RPECzo0fzLoA>t<*KOp?8JRuhEu?rx1 zldvML_wHjOhn7-99m8}B#7`o041NrslKssAqb7^D zhYnZx*Ew=9R$nb~|2QG^|FK=Xqt7NeToGpv+n zU}0e~{b^sFl>Hkn#v}-av$C*oGm-$bBh>$>NKvQwA-OTXxj#8N0q2eKwpg9=7Lz7A zA+YI&8NLN#<|jI*fNljA(srpVdBb_q8?Y#-_>6?AwEl zzHYY#;62oZvaLZwtI@k^nc)S`;$t1O_1<2|W<{97Km4~pO^yGhEA1lZycIUDvVG-J zIaBo}KE(3C-YF*(N}m$h&i-o=wq)ZZYJxzr+D)*!i`JQa z;fLZqu_dwvD??`X+e`TdZsmVlFSq+l0zg|BFm{kE{Q*3oSW>@$nMe=T6p z9;5;HaXaq+0(2a%i=4cf2A#MyLw`iglvjf|6wE8w{*O@BK8x<3iXwt!nLC4)-`2tg zZ1f!Gu?4C4~%h*Vt?^s8g_MuOts?wKnjtOv1Sm!j~Ese3bqA z?LMuZ9@Ez`|DA^#jBhN6ci5;oF%9yy8TC0BE9MLB!|4zT0xUv*4kF5v3c~nCit}g4A%}&INjs?*%)k(*ZkhjgIc})&6IZ1Y4Uh7Dw-1SitW{M&iiCTwq3I@8kwJb?slz{&X7j6ciB1lxo9@ncUZ%@_ZO<5;O;zXpQ1Sp)x5~?O43c^b1a9YAUB-E(g1^Rkw0()xXDzJN#%Z zc%cBkJq`ryyCSO=D-}?l!#~4Ziepk4$xv6z&4SSS=gozpp`m3w@dY2TJD;h%k_aTf&-J;19JLudfRZF=r*v<*Rn`Y;eMz8ie$_1%lMGZlat}W<>Z($2ib3a&?$AyPP-Vfj|?zUw51H$TZsQ zCo!&c7YST`WjRdv`X;(hITiXOa6DS8`kd9b2^d!{4hD<%W)_1R;g3@w^fM@?P^xLTrCZ zv>2MCcLkgl;RiZ~E)fa>pI5|j1-I{`Ci~Ee=xSPEfO9wV{WHel&+cKzX|gXVP6j%#@U$ZgDeZ#|YnZya!&{s!R|>VGpd zH~?Va0y_Y0sBqM{1)cZQ*q;RX;*x1NwxK%l)tmy(Kb>a-lIdtZO z3F%$;mj+FHHF@rHwXyFzmdnfO{$Ss)1l-?Va340ighJIM0z>wj7Mvy_Ue0qhrc#KG zPfwOSC5~i-Eq4jC5i`=4_Up#5(^#HrKL867jrDw83e3g0Gl7kIuue$vg<*e=LGAyv zt+K!h9l(=uCElK@ij53X2C)e&yMG#zqLK)f!LIJQYg-H)Jjj>v{j*K2i=+;;i|b4# zK!IB4dz6}>mFcu>9r{@SnY(DCtM0hVF6h; zBwz+9Cw)p3ozDUX?pV$NkW63Z^`8S;rA&@=C5~3}8Tp+gHzqW|!W3rpQ0e&byer$)+mcu(}ZE*|HSyl+1_VXd1| z5sDZA)(I+yL92m2z$Sn`ej%6MW5(|QiEX^_jTh)rkp#e=p9zx87^ezdx4ou^v70&} z1Gi{3Y{t5|3X&}+x($NpG`y1MNYbb6eWOuk?qc~LsYb&{1 z_!Ul~U2s{sV{Q|-*#-vR^W>{uY>+%)W9s8KFr=eFz~iQ@-w2#ZPW0yTy8e_`<3dZu zSS!g_c908g5(5zbcgDD)2$R(1FPqw~%RXBX2R{g;qHv#JyQJ;SH&z_?N^XCi?NQvj z2ODXB{CMywL6l{&>*{QeoErNn266@RvSG9sYBYSy*c%*3dGCkN+xuBg>vz~%)|yaw zhx<~H^auG!eLoE%jdeaGv#FuDVN^)ubd<+(LMH6Z^%eHA5QfGm-cO`t9Yakv{38s6 z|F-ook-v_Sht_P^d(VsQ#;Bh|73QK{S?j^@)kk>40Q%-Qc_RZXkXs+@kQYnz9a}8F z7Gr=Z0U3YeLTxTk@c=Uq-8f<85J4E_G< zyv1DECtA>ZdcMKd`~=?iv9kyIgx|+oVU=A1iDVQn{GC+Gc5mzkK!s!Covy`ReQ+D+ zGDAHGx;XG!vj_BV>N8crAB&Ge;N4+(-@;`QD6bO`T>GN-8~K zg0$UMRqU(**xQ)Zq*JL&Z4z1d4%I;Txzx5!ksCrq@RII#d7q-5cb}$s7^5(NU7u&! z4iodq$jG?<;gTcod6c2+;|R(@z;c~8La{-d7pAXXk@Tw5W&Z)3E-Jl{f-fD15e&3(lH9EuRMKULmrqQ^@8yHei zID8b8xl4fT#LosdW31!2&EY$&MO@l@-5sf1^QYde$AU&lIJmQPT2?x*pF_?`vYS_Y z7e=4*l2jV&82Us!z~=*IK|Koxvc9Fdk?v|-(luBdj=lgE>pHs@m0dX_8u=bW)Ef`t zab&eCdhx++^X3#fW2GF|ngr@PsdL|1>>s85`P%0k|6Q?GQ&z6`2zzsuuLGD9!Hj4} zWxO)EaGoXU8}(P1JsuVDJ?AvTm)fZYfY{ct-iMtAk$KbCc48~rKR#_%)y&z!^o>Nr%HilQfqQO&gM{%D~#?^l_2?sA2vP}4|S zZ4~Mw)%lwD#jjpsHUnz9uhdh>^FZR6KSFZBt$Iw#{cz%|$WI6WXb%NkEbAnQej|(I z2V_F`cRI5Ul?4+!JD*rfX&LLR8Tb4z5WJge^&8$zx50G1j;guPUCZyy9Q*dII#VZ; zmB0Yy)+tpnBaV&@^;DG%fMsHbUHG6yb4O!CjMy`ySNp8X-7?2ZS#X9r5xudK{O)9m zGRUVdxK_C4wK>o6MI--kHB|Sg_INF~Zu#Ik|HeZfC6ca*XuB2lP$@iLNT}6?^=E>4 z84TdJfF8E`Oisd?w|c(dbn(I7Zl?c1JHj5c&s=>Z0Gx3Wx{~^g4MADQe?719vg)x6*Xh*)e&Iu>MPA`#T%MXyeF$hbFtVpule)w zP#TONo@=HP@2n*;aguipn{3?BJn#lW%J6t&)f;eutuh@Ki>!~(aVARpxbC`kx~zjz zn@48BCJUz|oyRNRAV(dnGsaz)0dt{vr}v!APYYO|L{a#D{$KU!jg3@QT&P#L7clkos*Tmk8kl$;Il%`AH_iKZ4qa2#L1I(% z0Q_2P*7pvTjpIG9Cc!~{%z-r(KrQ7C-`z$)afMfswPO}6vw`i`7^1)?(bX>672nV6 zqmC2*c5+P7eGku#AhV0%*1oLsce=$OyUb584+#vIk?BIFZrE+VBZ%|3&6woXtW-7^ zW+!H$qn>2!3w#>Kkra-eGJsMijO-6W1fJeS*va7}pG?MrvZ>#Vy7Wu1E4ESI?qQUd3O!#Nm+hzX^PCYVNI7_bW_m zU6+0QVXx=h!>Dk5OW$|_j0%dts4!3D*<$RdPvMe;T7X+oRE>|BJTBkQ@QqCL5|IkG z>Ywd$*fQcjQu8|QCmDJE2s&X#GywfS?7e4HQ{UD;DhSf0Ns}rFs34&zH3Sh56cAK8 zNEHz2N>AtzP?`@;jdMzUSk;1zMM5{eD7_ypGiwUfzozGw?mN2jP5-W=p0m%$@$a%@G^1>bGhxS^I}gbm zM*AQ=dh7-U4>$dDC8vTf;5B9r4@h&lx7veC)k~kCT_lh%=})AJeOa_G?K6e9DA79E z9Ikv8Q&D6_Y~kdhKL(d=QyNr7es-lpk)*+Crvog-dcd*e9BrK*@f?kHEFD3e9=_NG z1;@#TP+%On{X-q1n_M0R;-X6q4@#~2xK5$N%GC|_0~$!To@VY1HxC)-nz*1zA9fDR zghW_$m5wzY3G{C-4%nt{a@@v9@=JgGd~NgexVPWdyxL-H=}=D6HW9Tx#|`uDW-@L+ za}9&4$MR!&^QC=URT?$RxXzqo}xZ|Nv#uVwT{ahS*DR1 zHNQC+K$9vCm#|D>iO&k7?OLDzlsnkb_!22dx!2;ILMQIIY%<_kSwiF-<`P&Zysdc z+A18EIJx@CvY!co*ANDvY_gu^SlEVO8iH5f)ICG&XeIjS@p8)SD=QH8^GJ|`s^~`e zT1FfYhei;-{UZ+ABx9g>uj2#N>`ZZ=i4X`MIi9By9U%3ouO45oN7Ax$F&l1=_@_aS z_SZqE^vHA7IzqQLJYR4$JqG%qb%LtIGi_8LN;V87|85v8;%B7%ce+u;q~?G{7^jAj zehRM@yr8||3#2IY%>zJnr?lrru<``Jc5&(ARJRsog0dh3K`S+V`+W}8kr!P=ITrZ! zHf4rchuY2Y3|(YzBCKi_%^`1PfHN~yM$@UPdNS0Xf)>rR>PYFn%kX%{q4&uy6BLcXVQ}oh!cb{0p+IpO`gO>u<|2x? zXLGcl_?YQ6#4U`yEfoG{5`#^+>N2y##KMvk6*qoaxt8~2cnfofaeSeUSgyL1!j_SI zBn%V{O{cfJ5FalgM$b>C`5(%l0&_n|ZwLYHiSFQEs_NDvKkF zT=j%esk3rhuY*gO#y9&+rQoY)XmWX!lT+6MFY5DPWj*JG`?F44YQ;1tnZb@0*To-= zKp}i*_qY++$NbqUZI-yU_SUvHh2r@Uwg{TGsi- zId-~ck37<}5nIHT%NkXuQllZtKxbptDh4Ta#I-sS{t&)Duqh5W96AK(Z-Mq#t=`mb zNE~|oFH=v4pwsBjWx1gjyAdFg&;eM?)@6DmSnAslt(^fruOmSwnoQCMyCoza> zH6pXoF6exFK&29mZjD zEu<{y`%+9wsr*MmiGrHx`j~xdUL&Gyy;Gr!+_Ew|sYJhaun#r*qH^V~O4q9k=rF&| z1i2=!4^=Q)=?lw2>yW4FB31R!=b*H+i4^r_mn`U@-6IiGE=lwLI?wMWAL>3`y4mGS zLjau-T6C!yEP_t(ki+)QT~2lhbX2yapmUG!B7@LDFIh*AQF8i|-U=93lEbz!+vZr_ zOwbS5rcip&C4XeXh7Ol>d37PW|H%*EWJ$+FD6aPSG+wIGDO}m>!QvbFJ=q?*$o}4w zk0{28uEGy?wgGXnVyH`c=P2xQ>$pT z-aS9IJ#~#yJ_Hg;&B+@~Mtc%oRCF@0F}YrCg>lrt!Gd_yDZrD8SFGn+=Y1BXZ7ul& z9fRx%X;fk7AmT>ocb27r#T^#|ich18@F}-<7I)xQ_l2Qo;!{%C9oWI*ogK$v=i8_P z1479>3PZ->5+IXi7;~(MlzToPjxj>Q)PHglS7^iLv&zO0yy1 zyZRS4DHpCsFkj>q(QsJ^#41edAfFLuFweP8wWm{8SV?;tNjHQ4@C(aB|*MhY!mY@e>8Z+UHi{8K8oN?oZ4Sp z`AA`&Ap&(XI6qo!uirJjnEvf8=hTe1L=;#8T_iv&V!dM@D}+C}Tq-cM9P1IvxvZb` zN>u|r^Bh3741BMRk6RcVzA|>APd@dQC{^2GDVUF#KCkb`morM3PhQ0wR>0@F7<`O$ z?A2h6}(aSdzHNB1M|7XvSj(GSpdGS~{P()}K^xFcU-B|?%nU)v9VzivSf{xQjWHXI>a(P?qFDv!??Z5aA!0(Z!DI`n0Wdbv` z{S)y1c@cOF_#FcOU_I>7wW7C{9ZT7Nd7$rqea?T5nrQzy>Q@K?7z@LH2sv)BB8M)W z;d(mRMsTy#=XBd3~k&FMuyJ_fR`TMfwSu83@$aBG5hcuD+E!Tz>~<%CFZrXt3`oqnpYWo^ZV@8iUq7&elT1|e%*vk~ zmWRs$OUm~_?{nh$Fav*cm!*=6MyLBO-RDf~D>G6%>(E0z`IRv|WayuxU_df9ulp(E z-Mh1!v~A44v3!uy_4LIHzT$tq)xzMNXdMkg3^v|9I zkX#^;ab^#`XR~cOQC4Spef+M%WV?LlUw&W&R%vKst=u`_59yZ05omydXC(yXK14{Y zi2#0M<}c}X%?_g7&J{L24|0=~p8rFizGfgPC@44!QGmRc0~rf-I+rOwNIyzz{AXHT zAiea8R@$CG{BM3Ku%EnsexP^o9R6iJGaU%^xWE0iKTSe-ZQALL*jFzT<3D*ShWvRE zio*P|$n?DR%sqW++pf=nKp<|1u7hKwTJ$GStP&6ETB7n;{V$LM)QEQnaFLdIz;F1FyJIwzse@ZSpHwjWhap5dT`qA zYt}QJPkJ32B>9K6*g;3*qDwh;Ut|FTSp3wy{~cT72SK{Opomk55B^~ee>Uj?A*vPs zD!#!5glGFN;SmdSy9;#eKkMY8CiQ>blHdGgt^SAD8T=Bv|4nJ~u!0yhXq5{w7y3k^ zRO~-Y)<4VnkJ#D&6ubXbYyKs}uD@jXr>ida%V_;F8-IzNWyepk``^^&-!k0!GgA6D zGxkdr{v~$VKcmk-deic6?fSP2C;XJ*|7l+SUW3a&*Wf?Q|NmIf|C7$~|0{(G{+^<` z=5V*|wHIhTqnW+4R5~{LqjbngP6nXICMN$Vf<}4(QIzTH?VJQ=z&`IUo0haF(GoJ-Wt!^U0&-AOZ z=DyirWVm4CoHDp5o(P1k^<#$!qZ`^21pf7l-|lcZah&S*+~`(qYfq3ESq@!~VwZY# z*KL>7QxxUUs7vpFxfRP=(-5hT#tfwH<5Md=YjQo;%hi*frWk%g8* z?jh!&KR9f-QpE>4cDwj;m zM`}j-y5I(IyCVr6>$BX?4^sdyJ?i+E@szlWn|oj~^1fl}$8@hu5j*fm{JDK*&v*lb zShG(xlJ$(Cq2aO>A;0)Xl5_PlLVKCS%l7X;U~#Sa>RF#qz+{`|-G2uPn+gE(o45@F zRmcg_u5TUs^U&?;m|-;N@8>gO+GtZ+_2AouJGPlOQ{I7u*xNuD#I2v5KYv~p_(Z~4=3;7A(JLKd4fnSz zSE?mO?+BM=-K{JEWb_Ttc%lwwPuUB5@qqy?tgSp8hQHijcE|G=LE|ihMzlkj$C4~CnI7p4|6hg zyH^baP%;Fxa^^1o@Ezb>sQlwxkln(ixb<9Lp1jshaGh>dpb+_#)u7Ldj#KrH&3Zee zYU6p4^H_EJIxU;{*|%YIX8Un>JrY1Kx9oQL(ovsJ2#}c2Qw8@V8WYMXhxDeWJ#y= zB=u$0sBUg@eVY4)sxe$a;}7Anm+A#c9%d_o*k9?(49=2#6kvB+2_H8(b@XtG`wD<# z9+zERUg*nphGvM?*?%AgZ5<*XQ!q5&2WOM*V0U5x1>!%Eznof*OX#>rM*|Z*C)iy~ zMZOna6!iqM;5yluU{OU2!}ARLI>`DC+GQp{l`({_BFH-ywZ5QVPWDgKa`${@vL*6{ zroe`J19BQth9#bC=^eN9`ui$e}QbId07V=x!%;UE@Vq^WNCeAxm}inJK^- zLZGLp=tHaLa5+okN~B>N55rA-<}$2F?8@F~^FEV7fF2DGXWxV&8(2|~M=IQ@wn>%x z=Y4~DjspgO5_XZ63wLAs#}8IUWILo{me&xZ{I%5J>ZpQVm|dEc%ZkKezQ9lg5}Eu)sw95sRq| z$O>SgC8{Iu%2Fgf4wZp`+c_3qDq<_f_ zkm!Y1$a$$bq(y~sPs5rt$i%z4!Rn) zU(PiH%;MKD87JH|%X1v-3?r|*f-JO+wJWEFQ5W%8F=bPg&#w+Zx4gR4=E-QePU^k+ z%f4i>^a$Mi{0uStSly%@nZs;U1B_8XL)3;6mUOcB9|pT&LtA_iH;Zt;SF?F5sMu=4qK6~QDB67c@SQxb-_@dP*LDz7TU95Z4T{_Ys0 zADCADot>R=-jdn6;fs(iCQ2*mTUB~=4s=e0?~g88W9duvnLjTf9O?bQ%y*MX!{!Vr zQiLAcAP3+<&B28@$`>%--FS<;FY`&cl^Lf|DpVKZ8Ae`R3%#UQ)d~9N;b;YsZc95) zFk!TDC=}}c2!&Z8saerpUK}sdfF6a)UZsR(pMZhh7aG2$!w(}Cf$|qvI;@lU7!Sab zIZ-X*gzG$?gPcKjKvK{X8jODYix7%Qd|l7Zb;Sh;G8wjyQ(CtUfbi@GN6Q$roV>93 z_S+tyZLY6Rcce3|ZW>w3yRUkk=#_s*^?J7(Wl7<7O$f;D?;M0oQjftnpB~CA~1%%d|W`rLgmZu`ng9ugbu8?{IG=X-V&A7rT3a zVi~X6!J;FIw76l;vLOz+0h=Jfn@USGJMw7ew4{T<^sW5sQpk4t3qp9&P@d>6#Lk(q;gsP0W@hU$&%wIhCP8pkR;<2pBc8}Lyq^SSZ~CI zoeCi*iR!yx3}0aOI`>UT*ANZr#PYsrK#7ds%m6WOjKXAy(3QEUsQ1>B&;grd|^FlS?C`(l48DUaA*Hy^QCZy}a_pPlOW2 z-xNMzdWHWW?Ftu1A*3Q)pap4f7w`78j}(+I}7k`DS}Xmx^K zO&l;sVoS2Os>g4d)>0aikSpb;H*z6xir$k^QtiIja2Ysx2{w3!VslnI{XE@nM(X(= zcOh>(%`lg}I-;c~$j@SkEa|SQvM8II!A1l{OwpNjq?nd|(*|`DB)bQLLEc-g2|{bY zWL!fLW%k4G<-V~@h*@@NA+x0uBq^!nglwpU?GJr<0ZK;~Ym=J^S*#EWTh+0WX|uF--Hq8U@dJn{ivtF8|{20DPbA) zzqgiul)IWv3>D#jDt}FsD_4;}QNs8&R@=)fwTDSu7Z<3X%~k2u|NhS6n=~eG{DvWx zt?rf``SRHV5Veulzv6n6vE45Q_3USU2l_x-6jH*HJtKCq4GhMaTp-F3aw{~}R(5-! z^jqrf*Bm)hG?_(+chCh>ZT+IqlOD!uR%M%@bD(F(ICT2U9KAg-yL9!e;zuCFtaBfv z*k?aZ`_V^w$sTVqd5R%7;EW}?y=OP_RRdHjSv%;>U8&caUKXp)Cy4=S67J2=tO2~S zSTE;z<6sCEi5LHd^t&56OuZMSOYfAbh0h^Rooc;@CgWJ8UeXJJj9Ahh1WS>dnt3|K z4S9O`@Fde=&({r9c+~Pzdc`<6$CxSrcfUQZs#5JbDR&6tbQ!vL>d9MhgG17i{vzME zBGagDNhGuT?5!obC#=zpDEaM}^Pq2dKdWkr?kVgC%$J;t8dcL(-rtVL$Rc&mR5)~# z-wII?oE-yM0X9xiwv*PJYsl?W>krT5xpgTmk>yw%T3&KKd$YB+jvm2Ghr5|qOQJME zXa{Lw4;lPYL67MrgNW);VC)|khUXKPgDAp=o?`S2m^`}H?CjH35RqKiq5RgSU-~-d z)OR7_?FOBe2W=xK-h@I(;B_A5S7EH;cX33w#G$@h6DPUz_P&##6mb<6V=`W!#nOa( z^v;xEG4K3CbU*1)9Et|sujN*ZpFC|}d+G7c`b?7UssVNA-X?3Q!9sWjM=A{_7NFcQ z@C^A7Zb?356CJ{3Uaep+{4ns0@>y0d?`wSWf~*s?x1h%2#4uVP-qzQO!R=3d#pkU# zL2N(A2a?3EZ{h8^&A4#Omw~;Sh_mAVL_3CroOZ~jicywT(xpRSbSYfjR}g!K9!xBU(~EzN}U{IcPNPp@I98_lSE*13JEt%tR2kdiJ*A;M|lSPm+VVL z7Yl$-ciQzsiCev{3NDaSnVT3{XGwvZS6n<}gFn)(SZxpGE5*`%;amB_mz<6O9ywcA z_7>gHrioj(%J`W@HRGQ9-UQ|^fdEVgZin1mpm+0XN^!m?B7mdfr3#|m%(QpU@u;~# z0rM58r}LM7ev(MYhhC>&@yCGoi}74si7@EmzhhDR<dL>;3qQO4I)5(oB$YHmyQ)` z3ntXn6Mp2BKc`6o278@CSbK~9x#~j^l*-4|CcVZAQN;x13e!&iLJ`$?&H<@r@L~jm z$8X_cr_da~xhWC?=tS&nsm_Fuhi_f_x~OTGFSbr0n9S3WB&8K_&YAyr`3!>^DdC-2 zPIf^n0QizDA|y-GdF>wO>j4z(Td->D^~S#o;&^lkZ{w-VAg@-3RO#S?k6B~_B0v#s z_Gcx$ey+q@z>|u)QN79Jl7CHj!g0($PtYVh;h0%0DR=$o(=$rcoP96I(gz)gn$L`o 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

#o^08*x-gg$QT%+ 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 API Keys + +![account settings](account_settings.png) +![api key](api_key.png) + +Install the source into your Kubernetes cluster using Helm: + +```sh +helm repo add overmind https://dl.cloudsmith.io/public/overmind/tools/helm/charts +helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.value=YOUR_API_KEY \ + --set source.clusterName=my-cluster-name +``` + +## Uninstalling + +```sh +helm uninstall overmind-kube-source +``` + +## Upgrading + +```shell +helm upgrade overmind-kube-source overmind/overmind-kube-source +``` + +## Configuration + +The following table lists the configurable parameters and their default values. + +### Image Configuration + +| Parameter | Description | Default | +| ------------------ | ---------------------------------- | ------------------------------------------- | +| `image.repository` | Image repository | `ghcr.io/overmindtech/workspace/k8s-source` | +| `image.pullPolicy` | Image pull policy | `Always` | +| `image.tag` | Image tag (defaults to appVersion) | `""` | +| `imagePullSecrets` | Image pull secrets | `[]` | + +### Deployment Configuration + +| Parameter | Description | Default | +| -------------------- | -------------------------- | ------- | +| `replicaCount` | Number of replicas | `1` | +| `nameOverride` | Override chart name | `""` | +| `fullnameOverride` | Override full name | `""` | +| `podAnnotations` | Pod annotations | `{}` | +| `podSecurityContext` | Pod security context | `{}` | +| `securityContext` | Container security context | `{}` | +| `nodeSelector` | Node selector | `{}` | +| `tolerations` | Pod tolerations | `[]` | +| `affinity` | Pod affinity rules | `{}` | + +### Source Configuration + +| Parameter | Description | Default | +| ---------------------------------- | ----------------------------------------------------- | --------------------------- | +| `source.log` | Log level (info, debug, trace) | `info` | +| `source.apiKey.value` | Direct API key value (not recommended for production) | `""` | +| `source.apiKey.existingSecretName` | Name of existing secret containing API key | `""` | +| `source.app` | Overmind instance URL | `https://app.overmind.tech` | +| `source.maxParallel` | Max parallel requests | `20` | +| `source.rateLimitQPS` | K8s API rate limit QPS | `10` | +| `source.rateLimitBurst` | K8s API rate limit burst | `30` | +| `source.clusterName` | Cluster name | `""` | +| `source.honeycombApiKey` | Honeycomb API key | `""` | + +### Pod Disruption Budget Configuration + +| Parameter | Description | Default | +| ----------------------------- | ---------------------------- | ------- | +| `podDisruptionBudget.enabled` | Enable Pod Disruption Budget | `true` | + +### Example values.yaml + +```yaml +source: + apiKey: 'your-api-key' + clusterName: 'production-cluster' + log: 'debug' + maxParallel: 30 + rateLimitQPS: 20 + rateLimitBurst: 40 + +# Pod Disruption Budget is enabled by default for production protection +podDisruptionBudget: + enabled: true + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## API Key Management + +The chart provides two methods for managing the required Overmind API key: + +### Using an Existing Secret + +1. Create a Kubernetes secret containing your API key: + + ```sh + kubectl create secret generic overmind-api-key \ + --from-literal=API_KEY=your-api-key-here + ``` + +2. Install the chart: + + ```sh + helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.existingSecretName=overmind-api-key + ``` + +**Important Notes:** + +- The secret MUST contain a key named `API_KEY` +- The secret must exist in the same namespace as the chart +- Installation will fail if: + - The secret doesn't exist + - The secret exists but doesn't contain an `API_KEY` key + - Neither `source.apiKey.existingSecretName` nor `source.apiKey.value` is provided + +### Using Direct Value + +```sh +helm install overmind-kube-source overmind/overmind-kube-source \ + --set source.apiKey.value=YOUR_API_KEY + --set source.clusterName=my-cluster-name +``` + +**Warning:** This method stores the API key in clear text in your values file. Only use for development/testing. + +## Support + +This source will support all Kubernetes versions that are currently maintained in the kubernetes project. The list can be found [here](https://kubernetes.io/releases/) diff --git a/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json b/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json new file mode 100644 index 00000000..1392e33e --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ClusterRole.json @@ -0,0 +1,18 @@ +{ + "type": "ClusterRole", + "category": 4, + "descriptiveName": "Cluster Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cluster Role by name", + "list": true, + "listDescription": "List all Cluster Roles", + "search": true, + "searchDescription": "Search for a Cluster Role using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cluster_role_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json b/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json new file mode 100644 index 00000000..e74b86e7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ClusterRoleBinding.json @@ -0,0 +1,22 @@ +{ + "type": "ClusterRoleBinding", + "category": 4, + "potentialLinks": ["ClusterRole", "ServiceAccount", "User", "Group"], + "descriptiveName": "Cluster Role Binding", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cluster Role Binding by name", + "list": true, + "listDescription": "List all Cluster Role Bindings", + "search": true, + "searchDescription": "Search for a Cluster Role Binding using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cluster_role_binding_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_cluster_role_binding.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json b/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json new file mode 100644 index 00000000..08db112b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ConfigMap.json @@ -0,0 +1,21 @@ +{ + "type": "ConfigMap", + "category": 7, + "descriptiveName": "Config Map", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Config Map by name", + "list": true, + "listDescription": "List all Config Maps", + "search": true, + "searchDescription": "Search for a Config Map using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_config_map_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_config_map.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/CronJob.json b/docs.overmind.tech/docs/sources/k8s/data/CronJob.json new file mode 100644 index 00000000..8780624c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/CronJob.json @@ -0,0 +1,21 @@ +{ + "type": "CronJob", + "category": 1, + "descriptiveName": "Cron Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Cron Job by name", + "list": true, + "listDescription": "List all Cron Jobs", + "search": true, + "searchDescription": "Search for a Cron Job using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_cron_job_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_cron_job.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json b/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json new file mode 100644 index 00000000..26aa22bf --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/DaemonSet.json @@ -0,0 +1,21 @@ +{ + "type": "DaemonSet", + "category": 1, + "descriptiveName": "Daemon Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Daemon Set by name", + "list": true, + "listDescription": "List all Daemon Sets", + "search": true, + "searchDescription": "Search for a Daemon Set using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_daemon_set_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_daemonset.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Deployment.json b/docs.overmind.tech/docs/sources/k8s/data/Deployment.json new file mode 100644 index 00000000..d683258a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Deployment.json @@ -0,0 +1,22 @@ +{ + "type": "Deployment", + "category": 1, + "potentialLinks": ["ReplicaSet"], + "descriptiveName": "Deployment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Deployment by name", + "list": true, + "listDescription": "List all Deployments", + "search": true, + "searchDescription": "Search for a Deployment using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_deployment_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_deployment.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json new file mode 100644 index 00000000..a8a9ef2a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json @@ -0,0 +1,22 @@ +{ + "type": "EndpointSlice", + "category": 3, + "potentialLinks": ["Node", "Pod", "dns", "ip"], + "descriptiveName": "Endpoint Slice", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a EndpointSlice by name", + "list": true, + "listDescription": "List all EndpointSlices", + "search": true, + "searchDescription": "Search for a EndpointSlice using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_endpoints_slice_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_endpoints_slice.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json b/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json new file mode 100644 index 00000000..0edba0b7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Endpoints.json @@ -0,0 +1,22 @@ +{ + "type": "Endpoints", + "category": 3, + "potentialLinks": ["Node", "ip", "Pod", "ExternalName", "DNS"], + "descriptiveName": "Endpoints", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Endpoints by name", + "list": true, + "listDescription": "List all Endpointss", + "search": true, + "searchDescription": "Search for a Endpoints using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_endpoints.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_endpoints_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json b/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json new file mode 100644 index 00000000..bd809e11 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/HorizontalPodAutoscaler.json @@ -0,0 +1,18 @@ +{ + "type": "HorizontalPodAutoscaler", + "category": 7, + "descriptiveName": "Horizontal Pod Autoscaler", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Horizontal Pod Autoscaler by name", + "list": true, + "listDescription": "List all Horizontal Pod Autoscalers", + "search": true, + "searchDescription": "Search for a Horizontal Pod Autoscaler using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_horizontal_pod_autoscaler_v2.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Ingress.json b/docs.overmind.tech/docs/sources/k8s/data/Ingress.json new file mode 100644 index 00000000..0cd1a1ab --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Ingress.json @@ -0,0 +1,19 @@ +{ + "type": "Ingress", + "category": 3, + "potentialLinks": ["Service", "IngressClass", "dns"], + "descriptiveName": "Ingress", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Ingress by name", + "list": true, + "listDescription": "List all Ingresss", + "search": true, + "searchDescription": "Search for a Ingress using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_ingress_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Job.json b/docs.overmind.tech/docs/sources/k8s/data/Job.json new file mode 100644 index 00000000..65e9cec1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Job.json @@ -0,0 +1,22 @@ +{ + "type": "Job", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Job", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Job by name", + "list": true, + "listDescription": "List all Jobs", + "search": true, + "searchDescription": "Search for a Job using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_job.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_job_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json b/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json new file mode 100644 index 00000000..2d34fa2d --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/LimitRange.json @@ -0,0 +1,21 @@ +{ + "type": "LimitRange", + "category": 7, + "descriptiveName": "Limit Range", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Limit Range by name", + "list": true, + "listDescription": "List all Limit Ranges", + "search": true, + "searchDescription": "Search for a Limit Range using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_limit_range_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_limit_range.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json b/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json new file mode 100644 index 00000000..ae7f932e --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/NetworkPolicy.json @@ -0,0 +1,14 @@ +{ + "type": "NetworkPolicy", + "category": 4, + "potentialLinks": ["Pod"], + "descriptiveName": "Network Policy", + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_network_policy.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_network_policy_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Node.json b/docs.overmind.tech/docs/sources/k8s/data/Node.json new file mode 100644 index 00000000..67da5db0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Node.json @@ -0,0 +1,19 @@ +{ + "type": "Node", + "category": 1, + "potentialLinks": ["dns", "ip", "ec2-volume"], + "descriptiveName": "Node", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Node by name", + "list": true, + "listDescription": "List all Nodes", + "search": true, + "searchDescription": "Search for a Node using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_node_taint.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json new file mode 100644 index 00000000..cb128430 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolume.json @@ -0,0 +1,22 @@ +{ + "type": "PersistentVolume", + "category": 2, + "potentialLinks": ["ec2-volume", "efs-access-point", "StorageClass"], + "descriptiveName": "Persistent Volume", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PersistentVolume by name", + "list": true, + "listDescription": "List all PersistentVolumes", + "search": true, + "searchDescription": "Search for a PersistentVolume using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_persistent_volume.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_persistent_volume_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json new file mode 100644 index 00000000..4880294a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PersistentVolumeClaim.json @@ -0,0 +1,22 @@ +{ + "type": "PersistentVolumeClaim", + "category": 2, + "potentialLinks": ["PersistentVolume"], + "descriptiveName": "Persistent Volume Claim", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PersistentVolumeClaim by name", + "list": true, + "listDescription": "List all PersistentVolumeClaims", + "search": true, + "searchDescription": "Search for a PersistentVolumeClaim using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_persistent_volume_claim.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_persistent_volume_claim_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Pod.json b/docs.overmind.tech/docs/sources/k8s/data/Pod.json new file mode 100644 index 00000000..26e8d232 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Pod.json @@ -0,0 +1,31 @@ +{ + "type": "Pod", + "category": 1, + "potentialLinks": [ + "ConfigMap", + "ec2-volume", + "dns", + "ip", + "PersistentVolumeClaim", + "PriorityClass", + "Secret", + "ServiceAccount" + ], + "descriptiveName": "Pod", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Pod by name", + "list": true, + "listDescription": "List all Pods", + "search": true, + "searchDescription": "Search for a Pod using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_pod.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_pod_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json b/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json new file mode 100644 index 00000000..356471b5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PodDisruptionBudget.json @@ -0,0 +1,19 @@ +{ + "type": "PodDisruptionBudget", + "category": 7, + "potentialLinks": ["Pod"], + "descriptiveName": "Pod Disruption Budget", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a PodDisruptionBudget by name", + "list": true, + "listDescription": "List all PodDisruptionBudgets", + "search": true, + "searchDescription": "Search for a PodDisruptionBudget using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_pod_disruption_budget_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json b/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json new file mode 100644 index 00000000..bfc8f10a --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/PriorityClass.json @@ -0,0 +1,21 @@ +{ + "type": "PriorityClass", + "category": 7, + "descriptiveName": "Priority Class", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Priority Class by name", + "list": true, + "listDescription": "List all Priority Classs", + "search": true, + "searchDescription": "Search for a Priority Class using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_priority_class_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_priority_class.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json b/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json new file mode 100644 index 00000000..4e4900b9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ReplicaSet.json @@ -0,0 +1,14 @@ +{ + "type": "ReplicaSet", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Replica Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ReplicaSet by name", + "list": true, + "listDescription": "List all ReplicaSets", + "search": true, + "searchDescription": "Search for a ReplicaSet using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + } +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json b/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json new file mode 100644 index 00000000..3c859acd --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ReplicationController.json @@ -0,0 +1,22 @@ +{ + "type": "ReplicationController", + "category": 1, + "potentialLinks": ["Pod"], + "descriptiveName": "Replication Controller", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ReplicationController by name", + "list": true, + "listDescription": "List all ReplicationControllers", + "search": true, + "searchDescription": "Search for a ReplicationController using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_replication_controller.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_replication_controller_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json b/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json new file mode 100644 index 00000000..84d3c985 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ResourceQuota.json @@ -0,0 +1,21 @@ +{ + "type": "ResourceQuota", + "category": 7, + "descriptiveName": "Resource Quota", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Resource Quota by name", + "list": true, + "listDescription": "List all Resource Quotas", + "search": true, + "searchDescription": "Search for a Resource Quota using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_resource_quota_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_resource_quota.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Role.json b/docs.overmind.tech/docs/sources/k8s/data/Role.json new file mode 100644 index 00000000..9783695b --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Role.json @@ -0,0 +1,21 @@ +{ + "type": "Role", + "category": 4, + "descriptiveName": "Role", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Role by name", + "list": true, + "listDescription": "List all Roles", + "search": true, + "searchDescription": "Search for a Role using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_role_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_role.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json b/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json new file mode 100644 index 00000000..2104a973 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/RoleBinding.json @@ -0,0 +1,22 @@ +{ + "type": "RoleBinding", + "category": 4, + "potentialLinks": ["Role", "ClusterRole", "ServiceAccount", "User", "Group"], + "descriptiveName": "Role Binding", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a RoleBinding by name", + "list": true, + "listDescription": "List all RoleBindings", + "search": true, + "searchDescription": "Search for a RoleBinding using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_role_binding.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_role_binding_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Secret.json b/docs.overmind.tech/docs/sources/k8s/data/Secret.json new file mode 100644 index 00000000..bf72898c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Secret.json @@ -0,0 +1,21 @@ +{ + "type": "Secret", + "category": 7, + "descriptiveName": "Secret", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Secret by name", + "list": true, + "listDescription": "List all Secrets", + "search": true, + "searchDescription": "Search for a Secret using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_secret_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_secret.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/Service.json b/docs.overmind.tech/docs/sources/k8s/data/Service.json new file mode 100644 index 00000000..4f22397c --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/Service.json @@ -0,0 +1,22 @@ +{ + "type": "Service", + "category": 3, + "potentialLinks": ["Pod", "ip", "dns", "Endpoint"], + "descriptiveName": "Service", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Service by name", + "list": true, + "listDescription": "List all Services", + "search": true, + "searchDescription": "Search for a Service using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_service.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_service_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json b/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json new file mode 100644 index 00000000..f5920a8f --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/ServiceAccount.json @@ -0,0 +1,22 @@ +{ + "type": "ServiceAccount", + "category": 4, + "potentialLinks": ["Secret"], + "descriptiveName": "Service Account", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a ServiceAccount by name", + "list": true, + "listDescription": "List all ServiceAccounts", + "search": true, + "searchDescription": "Search for a ServiceAccount using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_service_account.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_service_account_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json b/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json new file mode 100644 index 00000000..b7c59ab6 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/StatefulSet.json @@ -0,0 +1,21 @@ +{ + "type": "StatefulSet", + "category": 1, + "descriptiveName": "Stateful Set", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Stateful Set by name", + "list": true, + "listDescription": "List all Stateful Sets", + "search": true, + "searchDescription": "Search for a Stateful Set using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_stateful_set_v1.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_stateful_set.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json b/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json new file mode 100644 index 00000000..c1a26a10 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/StorageClass.json @@ -0,0 +1,21 @@ +{ + "type": "StorageClass", + "category": 2, + "descriptiveName": "Storage Class", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a Storage Class by name", + "list": true, + "listDescription": "List all Storage Classs", + "search": true, + "searchDescription": "Search for a Storage Class using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + }, + "terraformMappings": [ + { + "terraformQueryMap": "kubernetes_storage_class.metadata[0].name" + }, + { + "terraformQueryMap": "kubernetes_storage_class_v1.metadata[0].name" + } + ] +} diff --git a/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json b/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json new file mode 100644 index 00000000..7d1a59b0 --- /dev/null +++ b/docs.overmind.tech/docs/sources/k8s/data/VolumeAttachment.json @@ -0,0 +1,14 @@ +{ + "type": "VolumeAttachment", + "category": 2, + "potentialLinks": ["PersistentVolume", "Node"], + "descriptiveName": "Volume Attachment", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get a VolumeAttachment by name", + "list": true, + "listDescription": "List all VolumeAttachments", + "search": true, + "searchDescription": "Search for a VolumeAttachment using the ListOptions JSON format e.g. {\"labelSelector\": \"app=wordpress\"}" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md b/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md new file mode 100644 index 00000000..626f922a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/certificate.md @@ -0,0 +1,13 @@ +--- +title: Certificate +sidebar_label: certificate +--- + +A Certificate resource represents an X.509 public-key certificate (typically served during a TLS/SSL handshake) together with any intermediate certificates that form its trust chain. Overmind analyses these certificates to surface risks such as imminent expiry, weak signature algorithms, incorrect key usage flags, or hostnames that do not match the Subject Alternative Names (SANs). +For the formal specification of X.509 certificates, see RFC 5280 – Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile: https://datatracker.ietf.org/doc/html/rfc5280 + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Takes a full certificate, or certificate bundle as input in PEM encoded format diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/dns.md b/docs.overmind.tech/docs/sources/stdlib/Types/dns.md new file mode 100644 index 00000000..e4193d4c --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/dns.md @@ -0,0 +1,27 @@ +--- +title: DNS Entry +sidebar_label: dns +--- + +The Domain Name System (DNS) translates human-readable names into machine-usable information. A DNS _A_ record maps a hostname to an IPv4 address, while an _AAAA_ record maps it to an IPv6 address. By querying these records, Overmind can reveal the infrastructure that a name ultimately points to, allowing you to spot configuration mistakes, dangling records, or unexpected dependencies before you deploy. +Reference documentation: RFC 1034 & RFC 1035 – Domain Names – Concepts and Facilities / Implementation and Specification (https://www.rfc-editor.org/rfc/rfc1034 and https://www.rfc-editor.org/rfc/rfc1035) + +## Supported Methods + +- `GET`: A DNS A or AAAA entry to look up +- ~~`LIST`~~ +- `SEARCH`: A DNS name (or IP for reverse DNS), this will perform a recursive search and return all results. It is recommended that you always use the SEARCH method + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +If the queried record is a CNAME, MX, NS or contains additional glue, Overmind follows those pointers and links the resulting records back as further `dns` items for deeper traversal. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +An A or AAAA record resolves to one or more IP addresses; each discovered address is linked as an `ip` item so their ownership, location and associated services can be examined. + +### [`rdap-domain`](/sources/stdlib/Types/rdap-domain) + +The second-level or higher-level domain extracted from the DNS name is linked to its corresponding `rdap-domain` item, giving visibility into registrar, registrant and name-server information that may present additional risk factors. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/http.md b/docs.overmind.tech/docs/sources/stdlib/Types/http.md new file mode 100644 index 00000000..5ae0276c --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/http.md @@ -0,0 +1,31 @@ +--- +title: HTTP Endpoint +sidebar_label: http +--- + +An HTTP Endpoint represents a reachable URL that Overmind can interrogate in order to discover configuration or security issues before deployment. By performing lightweight `HEAD` or `GET` requests, Overmind determines the availability, response headers, redirects, and TLS configuration (if the endpoint is served over HTTPS). This allows you to spot problems such as broken links, unexpected redirections, missing security headers, or invalid certificates early in the pipeline. +For more background on how HTTP endpoints are conventionally exposed and managed on the internet, refer to the W3C documentation on HTTP semantics: https://www.w3.org/Protocols/ (external). + +## Supported Methods + +- `GET`: A HTTP endpoint to run a `HEAD` request against +- ~~`LIST`~~ +- `SEARCH`: A HTTP URL to search for. Query parameters and fragments will be stripped from the URL before processing. + +## Possible Links + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The hostname or FQDN of the HTTP endpoint ultimately resolves to one or more IP addresses. Overmind records these addresses to understand network-level reachability and to cross-reference them with firewall, VPC or load-balancer configurations. + +### [`dns`](/sources/stdlib/Types/dns) + +Before an HTTP request can be made, the client performs a DNS lookup. Overmind connects the endpoint to its corresponding DNS records (A, AAAA, CNAME, etc.) so you can see how changes in DNS zone files might affect the endpoint’s availability. + +### [`certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) + +If the endpoint is accessed over HTTPS, the server presents an X.509 certificate. Overmind links the endpoint to the certificate resource it observes during the TLS handshake, enabling validation of expiry dates, issuer trust chains, and key strengths. + +### [`http`](/sources/stdlib/Types/http) + +HTTP endpoints often redirect to, embed, or call other HTTP endpoints (for example via 3xx redirects or links in HTML/JSON responses). Overmind establishes links between them so you can trace dependencies, spot redirect loops, and ensure downstream endpoints meet your security standards. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/ip.md b/docs.overmind.tech/docs/sources/stdlib/Types/ip.md new file mode 100644 index 00000000..693acc9a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/ip.md @@ -0,0 +1,23 @@ +--- +title: IP Address +sidebar_label: ip +--- + +An IP address is a numerical label assigned to every device connected to an Internet Protocol network. It uniquely identifies the source and destination of traffic and is used for routing packets across interconnected networks. Overmind treats each IPv4 or IPv6 address that appears in your configuration as a discrete resource, allowing you to map how code, infrastructure and third-party services depend on it and to identify security or availability risks before deployment. +Official specification documents can be found in the relevant IETF RFCs: IPv4 is defined in RFC 791 and IPv6 in RFC 8200 (see https://www.rfc-editor.org/rfc/rfc791 and https://www.rfc-editor.org/rfc/rfc8200). + +## Supported Methods + +- `GET`: An ipv4 or ipv6 address +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +DNS records (such as A, AAAA or PTR) map human-readable hostnames to this IP address or resolve the address back to a hostname. Overmind links the `ip` resource to `dns` items whenever the address appears in one of these records so that you can trace how name resolution affects your deployment. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +Querying the Registration Data Access Protocol (RDAP) for an IP returns information about the allocation block, the organisation that owns it, contact details and abuse mailboxes. Overmind links an `ip` to the corresponding `rdap-ip-network` resource to surface ownership, geolocation and abuse-handling context that may influence compliance or threat-modelling decisions. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md new file mode 100644 index 00000000..2ad5fc1a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-asn.md @@ -0,0 +1,18 @@ +--- +title: Autonomous System Number (ASN) +sidebar_label: rdap-asn +--- + +An Autonomous System Number (ASN) is a unique 16- or 32-bit identifier assigned to an Autonomous System so that it can participate in Border Gateway Protocol (BGP) routing on the public Internet. Using the Registration Data Access Protocol (RDAP), you can query an ASN to obtain registration details such as the holder, allocation status, and associated contacts. For the formal specification of RDAP responses for ASNs, see [RFC 9083: Registration Data Access Protocol (RDAP)](https://datatracker.ietf.org/doc/html/rfc9083). + +## Supported Methods + +- `GET`: Get an ASN by handle i.e. "AS15169" +- ~~`LIST`~~ +- ~~`SEARCH`~~ + +## Possible Links + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +An ASN RDAP record frequently contains an `entities` array. Each item in that array is an RDAP Entity object representing the organisation or individual responsible for the ASN (registrant, administrative contact, technical contact, etc.). Overmind therefore links an `rdap-asn` resource to one or more `rdap-entity` resources so that you can inspect the people or organisations behind a particular network. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md new file mode 100644 index 00000000..f9f21abd --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-domain.md @@ -0,0 +1,31 @@ +--- +title: RDAP Domain +sidebar_label: rdap-domain +--- + +An RDAP Domain record represents the authoritative registration data for a domain name as returned by the Registration Data Access Protocol (RDAP). The record contains information such as the registrar, registrant and administrative contacts, name-servers, status flags (e.g. `clientTransferProhibited`), and important lifecycle dates (creation, expiry, last update). In Overmind the resource lets you inspect this registration data and understand how a domain fits into the rest of your deployment before any changes are made. +Official RDAP specification: https://www.rfc-editor.org/rfc/rfc9082 + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for a domain record by the domain name e.g. "www.google.com" + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +The name portion of the RDAP domain (e.g. `example.com`) will typically have authoritative DNS records such as `A`, `AAAA`, `MX`, etc. Overmind links the RDAP Domain to those `dns` items so that you can trace from the registration layer straight through to the operational zone file that will actually be served. + +### [`rdap-nameserver`](/sources/stdlib/Types/rdap-nameserver) + +An RDAP Domain record contains a list of host objects (name-servers) delegated for the zone. Each of those host objects is represented as an `rdap-nameserver` item. The link allows you to drill into the registration data for each individual name-server. + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +Entities in RDAP describe people or organisations such as the registrant, administrative contact, or registrar. Overmind links the RDAP Domain to every referenced `rdap-entity` so that you can view contact details, roles and other domains controlled by the same party. + +### [`rdap-ip-network`](/sources/stdlib/Types/rdap-ip-network) + +If the RDAP Domain record (or any of its linked name-servers) includes embedded references to address space—commonly via `v4network` or `v6network` objects—Overmind exposes those as `rdap-ip-network` items. This lets you see which blocks of IP addresses are directly associated with the domain and whether they overlap with other infrastructure you manage. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md new file mode 100644 index 00000000..1989ba3e --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-entity.md @@ -0,0 +1,20 @@ +--- +title: RDAP Entity +sidebar_label: rdap-entity +--- + +An RDAP (Registration Data Access Protocol) Entity resource represents a single contact object – either a person, organisation or role – that appears in the registration data held by Regional Internet Registries (RIRs) and other RDAP servers. It typically contains identifying information such as names, postal addresses, e-mail addresses, telephone numbers and public identifiers, and is referenced by other RDAP objects (e.g. ASNs, IPv4/IPv6 prefix ranges and domain names) as their administrative, technical or abuse contact. + +The formal structure and semantics of an RDAP Entity are defined in RFC 9083, section 5.1 (https://www.rfc-editor.org/rfc/rfc9083#section-5.1). + +## Supported Methods + +- `GET`: Get an entity by its handle. This method is discouraged as it's not reliable since entity bootstrapping isn't comprehensive +- ~~`LIST`~~ +- `SEARCH`: Search for an entity by its URL e.g. https://rdap.apnic.net/entity/AIC3-AP + +## Possible Links + +### [`rdap-asn`](/sources/stdlib/Types/rdap-asn) + +An ASN record can reference one or more RDAP Entities as its registrant, administrative or technical contacts. Overmind links the rdap-asn resource to the corresponding rdap-entity resources so that you can see who is responsible for a particular Autonomous System and assess any associated risk or exposure stemming from those contacts. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md new file mode 100644 index 00000000..efe67c03 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-ip-network.md @@ -0,0 +1,19 @@ +--- +title: RDAP IP Network +sidebar_label: rdap-ip-network +--- + +An **RDAP IP Network** represents a block of IPv4 or IPv6 address space as returned by the Registration Data Access Protocol (RDAP). Overmind queries the authoritative RDAP service for a supplied address or prefix and surfaces the resulting network object, revealing who owns the range, the exact start- and end-addresses, its allocation status (allocated, assigned, reserved, etc.), and any policy or abuse information attached to it. Seeing this data in advance helps you verify that the addresses your deployment will use are valid and not bogon, reserved, or owned by an unexpected party. +The RDAP specification for IP networks is defined in [RFC 9083 – Registration Data Access Protocol (RDAP): Query Format](https://datatracker.ietf.org/doc/html/rfc9083). + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for the most specific network that contains the specified IP or CIDR + +## Possible Links + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +An RDAP network record contains an `entities` array referencing the people, organisations, and roles (registrant, technical, abuse, etc.) responsible for the address space. Overmind links each of these references to its corresponding `rdap-entity` item, letting you inspect contact details and responsibility assignments related to the network. diff --git a/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md new file mode 100644 index 00000000..663aa01a --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/Types/rdap-nameserver.md @@ -0,0 +1,28 @@ +--- +title: RDAP Nameserver +sidebar_label: rdap-nameserver +--- + +The Registration Data Access Protocol (RDAP) is the modern, machine-readable replacement for the old WHOIS service. +An **RDAP ­nameserver resource** represents the authoritative information that a Top-Level Domain (TLD) registry publishes about a particular DNS nameserver. By querying this endpoint you can discover, for example, the registrar that manages the server, its associated IP addresses, its status with the registry and any abuse or support contacts. +For details of the protocol and the structure of a nameserver response, see the IETF specification: https://datatracker.ietf.org/doc/html/rfc7483#section-5.5. + +## Supported Methods + +- ~~`GET`~~ +- ~~`LIST`~~ +- `SEARCH`: Search for the RDAP entry for a nameserver by its full URL e.g. "https://rdap.verisign.com/com/v1/nameserver/NS4.GOOGLE.COM" + +## Possible Links + +### [`dns`](/sources/stdlib/Types/dns) + +A nameserver appears in DNS NS records. Overmind links the RDAP nameserver object to the corresponding `dns` item so that you can see which zones delegate to this server and whether those zones are also in your inventory. + +### [`ip`](/sources/aws/Types/networkmanager-network-resource-relationship) + +The RDAP response normally includes the A and/or AAAA records for the nameserver. These addresses are represented as `ip` items, allowing you to trace from the logical nameserver to the concrete IP resources that sit behind it. + +### [`rdap-entity`](/sources/stdlib/Types/rdap-entity) + +Each nameserver RDAP document references one or more entities (registrar, registrant, technical contact, abuse contact, etc.). These are captured as separate `rdap-entity` items and linked so you can quickly identify who is responsible for the server and how to contact them. diff --git a/docs.overmind.tech/docs/sources/stdlib/_category_json b/docs.overmind.tech/docs/sources/stdlib/_category_json new file mode 100644 index 00000000..35c138aa --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/_category_json @@ -0,0 +1,9 @@ +{ + "label": "Public Resources (stdlib)", + "position": 3, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to explore with Overminds built-in Public Resoures." + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/certificate.json b/docs.overmind.tech/docs/sources/stdlib/data/certificate.json new file mode 100644 index 00000000..093db560 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/certificate.json @@ -0,0 +1,9 @@ +{ + "type": "certificate", + "category": 3, + "descriptiveName": "Certificate", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Takes a full certificate, or certificate bundle as input in PEM encoded format" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/dns.json b/docs.overmind.tech/docs/sources/stdlib/data/dns.json new file mode 100644 index 00000000..4bac0bc7 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/dns.json @@ -0,0 +1,12 @@ +{ + "type": "dns", + "category": 3, + "potentialLinks": ["dns", "ip", "rdap-domain"], + "descriptiveName": "DNS Entry", + "supportedQueryMethods": { + "get": true, + "getDescription": "A DNS A or AAAA entry to look up", + "search": true, + "searchDescription": "A DNS name (or IP for reverse DNS), this will perform a recursive search and return all results. It is recommended that you always use the SEARCH method" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/http.json b/docs.overmind.tech/docs/sources/stdlib/data/http.json new file mode 100644 index 00000000..a7c70fb1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/http.json @@ -0,0 +1,12 @@ +{ + "type": "http", + "category": 3, + "potentialLinks": ["ip", "dns", "certificate", "http"], + "descriptiveName": "HTTP Endpoint", + "supportedQueryMethods": { + "get": true, + "getDescription": "A HTTP endpoint to run a `HEAD` request against", + "search": true, + "searchDescription": "A HTTP URL to search for. Query parameters and fragments will be stripped from the URL before processing." + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/ip.json b/docs.overmind.tech/docs/sources/stdlib/data/ip.json new file mode 100644 index 00000000..e0840b7b --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/ip.json @@ -0,0 +1,10 @@ +{ + "type": "ip", + "category": 3, + "potentialLinks": ["dns", "rdap-ip-network"], + "descriptiveName": "IP Address", + "supportedQueryMethods": { + "get": true, + "getDescription": "An ipv4 or ipv6 address" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json new file mode 100644 index 00000000..41fe8522 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-asn.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-asn", + "category": 3, + "potentialLinks": ["rdap-entity"], + "descriptiveName": "Autonomous System Number (ASN)", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an ASN by handle i.e. \"AS15169\"" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json new file mode 100644 index 00000000..6db55b25 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-domain.json @@ -0,0 +1,15 @@ +{ + "type": "rdap-domain", + "category": 3, + "potentialLinks": [ + "dns", + "rdap-nameserver", + "rdap-entity", + "rdap-ip-network" + ], + "descriptiveName": "RDAP Domain", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for a domain record by the domain name e.g. \"www.google.com\"" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json new file mode 100644 index 00000000..705be5aa --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-entity.json @@ -0,0 +1,12 @@ +{ + "type": "rdap-entity", + "category": 4, + "potentialLinks": ["rdap-asn"], + "descriptiveName": "RDAP Entity", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get an entity by its handle. This method is discouraged as it's not reliable since entity bootstrapping isn't comprehensive", + "search": true, + "searchDescription": "Search for an entity by its URL e.g. https://rdap.apnic.net/entity/AIC3-AP" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json new file mode 100644 index 00000000..e08b0f84 --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-ip-network.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-ip-network", + "category": 3, + "potentialLinks": ["rdap-entity"], + "descriptiveName": "RDAP IP Network", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for the most specific network that contains the specified IP or CIDR" + } +} diff --git a/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json b/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json new file mode 100644 index 00000000..9e1d927d --- /dev/null +++ b/docs.overmind.tech/docs/sources/stdlib/data/rdap-nameserver.json @@ -0,0 +1,10 @@ +{ + "type": "rdap-nameserver", + "category": 3, + "potentialLinks": ["dns", "ip", "rdap-entity"], + "descriptiveName": "RDAP Nameserver", + "supportedQueryMethods": { + "search": true, + "searchDescription": "Search for the RDAP entry for a nameserver by its full URL e.g. \"https://rdap.verisign.com/com/v1/nameserver/NS4.GOOGLE.COM\"" + } +} diff --git a/go.mod b/go.mod index c076d262..1438a278 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( cloud.google.com/go/bigquery v1.73.1 cloud.google.com/go/bigtable v1.42.0 cloud.google.com/go/compute v1.54.0 - cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/compute/metadata v0.9.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 @@ -37,6 +37,8 @@ require ( cloud.google.com/go/spanner v1.87.0 cloud.google.com/go/storagetransfer v1.13.1 connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version + connectrpc.com/otelconnect v0.9.0 + github.com/1password/onepassword-sdk-go v0.3.1 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 @@ -53,6 +55,12 @@ require ( 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/a-h/templ v0.3.977 + github.com/adrg/strutil v0.3.1 + github.com/akedrou/textdiff v0.1.0 + github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 + github.com/antihax/optional v1.0.0 + github.com/auth0/go-auth0/v2 v2.4.0 github.com/auth0/go-jwt-middleware/v2 v2.3.1 github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -78,47 +86,81 @@ require ( 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.96.0 + github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1 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 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 github.com/aws/smithy-go v1.24.0 + github.com/bombsimon/logrusr/v4 v4.1.0 + github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 + github.com/brianvoe/gofakeit/v7 v7.14.0 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/exaring/otelpgx v0.10.0 github.com/getsentry/sentry-go v0.42.0 github.com/go-jose/go-jose/v4 v4.1.3 + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/btree v1.1.3 + github.com/google/go-github/v80 v80.0.0 github.com/google/uuid v1.6.0 github.com/googleapis/gax-go/v2 v2.17.0 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e + github.com/gorilla/mux v1.8.1 + github.com/harness/harness-go-sdk v0.7.6 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 + github.com/invopop/jsonschema v0.13.0 + github.com/jackc/pgx/v5 v5.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 + github.com/jxskiss/base62 v1.1.0 + github.com/kaptinlin/jsonrepair v0.2.7 + github.com/manifoldco/promptui v0.9.0 + github.com/mavolin/go-htmx v1.0.0 + github.com/mergestat/timediff v0.0.4 github.com/micahhausler/aws-iam-policy v0.4.2 github.com/miekg/dns v1.1.72 github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/go-ps v1.0.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.4 github.com/nats-io/nats.go v1.48.0 github.com/nats-io/nkeys v0.4.15 - github.com/onsi/ginkgo/v2 v2.28.1 // indirect - github.com/onsi/gomega v1.39.1 // indirect + github.com/neo4j/neo4j-go-driver/v6 v6.0.0 + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 + github.com/openai/openai-go/v3 v3.18.0 github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 + github.com/pborman/ansi v1.0.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/posthog/posthog-go v1.10.0 + github.com/projectdiscovery/subfinder/v2 v2.12.0 + github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf + github.com/riverqueue/river v0.30.2 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2 + github.com/riverqueue/river/rivertype v0.30.2 + github.com/riverqueue/rivercontrib/otelriver v0.7.0 + github.com/rs/cors v1.11.1 + github.com/samber/slog-logrus/v2 v2.5.3 + github.com/sashabaranov/go-openai v1.41.2 + github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/stripe/stripe-go/v84 v84.3.0 + github.com/tiktoken-go/tokenizer v0.7.0 github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 + github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b github.com/zclconf/go-cty v1.17.0 go.etcd.io/bbolt v1.4.3 @@ -148,11 +190,16 @@ require ( k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 + k8s.io/component-base v0.35.0 k8s.io/utils v0.0.0-20260108192941-914a6e750570 + modernc.org/sqlite v1.44.3 + riverqueue.com/riverui v0.14.0 + sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/kind v0.31.0 ) require ( + aead.dev/minisign v0.2.0 // indirect al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/schedule v0.1.0 // indirect @@ -163,14 +210,22 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect + github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect + github.com/PuerkitoBio/rehttp v1.4.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect + github.com/akrylysov/pogreb v0.10.1 // indirect github.com/alecthomas/chroma/v2 v2.16.0 // indirect github.com/alecthomas/kingpin/v2 v2.4.0 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect @@ -186,6 +241,14 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect; being pulled by glamour, this will be resolved in https://github.com/charmbracelet/glamour/pull/408 @@ -193,46 +256,98 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cheggaaa/pb/v3 v3.1.4 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/containerd/console v1.0.4 // indirect + github.com/corpix/uarand v0.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/extism/go-sdk v1.7.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gaissmai/bart v0.20.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-github/v30 v30.1.0 // indirect + github.com/google/go-github/v75 v75.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.8 // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/s2a-go v0.1.9 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.3 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mholt/archives v0.1.0 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect + github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect @@ -240,34 +355,97 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkoukk/tiktoken-go v0.1.7 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/projectdiscovery/blackrock v0.0.1 // indirect + github.com/projectdiscovery/cdncheck v1.1.24 // indirect + github.com/projectdiscovery/chaos-client v0.5.2 // indirect + github.com/projectdiscovery/dnsx v1.2.2 // indirect + github.com/projectdiscovery/fastdialer v0.4.1 // indirect + github.com/projectdiscovery/goflags v0.1.74 // indirect + github.com/projectdiscovery/gologger v1.1.54 // indirect + github.com/projectdiscovery/hmap v0.0.90 // indirect + github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect + github.com/projectdiscovery/networkpolicy v0.1.16 // indirect + github.com/projectdiscovery/ratelimit v0.0.81 // indirect + github.com/projectdiscovery/retryabledns v1.0.102 // indirect + github.com/projectdiscovery/retryablehttp-go v1.0.115 // indirect + github.com/projectdiscovery/utils v0.4.21 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/refraction-networking/utls v1.7.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e // indirect + github.com/riverqueue/river/riverdriver v0.30.2 // indirect + github.com/riverqueue/river/rivershared v0.30.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/samber/slog-common v0.20.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/shirou/gopsutil/v3 v3.23.7 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/btree v1.6.0 // indirect + github.com/tidwall/buntdb v1.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/grect v0.1.4 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/rtred v0.1.2 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/tidwall/tinyqueue v0.1.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/weppos/publicsuffix-go v0.30.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.10 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zcalusic/sysinfo v1.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect + github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect + go.devnw.com/structs v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.32.0 // indirect @@ -277,14 +455,22 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + gopkg.in/djherbis/times.v1 v1.3.0 // 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 + k8s.io/apiextensions-apiserver v0.35.0 // indirect + k8s.io/apiserver v0.35.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 15ea0ab2..39c77868 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= +aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= @@ -14,6 +16,15 @@ buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/aiplatform v1.115.0 h1:m/dIJ/HixZDvHoXBGkA5Sd0RbiQp5lBVyddvR9uxHqI= @@ -22,12 +33,15 @@ cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.73.1 h1:v//GZwdhtmCbZ87rOnxz7pectOGFS1GNRvrGTvLzka4= cloud.google.com/go/bigquery v1.73.1/go.mod h1:KSLx1mKP/yGiA8U+ohSrqZM1WknUnjZAxHAQZ51/b1k= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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.46.0 h1:xX94Lo3xrS5OkdMWKvpEVAbBwjN9uleVv6vOi02fL4s= @@ -38,6 +52,7 @@ cloud.google.com/go/dataplex v1.28.0 h1:rROI3iqMVI9nXT701ULoFRETQVAOAPC3mPSWFDxX cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= cloud.google.com/go/dataproc/v2 v2.15.0 h1:I/Yux/d8uaxf3W+d59kolGTOc52+VZaL6RzJw7oDOeg= cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/filestore v1.10.3 h1:3KZifUVTqGhNNv6MLeONYth1HjlVM4vDhaH+xrdPljU= cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= cloud.google.com/go/functions v1.19.7 h1:7LcOD18euIVGRUPaeCmgO6vfWSLNIsi6STWRQcdANG8= @@ -56,6 +71,8 @@ cloud.google.com/go/networksecurity v0.11.0 h1:+ahtCqEqwHw3a3UIeG21vT817xt9kkDDA cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM= cloud.google.com/go/orgpolicy v1.15.1 h1:0hq12wxNwcfUMojr5j3EjWECSInIuyYDhkAWXTomRhc= cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/redis v1.18.3 h1:6LI8zSt+vmE3WQ7hE5GsJ13CbJBLV1qUw6B7CY31Wcw= cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= cloud.google.com/go/resourcemanager v1.10.7 h1:oPZKIdjyVTuag+D4HF7HO0mnSqcqgjcuA18xblwA0V0= @@ -68,12 +85,19 @@ cloud.google.com/go/securitycentermanagement v1.1.6 h1:XFqjKq4ZpKTj8xCXWs/mTmh/U cloud.google.com/go/securitycentermanagement v1.1.6/go.mod h1:nt5Z6rU4s2/j8R/EQxG5K7OfVAfAfwo89j0Nx2Srzaw= cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/storagetransfer v1.13.1 h1:Sjukr1LtUt7vLTHNvGc2gaAqlXNFeDFRIRmWGrFaJlY= cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= +connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc= +github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -116,8 +140,10 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ 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= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= @@ -137,8 +163,27 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MrAlias/otel-schema-utils v0.4.0-alpha h1:6ZG9rw4NvxKwRp2Bmnfr8WJZVWLhK4e5n3+ezXE6Z2g= github.com/MrAlias/otel-schema-utils v0.4.0-alpha/go.mod h1:baehOhES9qiLv9xMcsY6ZQlKLBRR89XVJEvU7Yz3qJk= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/PuerkitoBio/rehttp v1.4.0 h1:rIN7A2s+O9fmHUM1vUcInvlHj9Ysql4hE+Y0wcl/xk8= +github.com/PuerkitoBio/rehttp v1.4.0/go.mod h1:LUwKPoDbDIA2RL5wYZCNsQ90cx4OJ4AWBmq6KzWZL1s= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= +github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= +github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/akedrou/textdiff v0.1.0 h1:K7nbOVQju7/coCXnJRJ2fsltTwbSvC+M4hKBUJRBRGY= +github.com/akedrou/textdiff v0.1.0/go.mod h1:a9CCC49AKtFTmVDNFHDlCg7V/M7C7QExDAhb2SkL6DQ= +github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= +github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= @@ -149,6 +194,12 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 h1:TdGQS+RoR4AUO6gqUL74yK1dz/Arrt/WG+dxOj6Yo6A= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -157,7 +208,11 @@ github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcy github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/auth0/go-auth0/v2 v2.4.0 h1:5fy8XAh/nlfaOppYWanJQk3M0hhE5433cQCt/Gfsktw= +github.com/auth0/go-auth0/v2 v2.4.0/go.mod h1:4GvvYxDvjiOoglqYngCUOUZWzE4iXNczLID5NDA2tYg= github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw= github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= @@ -228,6 +283,8 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0D github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1 h1:0Pitfk3kTCUeJp+7xvTYhdgwVQhszqw1i4s8U93Z/ds= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1/go.mod h1:lm1VCfakGKIqjexled4IMNMxgOQpDk7buAFd+7lr9pA= 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= @@ -244,16 +301,48 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= +github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= +github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk= +github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= @@ -274,43 +363,97 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 h1:8fUBS github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= +github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE= +github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZc= +github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U= +github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo= +github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gaissmai/bart v0.20.4 h1:Ik47r1fy3jRVU+1eYzKSW3ho2UgBVTVnUS8O993584U= +github.com/gaissmai/bart v0.20.4/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk= github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8= github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -318,24 +461,63 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= @@ -344,15 +526,43 @@ github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8i github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= +github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= +github.com/google/go-github/v50 v50.1.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= +github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= +github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= +github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs= +github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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= @@ -361,6 +571,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -371,14 +583,29 @@ github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IP github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd h1:FsX+T6wA8spPe4c1K9vi7T0LvNCO1TTqiL8u7Wok2hw= +github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= +github.com/harness/harness-go-sdk v0.7.6 h1:xCUafSFqAMk6Dt5P5ay5JzBX7MuW2fycbW6UdgPj6KY= +github.com/harness/harness-go-sdk v0.7.6/go.mod h1:T3nqoDb9WUuGxPbh0Dt2jfytimCLmZ9rQ8bCJgOgnRE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= @@ -387,25 +614,59 @@ github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= +github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= +github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= +github.com/kaptinlin/jsonrepair v0.2.7 h1:UMBY3c+8kxoioh8wJFx2oy3UmUQzI8MwF8RrM26hfOg= +github.com/kaptinlin/jsonrepair v0.2.7/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 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 v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -415,20 +676,53 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= +github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mavolin/go-htmx v1.0.0 h1:43rZuemWd23zrMcTU939EsflXjOPxtHy9VraT1CZ6qQ= +github.com/mavolin/go-htmx v1.0.0/go.mod h1:r6O09gzKou9kutq3UiDPZ//Q7IeBCMcs8US5/sHFbvg= +github.com/mergestat/timediff v0.0.4 h1:NZ3sqG/6K9flhTubdltmRx3RBfIiYv6LsGP+4FlXMM8= +github.com/mergestat/timediff v0.0.4/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= +github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= github.com/micahhausler/aws-iam-policy v0.4.2 h1:HF7bERLnpqEmffV9/wTT4jZ7TbSNVk0JbpXo1Cj3up0= github.com/micahhausler/aws-iam-policy v0.4.2/go.mod h1:Ojgst9ZFn+VEEJpqtuw/LxVGqEf2+hwWBlkYWvF/XWM= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -437,8 +731,12 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -447,6 +745,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -465,14 +765,32 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/neo4j/neo4j-go-driver/v6 v6.0.0 h1:xVAi6YLOfzXUx+1Lc/F2dUhpbN76BfKleZbAlnDFRiA= +github.com/neo4j/neo4j-go-driver/v6 v6.0.0/go.mod h1:hzSTfNfM31p1uRSzL1F/BAYOgaiTarE6OAQBajfsm+I= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 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.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/openai/openai-go/v3 v3.18.0 h1:PpheJdvPgi8Ou77rJ1zsNmJTdmC7kvqDrGxbwAYq2nQ= +github.com/openai/openai-go/v3 v3.18.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 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= github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297/go.mod h1:bRQZYnvLrW1S5wYT6tbQnun8NpO5X6zP5cY3VKuDc4U= +github.com/pborman/ansi v1.0.0 h1:OqjHMhvlSuCCV5JT07yqPuJPQzQl+WXsiZ14gZsqOrQ= +github.com/pborman/ansi v1.0.0/go.mod h1:SgWzwMAx1X/Ez7i90VqF8LRiQtx52pWDiQP+x3iGnzw= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -485,13 +803,58 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= +github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posthog/posthog-go v1.10.0 h1:wfoy7Jfb4LigCoHYyMZoiJmmEoCLOkSaYfDxM/NtCqY= +github.com/posthog/posthog-go v1.10.0/go.mod h1:wB3/9Q7d9gGb1P/yf/Wri9VBlbP8oA8z++prRzL5OcY= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= +github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= +github.com/projectdiscovery/cdncheck v1.1.24 h1:6pJ4XnovIrTWzlCJs5/QD1tv6wvK0wiICmmdY0/8WAs= +github.com/projectdiscovery/cdncheck v1.1.24/go.mod h1:dFEGsG0qAJY0AaRr2N1BY0OtZiTxS4kYeT5+OkF8t1U= +github.com/projectdiscovery/chaos-client v0.5.2 h1:dN+7GXEypsJAbCD//dBcUxzAEAEH1fjc/7Rf4F/RiNU= +github.com/projectdiscovery/chaos-client v0.5.2/go.mod h1:KnoJ/NJPhll42uaqlDga6oafFfNw5l2XI2ajRijtDuU= +github.com/projectdiscovery/dnsx v1.2.2 h1:ZjUov0GOyrS8ERlKAAhk+AOkqzaYHBzCP0qZfO+6Ihg= +github.com/projectdiscovery/dnsx v1.2.2/go.mod h1:3iYm86OEqo0WxeGDkVl5WZNmG0qYE5TYNx8fBg6wX1I= +github.com/projectdiscovery/fastdialer v0.4.1 h1:kp6Q0odo0VZ0vZIGOn+q9aLgBSk6uYoD1MsjCAH8+h4= +github.com/projectdiscovery/fastdialer v0.4.1/go.mod h1:875Wlggf0JAz+fDIPwUQeeBqEF6nJA71XVrjuTZCV7I= +github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= +github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= +github.com/projectdiscovery/gologger v1.1.54 h1:WMzvJ8j/4gGfPKpCttSTaYCVDU1MWQSJnk3wU8/U6Ws= +github.com/projectdiscovery/gologger v1.1.54/go.mod h1:vza/8pe2OKOt+ujFWncngknad1XWr8EnLKlbcejOyUE= +github.com/projectdiscovery/hmap v0.0.90 h1:p8HWGvPI88hgJoAb4ayR1Oo5VzqPrOCdFG7mASUhQI4= +github.com/projectdiscovery/hmap v0.0.90/go.mod h1:dcjd9P82mkBpFGEy0wBU/3qql5Bx14kmJZvVg7o7vXY= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 h1:ZScLodGSezQVwsQDtBSMFp72WDq0nNN+KE/5DHKY5QE= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI= +github.com/projectdiscovery/networkpolicy v0.1.16 h1:H2VnLmMD7SvxF+rao+639nn8KX/kbPFY+mc8FxeltsI= +github.com/projectdiscovery/networkpolicy v0.1.16/go.mod h1:Vs/IRcJq4QUicjd/tl9gkhQWy7d/LssOwWbaz4buJ0U= +github.com/projectdiscovery/ratelimit v0.0.81 h1:u6lW+rAhS/UO0amHTYmYLipPK8NEotA9521hdojBtgI= +github.com/projectdiscovery/ratelimit v0.0.81/go.mod h1:tK04WXHuC4i6AsFkByInODSNf45gd9sfaMHzmy2bAsA= +github.com/projectdiscovery/retryabledns v1.0.102 h1:R8PzFCVofqLX3Bn4kdjOsE9wZ83FQjXZMDNs4/bHxzI= +github.com/projectdiscovery/retryabledns v1.0.102/go.mod h1:3+GL+YuHpV0Fp6UG7MbIG8mVxXHjfPO5ioQdwlnV08E= +github.com/projectdiscovery/retryablehttp-go v1.0.115 h1:ubIaVyHNj0/qxNv4gar+8/+L3G2Fhpfk54iMDctC7+E= +github.com/projectdiscovery/retryablehttp-go v1.0.115/go.mod h1:XlxLSMBVM7fTXeLVOLjVn1FLuRgQtD49NMFs9sQygfA= +github.com/projectdiscovery/subfinder/v2 v2.12.0 h1:MgEYn0F2qLvr63BWpV9jNjFiD8i9oXI3dp02tAGRft0= +github.com/projectdiscovery/subfinder/v2 v2.12.0/go.mod h1:FNy+bkJwZjUUWLte6T91IRBISqWDZ/q+ygUmoe8eb/w= +github.com/projectdiscovery/utils v0.4.21 h1:yAothTUSF6NwZ9yoC4iGe5gSBrovqKR9JwwW3msxk3Q= +github.com/projectdiscovery/utils v0.4.21/go.mod h1:HJuJFqjB6EmVaDl0ilFPKvLoMaX2GyE6Il2TqKXNs8I= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -501,22 +864,76 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.53 h1:8ERV5eXyvXlAIY8LRrhapPS34j7IKKDAnb7o1Ih3T0w= github.com/pterm/pterm v0.12.53/go.mod h1:BY2H3GtX2BX0ULqLY11C2CusIqnxsYerbkil3XvXIBg= +github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf h1:NxGxgo0KmC8w9fdn8jLCyG1SDrR/Vxbfa1nWErS3pmw= +github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf/go.mod h1:q6RK8Iv6obzk6i0rnLyYPtppwZ5uXJLloL3oxmfrwm8= +github.com/refraction-networking/utls v1.7.0 h1:9JTnze/Md74uS3ZWiRAabityY0un69rOLXsBf8LGgTs= +github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e h1:OwOgxT3MRpOj5Mp6DhFdZP43FOQOf2hhywAuT5XZCR4= +github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e/go.mod h1:O7UmsAMjpMYuToN4au5GNXdmN1gli+5FTldgXqAfaD0= +github.com/riverqueue/river v0.30.2 h1:RtJ3/CBat00Jjtllvy2P7A/QxSH3PRR0ri/B8PxWm1w= +github.com/riverqueue/river v0.30.2/go.mod h1:iPpsnw82MCcwAVhLo42g7eNdb5apT8VZ37Bel2x/Gws= +github.com/riverqueue/river/riverdriver v0.30.2 h1:JUmzh0iGPVpK4H7hugpgmQm2crOI9X4iKsd/9wz3IJk= +github.com/riverqueue/river/riverdriver v0.30.2/go.mod h1:w8DiNtR5uUfpIoNZVq1K7Xv0ER+1GrBK8nIxRFugiqI= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2 h1:nrz1NOLm9BXzTK96ANYmkiOXgjfD3+nLUbP7CrdSzY0= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2/go.mod h1:KmZHJvXC1eOXSHxJa3V0JKBI+sSYhhAxkAl7AKRQPXk= +github.com/riverqueue/river/rivershared v0.30.2 h1:LFGWnhFZIXNgooXVRY/+Of6bc9Z6ndZ8kf0A6hUO+8c= +github.com/riverqueue/river/rivershared v0.30.2/go.mod h1:K/DCaSKzbmVcOLC2PmaPycHdc56MMTZjU3LWiNh3yqQ= +github.com/riverqueue/river/rivertype v0.30.2 h1:9VVcrsXEPDFnl6qyOS0PxEoUSo9P5yD1E1HwyTpbXS8= +github.com/riverqueue/river/rivertype v0.30.2/go.mod h1:rWpgI59doOWS6zlVocROcwc00fZ1RbzRwsRTU8CDguw= +github.com/riverqueue/rivercontrib/otelriver v0.7.0 h1:zLjPf674dcGrz7OPG2JF5xea0fyitFax6Cc6q370Xzo= +github.com/riverqueue/rivercontrib/otelriver v0.7.0/go.mod h1:MuyMZmYBz3JXC8ZLP0dH9IqXK95qRY6gCQSoJGh9h7E= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= +github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= +github.com/samber/slog-logrus/v2 v2.5.3 h1:N6YGgQ9CQjUQXe75/iWKtE55EENjG67HYUsJQbPn/dE= +github.com/samber/slog-logrus/v2 v2.5.3/go.mod h1:W3njRsspuMRCd33S0ibPyK1ohRaMhuXKZ1BK8pNiM+c= +github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= +github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda h1:w3JksZEWJDI7x+No5yh2/8S86hq1dmJy7n5btakG30U= +github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda/go.mod h1:xVL4PnCuCPwkXhVVQysVrX3hEv7nWnIbfnDj2B+hsPw= +github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= +github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -535,10 +952,12 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -550,14 +969,71 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/stripe-go/v84 v84.3.0 h1:77HH+ro7yzmyyF7Xkbkj6y5QtnU1WWHC6t2y4mq0Wvk= +github.com/stripe/stripe-go/v84 v84.3.0/go.mod h1:Z4gcKw1zl4geDG2+cjpSaJES9jaohGX6n7FP8/kHIqw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= +github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= +github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw= +github.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db/go.mod h1:aiQaH1XpzIfgrJq3S1iw7w+3EDbRP7mF5fmwUhWyRUs= +github.com/weppos/publicsuffix-go v0.30.1 h1:8q+QwBS1MY56Zjfk/50ycu33NN8aa1iCCEQwo/71Oos= +github.com/weppos/publicsuffix-go v0.30.1/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= @@ -569,12 +1045,21 @@ github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc= +github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -583,8 +1068,24 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= +github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 h1:YOQ1vXEwE4Rnj+uQ/3oCuJk5wgVsvUyW+glsndwYuyA= +github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968/go.mod h1:xIuOvYCZX21S5Z9bK1BMrertTGX/F8hgAPw7ERJRNS0= +github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= +go.devnw.com/structs v1.0.0 h1:FFkBoBOkapCdxFEIkpOZRmMOMr9b9hxjKTD3bJYl9lk= +go.devnw.com/structs v1.0.0/go.mod h1:wHBkdQpNeazdQHszJ2sxwVEpd8zGTEsKkeywDLGbrmg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c h1:YSqSR1Fil5Ip0N6AlNBFbNv7cvIHZ2j4+wqbVafAGmQ= @@ -599,6 +1100,8 @@ go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= @@ -623,101 +1126,295 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.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-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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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.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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= +gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= +gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= +gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -725,18 +1422,66 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +riverqueue.com/riverui v0.14.0 h1:nDHvKywBSzgvnARjvberwGc5CgBMdIdQM4Mcci3+flU= +riverqueue.com/riverui v0.14.0/go.mod h1:uUwoeQGDO4+o4ofqenWL2UNuCED5/1/lwnkFKYR9vZw= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 h1:XotDXzqvJ8Nx5eiZZueLpTuafJz8SiodgOemI+w87QU= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= diff --git a/k8s-source/adapters/clusterrole.go b/k8s-source/adapters/clusterrole.go index 955436d4..365e319a 100644 --- a/k8s-source/adapters/clusterrole.go +++ b/k8s-source/adapters/clusterrole.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/clusterrole_test.go b/k8s-source/adapters/clusterrole_test.go index efb06ad2..9bd5982e 100644 --- a/k8s-source/adapters/clusterrole_test.go +++ b/k8s-source/adapters/clusterrole_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var clusterRoleYAML = ` diff --git a/k8s-source/adapters/clusterrolebinding.go b/k8s-source/adapters/clusterrolebinding.go index 17d479f0..07b38cd4 100644 --- a/k8s-source/adapters/clusterrolebinding.go +++ b/k8s-source/adapters/clusterrolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/clusterrolebinding_test.go b/k8s-source/adapters/clusterrolebinding_test.go index db5adaed..32e28854 100644 --- a/k8s-source/adapters/clusterrolebinding_test.go +++ b/k8s-source/adapters/clusterrolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var clusterRoleBindingYAML = ` diff --git a/k8s-source/adapters/configmap.go b/k8s-source/adapters/configmap.go index a25c99cd..d43efd41 100644 --- a/k8s-source/adapters/configmap.go +++ b/k8s-source/adapters/configmap.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/configmap_test.go b/k8s-source/adapters/configmap_test.go index 51d7b117..b056f668 100644 --- a/k8s-source/adapters/configmap_test.go +++ b/k8s-source/adapters/configmap_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var configMapYAML = ` diff --git a/k8s-source/adapters/cronjob.go b/k8s-source/adapters/cronjob.go index 7180f115..9fef5f7d 100644 --- a/k8s-source/adapters/cronjob.go +++ b/k8s-source/adapters/cronjob.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/batch/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/cronjob_test.go b/k8s-source/adapters/cronjob_test.go index e8c6f476..8d35b85e 100644 --- a/k8s-source/adapters/cronjob_test.go +++ b/k8s-source/adapters/cronjob_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var cronJobYAML = ` diff --git a/k8s-source/adapters/daemonset.go b/k8s-source/adapters/daemonset.go index 091b62bd..be512de0 100644 --- a/k8s-source/adapters/daemonset.go +++ b/k8s-source/adapters/daemonset.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/daemonset_test.go b/k8s-source/adapters/daemonset_test.go index bedea3fa..b923b4e2 100644 --- a/k8s-source/adapters/daemonset_test.go +++ b/k8s-source/adapters/daemonset_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var daemonSetYAML = ` diff --git a/k8s-source/adapters/deployment.go b/k8s-source/adapters/deployment.go index 12916a45..eb57734c 100644 --- a/k8s-source/adapters/deployment.go +++ b/k8s-source/adapters/deployment.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/deployment_test.go b/k8s-source/adapters/deployment_test.go index 53219651..60f3b615 100644 --- a/k8s-source/adapters/deployment_test.go +++ b/k8s-source/adapters/deployment_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var deploymentYAML = ` diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index 556a9b74..b98fe2c9 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/endpoints_test.go b/k8s-source/adapters/endpoints_test.go index cff9109a..f6f83b07 100644 --- a/k8s-source/adapters/endpoints_test.go +++ b/k8s-source/adapters/endpoints_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var endpointsYAML = ` diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index de68e992..d9d096d4 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -5,9 +5,9 @@ import ( v1 "k8s.io/api/discovery/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/endpointslice_test.go b/k8s-source/adapters/endpointslice_test.go index 1d41389f..238fcc42 100644 --- a/k8s-source/adapters/endpointslice_test.go +++ b/k8s-source/adapters/endpointslice_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var endpointSliceYAML = ` diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index 5ca362a8..c6e2e78d 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" corev1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/k8s-source/adapters/generic_source_test.go b/k8s-source/adapters/generic_source_test.go index 7fcf5eb8..0cc9e342 100644 --- a/k8s-source/adapters/generic_source_test.go +++ b/k8s-source/adapters/generic_source_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" diff --git a/k8s-source/adapters/horizontalpodautoscaler.go b/k8s-source/adapters/horizontalpodautoscaler.go index db7d74ff..1ef1d84d 100644 --- a/k8s-source/adapters/horizontalpodautoscaler.go +++ b/k8s-source/adapters/horizontalpodautoscaler.go @@ -3,9 +3,9 @@ package adapters import ( v2 "k8s.io/api/autoscaling/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/horizontalpodautoscaler_test.go b/k8s-source/adapters/horizontalpodautoscaler_test.go index 7d5cf303..ea1227b7 100644 --- a/k8s-source/adapters/horizontalpodautoscaler_test.go +++ b/k8s-source/adapters/horizontalpodautoscaler_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var horizontalPodAutoscalerYAML = ` diff --git a/k8s-source/adapters/ingress.go b/k8s-source/adapters/ingress.go index 45fab9f0..23eda968 100644 --- a/k8s-source/adapters/ingress.go +++ b/k8s-source/adapters/ingress.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/ingress_test.go b/k8s-source/adapters/ingress_test.go index 16a8ef1a..f6933961 100644 --- a/k8s-source/adapters/ingress_test.go +++ b/k8s-source/adapters/ingress_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var ingressYAML = ` diff --git a/k8s-source/adapters/job.go b/k8s-source/adapters/job.go index fce17eec..714fc136 100644 --- a/k8s-source/adapters/job.go +++ b/k8s-source/adapters/job.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/batch/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/job_test.go b/k8s-source/adapters/job_test.go index fa69c4ef..91f9888a 100644 --- a/k8s-source/adapters/job_test.go +++ b/k8s-source/adapters/job_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var jobYAML = ` diff --git a/k8s-source/adapters/limitrange.go b/k8s-source/adapters/limitrange.go index a0c19298..5888708e 100644 --- a/k8s-source/adapters/limitrange.go +++ b/k8s-source/adapters/limitrange.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/limitrange_test.go b/k8s-source/adapters/limitrange_test.go index c28b5fa0..54386246 100644 --- a/k8s-source/adapters/limitrange_test.go +++ b/k8s-source/adapters/limitrange_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var limitRangeYAML = ` diff --git a/k8s-source/adapters/main.go b/k8s-source/adapters/main.go index 3fcb4051..ce59869f 100644 --- a/k8s-source/adapters/main.go +++ b/k8s-source/adapters/main.go @@ -1,8 +1,8 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/networkpolicy.go b/k8s-source/adapters/networkpolicy.go index fa6b9810..eb7df6ae 100644 --- a/k8s-source/adapters/networkpolicy.go +++ b/k8s-source/adapters/networkpolicy.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/networkpolicy_test.go b/k8s-source/adapters/networkpolicy_test.go index b489a0e8..e2838848 100644 --- a/k8s-source/adapters/networkpolicy_test.go +++ b/k8s-source/adapters/networkpolicy_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var NetworkPolicyYAML = ` diff --git a/k8s-source/adapters/node.go b/k8s-source/adapters/node.go index 4c066308..a8f6a23d 100644 --- a/k8s-source/adapters/node.go +++ b/k8s-source/adapters/node.go @@ -3,9 +3,9 @@ package adapters import ( "strings" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/node_test.go b/k8s-source/adapters/node_test.go index a268cfd4..f0453ef1 100644 --- a/k8s-source/adapters/node_test.go +++ b/k8s-source/adapters/node_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestNodeAdapter(t *testing.T) { diff --git a/k8s-source/adapters/persistentvolume.go b/k8s-source/adapters/persistentvolume.go index 6492dc05..bcc377bd 100644 --- a/k8s-source/adapters/persistentvolume.go +++ b/k8s-source/adapters/persistentvolume.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/persistentvolume_test.go b/k8s-source/adapters/persistentvolume_test.go index cc15002a..f9747099 100644 --- a/k8s-source/adapters/persistentvolume_test.go +++ b/k8s-source/adapters/persistentvolume_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var persistentVolumeYAML = ` diff --git a/k8s-source/adapters/persistentvolumeclaim.go b/k8s-source/adapters/persistentvolumeclaim.go index 70e66384..684c41ee 100644 --- a/k8s-source/adapters/persistentvolumeclaim.go +++ b/k8s-source/adapters/persistentvolumeclaim.go @@ -3,9 +3,9 @@ package adapters import ( "errors" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/persistentvolumeclaim_test.go b/k8s-source/adapters/persistentvolumeclaim_test.go index a5d0e47b..6f5f62ad 100644 --- a/k8s-source/adapters/persistentvolumeclaim_test.go +++ b/k8s-source/adapters/persistentvolumeclaim_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var persistentVolumeClaimYAML = ` diff --git a/k8s-source/adapters/poddisruptionbudget.go b/k8s-source/adapters/poddisruptionbudget.go index 12f0d58c..dc2e48fb 100644 --- a/k8s-source/adapters/poddisruptionbudget.go +++ b/k8s-source/adapters/poddisruptionbudget.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/policy/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/poddisruptionbudget_test.go b/k8s-source/adapters/poddisruptionbudget_test.go index f5cba81c..e18e3d22 100644 --- a/k8s-source/adapters/poddisruptionbudget_test.go +++ b/k8s-source/adapters/poddisruptionbudget_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var PodDisruptionBudgetYAML = ` diff --git a/k8s-source/adapters/pods.go b/k8s-source/adapters/pods.go index d5e61013..70d451df 100644 --- a/k8s-source/adapters/pods.go +++ b/k8s-source/adapters/pods.go @@ -5,9 +5,9 @@ import ( "slices" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/pods_test.go b/k8s-source/adapters/pods_test.go index fbe6f282..e1644443 100644 --- a/k8s-source/adapters/pods_test.go +++ b/k8s-source/adapters/pods_test.go @@ -6,8 +6,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" ) diff --git a/k8s-source/adapters/priorityclass.go b/k8s-source/adapters/priorityclass.go index 613c909d..8258b1a1 100644 --- a/k8s-source/adapters/priorityclass.go +++ b/k8s-source/adapters/priorityclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/scheduling/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/priorityclass_test.go b/k8s-source/adapters/priorityclass_test.go index 6e70d913..f9d9c33d 100644 --- a/k8s-source/adapters/priorityclass_test.go +++ b/k8s-source/adapters/priorityclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var priorityClassYAML = ` diff --git a/k8s-source/adapters/replicaset.go b/k8s-source/adapters/replicaset.go index e466fe0f..e32c90e3 100644 --- a/k8s-source/adapters/replicaset.go +++ b/k8s-source/adapters/replicaset.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/apps/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/replicaset_test.go b/k8s-source/adapters/replicaset_test.go index 37e0a3df..0880eff7 100644 --- a/k8s-source/adapters/replicaset_test.go +++ b/k8s-source/adapters/replicaset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var replicaSetYAML = ` diff --git a/k8s-source/adapters/replicationcontroller.go b/k8s-source/adapters/replicationcontroller.go index f4f8bf19..908d0ddc 100644 --- a/k8s-source/adapters/replicationcontroller.go +++ b/k8s-source/adapters/replicationcontroller.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/replicationcontroller_test.go b/k8s-source/adapters/replicationcontroller_test.go index 1fa97b58..d55a7642 100644 --- a/k8s-source/adapters/replicationcontroller_test.go +++ b/k8s-source/adapters/replicationcontroller_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var replicationControllerYAML = ` diff --git a/k8s-source/adapters/resourcequota.go b/k8s-source/adapters/resourcequota.go index e0dbc30a..34f5e74d 100644 --- a/k8s-source/adapters/resourcequota.go +++ b/k8s-source/adapters/resourcequota.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/resourcequota_test.go b/k8s-source/adapters/resourcequota_test.go index 57105817..eec11486 100644 --- a/k8s-source/adapters/resourcequota_test.go +++ b/k8s-source/adapters/resourcequota_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var resourceQuotaYAML = ` diff --git a/k8s-source/adapters/role.go b/k8s-source/adapters/role.go index 52db8ecd..fcf6c49f 100644 --- a/k8s-source/adapters/role.go +++ b/k8s-source/adapters/role.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/role_test.go b/k8s-source/adapters/role_test.go index 823334c4..a84d5d97 100644 --- a/k8s-source/adapters/role_test.go +++ b/k8s-source/adapters/role_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var RoleYAML = ` diff --git a/k8s-source/adapters/rolebinding.go b/k8s-source/adapters/rolebinding.go index 1e82d04a..b3c85c39 100644 --- a/k8s-source/adapters/rolebinding.go +++ b/k8s-source/adapters/rolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/rolebinding_test.go b/k8s-source/adapters/rolebinding_test.go index 3714cecb..1bb33c21 100644 --- a/k8s-source/adapters/rolebinding_test.go +++ b/k8s-source/adapters/rolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var roleBindingYAML = ` diff --git a/k8s-source/adapters/secret.go b/k8s-source/adapters/secret.go index 46e73b94..57907195 100644 --- a/k8s-source/adapters/secret.go +++ b/k8s-source/adapters/secret.go @@ -3,9 +3,9 @@ package adapters import ( "crypto/sha512" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/secret_test.go b/k8s-source/adapters/secret_test.go index 62b51a76..5aa1a5ef 100644 --- a/k8s-source/adapters/secret_test.go +++ b/k8s-source/adapters/secret_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var secretYAML = ` diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index a88f65f4..c080dff9 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/service_test.go b/k8s-source/adapters/service_test.go index 26f502f4..961523e2 100644 --- a/k8s-source/adapters/service_test.go +++ b/k8s-source/adapters/service_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var serviceYAML = ` diff --git a/k8s-source/adapters/serviceaccount.go b/k8s-source/adapters/serviceaccount.go index b7372430..cad46207 100644 --- a/k8s-source/adapters/serviceaccount.go +++ b/k8s-source/adapters/serviceaccount.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/serviceaccount_test.go b/k8s-source/adapters/serviceaccount_test.go index 79bed8c7..0e0f2a81 100644 --- a/k8s-source/adapters/serviceaccount_test.go +++ b/k8s-source/adapters/serviceaccount_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var serviceAccountYAML = ` diff --git a/k8s-source/adapters/shared_util.go b/k8s-source/adapters/shared_util.go index f8728fbb..6f59ea7c 100644 --- a/k8s-source/adapters/shared_util.go +++ b/k8s-source/adapters/shared_util.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s-source/adapters/statefulset.go b/k8s-source/adapters/statefulset.go index 37f43264..674ee3b8 100644 --- a/k8s-source/adapters/statefulset.go +++ b/k8s-source/adapters/statefulset.go @@ -4,9 +4,9 @@ import ( v1 "k8s.io/api/apps/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/statefulset_test.go b/k8s-source/adapters/statefulset_test.go index 3e022447..fd68485f 100644 --- a/k8s-source/adapters/statefulset_test.go +++ b/k8s-source/adapters/statefulset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var statefulSetYAML = ` diff --git a/k8s-source/adapters/storageclass.go b/k8s-source/adapters/storageclass.go index 8482d5d5..b440ac85 100644 --- a/k8s-source/adapters/storageclass.go +++ b/k8s-source/adapters/storageclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/storageclass_test.go b/k8s-source/adapters/storageclass_test.go index cd25744d..011408f8 100644 --- a/k8s-source/adapters/storageclass_test.go +++ b/k8s-source/adapters/storageclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) var storageClassYAML = ` diff --git a/k8s-source/adapters/volumeattachment.go b/k8s-source/adapters/volumeattachment.go index ce15ab65..f59a11d3 100644 --- a/k8s-source/adapters/volumeattachment.go +++ b/k8s-source/adapters/volumeattachment.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/volumeattachment_test.go b/k8s-source/adapters/volumeattachment_test.go index 094bd1d9..bec193c0 100644 --- a/k8s-source/adapters/volumeattachment_test.go +++ b/k8s-source/adapters/volumeattachment_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) var volumeAttachmentYAML = ` diff --git a/k8s-source/build/package/Dockerfile b/k8s-source/build/package/Dockerfile index 0360a8dd..5f013774 100644 --- a/k8s-source/build/package/Dockerfile +++ b/k8s-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/k8s-source/cmd/root.go b/k8s-source/cmd/root.go index 9a93677f..f64a5306 100644 --- a/k8s-source/cmd/root.go +++ b/k8s-source/cmd/root.go @@ -16,12 +16,12 @@ import ( "time" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/discovery" "github.com/overmindtech/cli/k8s-source/adapters" "github.com/overmindtech/cli/k8s-source/proc" - "github.com/overmindtech/cli/logging" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/logging" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/logging/logging.go b/logging/logging.go deleted file mode 100644 index f6da0438..00000000 --- a/logging/logging.go +++ /dev/null @@ -1,53 +0,0 @@ -package logging - -import ( - log "github.com/sirupsen/logrus" -) - -// ConfigureLogrusJSON sets the logger to emit JSON logs with a GCP severity field. -func ConfigureLogrusJSON(logger *log.Logger) { - if logger == nil { - return - } - - logger.SetFormatter(&log.JSONFormatter{}) - logger.AddHook(OtelSeverityHook{}) -} - -// OtelSeverityHook adds a GCP-compatible severity field to log entries. -type OtelSeverityHook struct{} - -func (OtelSeverityHook) Levels() []log.Level { - return log.AllLevels -} - -func (OtelSeverityHook) Fire(entry *log.Entry) error { - if entry == nil { - return nil - } - if _, ok := entry.Data["severity"]; ok { - return nil - } - - entry.Data["severity"] = severityForLevel(entry.Level) - return nil -} - -func severityForLevel(level log.Level) string { - switch level { - case log.PanicLevel: - return "emergency" - case log.FatalLevel: - return "critical" - case log.ErrorLevel: - return "error" - case log.WarnLevel: - return "warning" - case log.InfoLevel: - return "info" - case log.DebugLevel, log.TraceLevel: - return "debug" - default: - return "default" - } -} diff --git a/logging/logging_test.go b/logging/logging_test.go deleted file mode 100644 index a53a52bc..00000000 --- a/logging/logging_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package logging - -import ( - "bytes" - "encoding/json" - "testing" - - log "github.com/sirupsen/logrus" -) - -func TestSeverityForLevel(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - level log.Level - want string - }{ - {name: "panic", level: log.PanicLevel, want: "emergency"}, - {name: "fatal", level: log.FatalLevel, want: "critical"}, - {name: "error", level: log.ErrorLevel, want: "error"}, - {name: "warn", level: log.WarnLevel, want: "warning"}, - {name: "info", level: log.InfoLevel, want: "info"}, - {name: "debug", level: log.DebugLevel, want: "debug"}, - {name: "trace", level: log.TraceLevel, want: "debug"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := severityForLevel(tt.level) - if got != tt.want { - t.Errorf("severityForLevel(%v) = %q, want %q", tt.level, got, tt.want) - } - }) - } -} - -func TestConfigureLogrusJSONAddsSeverity(t *testing.T) { - t.Parallel() - - logger := log.New() - var buf bytes.Buffer - logger.SetOutput(&buf) - - ConfigureLogrusJSON(logger) - logger.WithField("component", "test").Info("hello") - - var payload map[string]any - if err := json.Unmarshal(buf.Bytes(), &payload); err != nil { - t.Fatalf("unmarshal log payload: %v", err) - } - - got, ok := payload["severity"] - if !ok { - t.Fatalf("expected severity field in log payload, got: %#v", payload) - } - if got != "info" { - t.Fatalf("expected severity %q, got %v", "info", got) - } -} - -func TestConfigureLogrusJSONRespectsExistingSeverity(t *testing.T) { - t.Parallel() - - logger := log.New() - var buf bytes.Buffer - logger.SetOutput(&buf) - - ConfigureLogrusJSON(logger) - logger.WithField("severity", "SPECIAL").Info("hello") - - var payload map[string]any - if err := json.Unmarshal(buf.Bytes(), &payload); err != nil { - t.Fatalf("unmarshal log payload: %v", err) - } - - got, ok := payload["severity"] - if !ok { - t.Fatalf("expected severity field in log payload, got: %#v", payload) - } - if got != "SPECIAL" { - t.Fatalf("expected severity %q, got %v", "SPECIAL", got) - } -} diff --git a/sdp-go/.gitignore b/sdp-go/.gitignore deleted file mode 100644 index 85d346aa..00000000 --- a/sdp-go/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -vendor -.DS_Store diff --git a/sdp-go/account.go b/sdp-go/account.go deleted file mode 100644 index a5cda679..00000000 --- a/sdp-go/account.go +++ /dev/null @@ -1,19 +0,0 @@ -package sdp - -import "github.com/google/uuid" - -func (a *SourceMetadata) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *SourceHealth) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} diff --git a/sdp-go/account.pb.go b/sdp-go/account.pb.go deleted file mode 100644 index 679cfe1f..00000000 --- a/sdp-go/account.pb.go +++ /dev/null @@ -1,4649 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: account.proto - -package sdp - -import ( - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - structpb "google.golang.org/protobuf/types/known/structpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SourceStatus int32 - -const ( - SourceStatus_STATUS_UNSPECIFIED SourceStatus = 0 - // The source is starting or updating. This is only applicable to managed - // sources where Overmind manages the source's lifecycle - SourceStatus_STATUS_PROGRESSING SourceStatus = 1 - // The source is healthy - SourceStatus_STATUS_HEALTHY SourceStatus = 2 - // The source is unhealthy - SourceStatus_STATUS_UNHEALTHY SourceStatus = 3 - // The source is sleeping due to inactivity. It will be woken up before it - // is needed. This is only applicable to managed sources where Overmind - // manages the source's lifecycle - SourceStatus_STATUS_SLEEPING SourceStatus = 4 - // The source is disconnected and therefore not able to handle requests. - // This will only be returned for non-managed sources that have recently - // stopped sending heartbeats such as a user running the CLI that has - // recently disconnected - SourceStatus_STATUS_DISCONNECTED SourceStatus = 5 -) - -// Enum value maps for SourceStatus. -var ( - SourceStatus_name = map[int32]string{ - 0: "STATUS_UNSPECIFIED", - 1: "STATUS_PROGRESSING", - 2: "STATUS_HEALTHY", - 3: "STATUS_UNHEALTHY", - 4: "STATUS_SLEEPING", - 5: "STATUS_DISCONNECTED", - } - SourceStatus_value = map[string]int32{ - "STATUS_UNSPECIFIED": 0, - "STATUS_PROGRESSING": 1, - "STATUS_HEALTHY": 2, - "STATUS_UNHEALTHY": 3, - "STATUS_SLEEPING": 4, - "STATUS_DISCONNECTED": 5, - } -) - -func (x SourceStatus) Enum() *SourceStatus { - p := new(SourceStatus) - *p = x - return p -} - -func (x SourceStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SourceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_account_proto_enumTypes[0].Descriptor() -} - -func (SourceStatus) Type() protoreflect.EnumType { - return &file_account_proto_enumTypes[0] -} - -func (x SourceStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SourceStatus.Descriptor instead. -func (SourceStatus) EnumDescriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{0} -} - -type RepositoryStatus int32 - -const ( - RepositoryStatus_REPOSITORY_STATUS_UNSPECIFIED RepositoryStatus = 0 - // Repository has had changes within the defined activity window - RepositoryStatus_REPOSITORY_STATUS_ACTIVE RepositoryStatus = 1 - // Repository has not had changes within the defined activity window - RepositoryStatus_REPOSITORY_STATUS_INACTIVE RepositoryStatus = 2 -) - -// Enum value maps for RepositoryStatus. -var ( - RepositoryStatus_name = map[int32]string{ - 0: "REPOSITORY_STATUS_UNSPECIFIED", - 1: "REPOSITORY_STATUS_ACTIVE", - 2: "REPOSITORY_STATUS_INACTIVE", - } - RepositoryStatus_value = map[string]int32{ - "REPOSITORY_STATUS_UNSPECIFIED": 0, - "REPOSITORY_STATUS_ACTIVE": 1, - "REPOSITORY_STATUS_INACTIVE": 2, - } -) - -func (x RepositoryStatus) Enum() *RepositoryStatus { - p := new(RepositoryStatus) - *p = x - return p -} - -func (x RepositoryStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (RepositoryStatus) Descriptor() protoreflect.EnumDescriptor { - return file_account_proto_enumTypes[1].Descriptor() -} - -func (RepositoryStatus) Type() protoreflect.EnumType { - return &file_account_proto_enumTypes[1] -} - -func (x RepositoryStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use RepositoryStatus.Descriptor instead. -func (RepositoryStatus) EnumDescriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{1} -} - -type AccountPlan int32 - -const ( - AccountPlan_ACCOUNT_PLAN_UNSPECIFIED AccountPlan = 0 - // Free plan with one repo - AccountPlan_ACCOUNT_PLAN_FREE AccountPlan = 1 - // Enterprise plan with unlimited repos - AccountPlan_ACCOUNT_PLAN_ENTERPRISE AccountPlan = 2 -) - -// Enum value maps for AccountPlan. -var ( - AccountPlan_name = map[int32]string{ - 0: "ACCOUNT_PLAN_UNSPECIFIED", - 1: "ACCOUNT_PLAN_FREE", - 2: "ACCOUNT_PLAN_ENTERPRISE", - } - AccountPlan_value = map[string]int32{ - "ACCOUNT_PLAN_UNSPECIFIED": 0, - "ACCOUNT_PLAN_FREE": 1, - "ACCOUNT_PLAN_ENTERPRISE": 2, - } -) - -func (x AccountPlan) Enum() *AccountPlan { - p := new(AccountPlan) - *p = x - return p -} - -func (x AccountPlan) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (AccountPlan) Descriptor() protoreflect.EnumDescriptor { - return file_account_proto_enumTypes[2].Descriptor() -} - -func (AccountPlan) Type() protoreflect.EnumType { - return &file_account_proto_enumTypes[2] -} - -func (x AccountPlan) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use AccountPlan.Descriptor instead. -func (AccountPlan) EnumDescriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{2} -} - -// Whether the source is managed by srcman or was created by the user locally -type SourceManaged int32 - -const ( - SourceManaged_LOCAL SourceManaged = 0 // Local is the default - SourceManaged_MANAGED SourceManaged = 1 -) - -// Enum value maps for SourceManaged. -var ( - SourceManaged_name = map[int32]string{ - 0: "LOCAL", - 1: "MANAGED", - } - SourceManaged_value = map[string]int32{ - "LOCAL": 0, - "MANAGED": 1, - } -) - -func (x SourceManaged) Enum() *SourceManaged { - p := new(SourceManaged) - *p = x - return p -} - -func (x SourceManaged) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SourceManaged) Descriptor() protoreflect.EnumDescriptor { - return file_account_proto_enumTypes[3].Descriptor() -} - -func (SourceManaged) Type() protoreflect.EnumType { - return &file_account_proto_enumTypes[3] -} - -func (x SourceManaged) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SourceManaged.Descriptor instead. -func (SourceManaged) EnumDescriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{3} -} - -type AdapterCategory int32 - -const ( - // Fall-back category for resources that do not fit into any other category - AdapterCategory_ADAPTER_CATEGORY_OTHER AdapterCategory = 0 - // This category includes resources that provide processing power and host - // applications or services. Examples are virtual machines, containers, - // serverless functions, and application hosting platforms. If the primary - // purpose of a resource is to execute workloads, run code, or host - // applications, it should belong here. - AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION AdapterCategory = 1 - // Encompassing resources designed to store, archive, and manage data, this - // category includes object storage, block storage, file storage, and data - // backup solutions. Select this category when the core function of a - // resource is persistent data storage or management - AdapterCategory_ADAPTER_CATEGORY_STORAGE AdapterCategory = 2 - // This category covers resources that facilitate connectivity and - // communication within cloud environments. Typical resources include - // virtual networks, load balancers, VPNs, and DNS services. Assign - // resources here if their primary role is related to communication, - // connectivity, or traffic management - AdapterCategory_ADAPTER_CATEGORY_NETWORK AdapterCategory = 3 - // Resources in this category focus on safeguarding data, applications, and - // cloud infrastructure. Examples include firewalls, identity and access - // management, encryption services, and security monitoring tools. Choose - // this category if a resource's main function is security, access control, - // or compliance - AdapterCategory_ADAPTER_CATEGORY_SECURITY AdapterCategory = 4 - // This category includes resources aimed at monitoring, tracing, and - // logging applications and cloud infrastructure. Examples are monitoring - // tools, logging services, and performance management solutions. Use this - // category for resources that provide insights into system performance and - // health - AdapterCategory_ADAPTER_CATEGORY_OBSERVABILITY AdapterCategory = 5 - // Focused on structured data storage and management, this category includes - // relational, NoSQL, and in-memory databases, along with data warehousing - // solutions. Choose this category for resources specifically designed for - // data querying, transaction processing, or complex data operations. This - // differs from "storage" in that "databases" have compute associated with - // them rather than just storing data. - AdapterCategory_ADAPTER_CATEGORY_DATABASE AdapterCategory = 6 - // This category includes resources designed for managing configurations and - // deployments. Examples are infrastructure as code tools, configuration - // management services, and deployment orchestration solutions. Classify - // resources here if they primarily handle configuration, environment - // management, or automated deployment - AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION AdapterCategory = 7 - // This category is dedicated to resources for developing, training, and - // deploying artificial intelligence models and machine learning - // applications. Include machine learning platforms, AI services, and data - // labeling tools here. Select this category if a resource's principal - // function involves AI or machine learning processes - AdapterCategory_ADAPTER_CATEGORY_AI AdapterCategory = 8 -) - -// Enum value maps for AdapterCategory. -var ( - AdapterCategory_name = map[int32]string{ - 0: "ADAPTER_CATEGORY_OTHER", - 1: "ADAPTER_CATEGORY_COMPUTE_APPLICATION", - 2: "ADAPTER_CATEGORY_STORAGE", - 3: "ADAPTER_CATEGORY_NETWORK", - 4: "ADAPTER_CATEGORY_SECURITY", - 5: "ADAPTER_CATEGORY_OBSERVABILITY", - 6: "ADAPTER_CATEGORY_DATABASE", - 7: "ADAPTER_CATEGORY_CONFIGURATION", - 8: "ADAPTER_CATEGORY_AI", - } - AdapterCategory_value = map[string]int32{ - "ADAPTER_CATEGORY_OTHER": 0, - "ADAPTER_CATEGORY_COMPUTE_APPLICATION": 1, - "ADAPTER_CATEGORY_STORAGE": 2, - "ADAPTER_CATEGORY_NETWORK": 3, - "ADAPTER_CATEGORY_SECURITY": 4, - "ADAPTER_CATEGORY_OBSERVABILITY": 5, - "ADAPTER_CATEGORY_DATABASE": 6, - "ADAPTER_CATEGORY_CONFIGURATION": 7, - "ADAPTER_CATEGORY_AI": 8, - } -) - -func (x AdapterCategory) Enum() *AdapterCategory { - p := new(AdapterCategory) - *p = x - return p -} - -func (x AdapterCategory) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (AdapterCategory) Descriptor() protoreflect.EnumDescriptor { - return file_account_proto_enumTypes[4].Descriptor() -} - -func (AdapterCategory) Type() protoreflect.EnumType { - return &file_account_proto_enumTypes[4] -} - -func (x AdapterCategory) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use AdapterCategory.Descriptor instead. -func (AdapterCategory) EnumDescriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{4} -} - -type ListAccountsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAccountsRequest) Reset() { - *x = ListAccountsRequest{} - mi := &file_account_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAccountsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAccountsRequest) ProtoMessage() {} - -func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[0] - 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 ListAccountsRequest.ProtoReflect.Descriptor instead. -func (*ListAccountsRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{0} -} - -type ListAccountsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Accounts []*Account `protobuf:"bytes,1,rep,name=accounts,proto3" json:"accounts,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAccountsResponse) Reset() { - *x = ListAccountsResponse{} - mi := &file_account_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAccountsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAccountsResponse) ProtoMessage() {} - -func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[1] - 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 ListAccountsResponse.ProtoReflect.Descriptor instead. -func (*ListAccountsResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{1} -} - -func (x *ListAccountsResponse) GetAccounts() []*Account { - if x != nil { - return x.Accounts - } - return nil -} - -type CreateAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *AccountProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateAccountRequest) Reset() { - *x = CreateAccountRequest{} - mi := &file_account_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateAccountRequest) ProtoMessage() {} - -func (x *CreateAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[2] - 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 CreateAccountRequest.ProtoReflect.Descriptor instead. -func (*CreateAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{2} -} - -func (x *CreateAccountRequest) GetProperties() *AccountProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateAccountResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateAccountResponse) Reset() { - *x = CreateAccountResponse{} - mi := &file_account_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateAccountResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateAccountResponse) ProtoMessage() {} - -func (x *CreateAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[3] - 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 CreateAccountResponse.ProtoReflect.Descriptor instead. -func (*CreateAccountResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{3} -} - -func (x *CreateAccountResponse) GetAccount() *Account { - if x != nil { - return x.Account - } - return nil -} - -type UpdateAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *AccountProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAccountRequest) Reset() { - *x = UpdateAccountRequest{} - mi := &file_account_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAccountRequest) ProtoMessage() {} - -func (x *UpdateAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[4] - 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 UpdateAccountRequest.ProtoReflect.Descriptor instead. -func (*UpdateAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{4} -} - -func (x *UpdateAccountRequest) GetProperties() *AccountProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateAccountResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAccountResponse) Reset() { - *x = UpdateAccountResponse{} - mi := &file_account_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAccountResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAccountResponse) ProtoMessage() {} - -func (x *UpdateAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[5] - 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 UpdateAccountResponse.ProtoReflect.Descriptor instead. -func (*UpdateAccountResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{5} -} - -func (x *UpdateAccountResponse) GetAccount() *Account { - if x != nil { - return x.Account - } - return nil -} - -type AdminUpdateAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the account to update - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Request *UpdateAccountRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminUpdateAccountRequest) Reset() { - *x = AdminUpdateAccountRequest{} - mi := &file_account_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminUpdateAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminUpdateAccountRequest) ProtoMessage() {} - -func (x *AdminUpdateAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[6] - 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 AdminUpdateAccountRequest.ProtoReflect.Descriptor instead. -func (*AdminUpdateAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{6} -} - -func (x *AdminUpdateAccountRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *AdminUpdateAccountRequest) GetRequest() *UpdateAccountRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminGetAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the account to get - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminGetAccountRequest) Reset() { - *x = AdminGetAccountRequest{} - mi := &file_account_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminGetAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminGetAccountRequest) ProtoMessage() {} - -func (x *AdminGetAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[7] - 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 AdminGetAccountRequest.ProtoReflect.Descriptor instead. -func (*AdminGetAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{7} -} - -func (x *AdminGetAccountRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type AdminDeleteAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the account to delete - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminDeleteAccountRequest) Reset() { - *x = AdminDeleteAccountRequest{} - mi := &file_account_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminDeleteAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminDeleteAccountRequest) ProtoMessage() {} - -func (x *AdminDeleteAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[8] - 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 AdminDeleteAccountRequest.ProtoReflect.Descriptor instead. -func (*AdminDeleteAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{8} -} - -func (x *AdminDeleteAccountRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type AdminDeleteAccountResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminDeleteAccountResponse) Reset() { - *x = AdminDeleteAccountResponse{} - mi := &file_account_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminDeleteAccountResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminDeleteAccountResponse) ProtoMessage() {} - -func (x *AdminDeleteAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[9] - 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 AdminDeleteAccountResponse.ProtoReflect.Descriptor instead. -func (*AdminDeleteAccountResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{9} -} - -type AdminListSourcesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *ListSourcesRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminListSourcesRequest) Reset() { - *x = AdminListSourcesRequest{} - mi := &file_account_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminListSourcesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminListSourcesRequest) ProtoMessage() {} - -func (x *AdminListSourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[10] - 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 AdminListSourcesRequest.ProtoReflect.Descriptor instead. -func (*AdminListSourcesRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{10} -} - -func (x *AdminListSourcesRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminListSourcesRequest) GetRequest() *ListSourcesRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminCreateSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *CreateSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminCreateSourceRequest) Reset() { - *x = AdminCreateSourceRequest{} - mi := &file_account_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminCreateSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminCreateSourceRequest) ProtoMessage() {} - -func (x *AdminCreateSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[11] - 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 AdminCreateSourceRequest.ProtoReflect.Descriptor instead. -func (*AdminCreateSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{11} -} - -func (x *AdminCreateSourceRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminCreateSourceRequest) GetRequest() *CreateSourceRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminGetSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *GetSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminGetSourceRequest) Reset() { - *x = AdminGetSourceRequest{} - mi := &file_account_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminGetSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminGetSourceRequest) ProtoMessage() {} - -func (x *AdminGetSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[12] - 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 AdminGetSourceRequest.ProtoReflect.Descriptor instead. -func (*AdminGetSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{12} -} - -func (x *AdminGetSourceRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminGetSourceRequest) GetRequest() *GetSourceRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminUpdateSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *UpdateSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminUpdateSourceRequest) Reset() { - *x = AdminUpdateSourceRequest{} - mi := &file_account_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminUpdateSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminUpdateSourceRequest) ProtoMessage() {} - -func (x *AdminUpdateSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AdminUpdateSourceRequest.ProtoReflect.Descriptor instead. -func (*AdminUpdateSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{13} -} - -func (x *AdminUpdateSourceRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminUpdateSourceRequest) GetRequest() *UpdateSourceRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminDeleteSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *DeleteSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminDeleteSourceRequest) Reset() { - *x = AdminDeleteSourceRequest{} - mi := &file_account_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminDeleteSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminDeleteSourceRequest) ProtoMessage() {} - -func (x *AdminDeleteSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AdminDeleteSourceRequest.ProtoReflect.Descriptor instead. -func (*AdminDeleteSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{14} -} - -func (x *AdminDeleteSourceRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminDeleteSourceRequest) GetRequest() *DeleteSourceRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminKeepaliveSourcesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *KeepaliveSourcesRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminKeepaliveSourcesRequest) Reset() { - *x = AdminKeepaliveSourcesRequest{} - mi := &file_account_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminKeepaliveSourcesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminKeepaliveSourcesRequest) ProtoMessage() {} - -func (x *AdminKeepaliveSourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[15] - 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 AdminKeepaliveSourcesRequest.ProtoReflect.Descriptor instead. -func (*AdminKeepaliveSourcesRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{15} -} - -func (x *AdminKeepaliveSourcesRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminKeepaliveSourcesRequest) GetRequest() *KeepaliveSourcesRequest { - if x != nil { - return x.Request - } - return nil -} - -type AdminCreateTokenRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - Request *CreateTokenRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdminCreateTokenRequest) Reset() { - *x = AdminCreateTokenRequest{} - mi := &file_account_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdminCreateTokenRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdminCreateTokenRequest) ProtoMessage() {} - -func (x *AdminCreateTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[16] - 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 AdminCreateTokenRequest.ProtoReflect.Descriptor instead. -func (*AdminCreateTokenRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{16} -} - -func (x *AdminCreateTokenRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *AdminCreateTokenRequest) GetRequest() *CreateTokenRequest { - if x != nil { - return x.Request - } - return nil -} - -type Source struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *SourceMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *SourceProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Source) Reset() { - *x = Source{} - mi := &file_account_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Source) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Source) ProtoMessage() {} - -func (x *Source) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Source.ProtoReflect.Descriptor instead. -func (*Source) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{17} -} - -func (x *Source) GetMetadata() *SourceMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Source) GetProperties() *SourceProperties { - if x != nil { - return x.Properties - } - return nil -} - -type SourceMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` // TODO: Change to ID along with everything else - // The name of the NATS JWT that has been generated for this source - TokenName string `protobuf:"bytes,2,opt,name=TokenName,proto3" json:"TokenName,omitempty"` - // When the NATS JWT expires (unix time) - TokenExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=TokenExpiry,proto3" json:"TokenExpiry,omitempty"` - // The public NKey associated with the NATS JWT - PublicNkey string `protobuf:"bytes,5,opt,name=PublicNkey,proto3" json:"PublicNkey,omitempty"` - // Status of the source - Status SourceStatus `protobuf:"varint,9,opt,name=Status,proto3,enum=account.SourceStatus" json:"Status,omitempty"` - // The error message if the source is unhealthy - Error string `protobuf:"bytes,10,opt,name=Error,proto3" json:"Error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SourceMetadata) Reset() { - *x = SourceMetadata{} - mi := &file_account_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SourceMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SourceMetadata) ProtoMessage() {} - -func (x *SourceMetadata) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SourceMetadata.ProtoReflect.Descriptor instead. -func (*SourceMetadata) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{18} -} - -func (x *SourceMetadata) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *SourceMetadata) GetTokenName() string { - if x != nil { - return x.TokenName - } - return "" -} - -func (x *SourceMetadata) GetTokenExpiry() *timestamppb.Timestamp { - if x != nil { - return x.TokenExpiry - } - return nil -} - -func (x *SourceMetadata) GetPublicNkey() string { - if x != nil { - return x.PublicNkey - } - return "" -} - -func (x *SourceMetadata) GetStatus() SourceStatus { - if x != nil { - return x.Status - } - return SourceStatus_STATUS_UNSPECIFIED -} - -func (x *SourceMetadata) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -// A source that is capable of discovering items -type SourceProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The descriptive name of the source - DescriptiveName string `protobuf:"bytes,1,opt,name=DescriptiveName,proto3" json:"DescriptiveName,omitempty"` - // What source to configure. Can be "stdlib", "aws", or "gcp". - Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` - // Config for this source. See the source documentation for what - // source-specific config is available/required. This will be supplied - // directly to viper via a config file at `/etc/srcman/config/source.yaml` - Config *structpb.Struct `protobuf:"bytes,3,opt,name=Config,proto3" json:"Config,omitempty"` - // Additional config options that should be passed to the source. The keys - // of this object should be file names, and the values should be their - // content. These files will be made available to the source at runtime. - // Check the source's documentation for what to configure here if required - AdditionalConfig *structpb.Struct `protobuf:"bytes,4,opt,name=AdditionalConfig,proto3" json:"AdditionalConfig,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SourceProperties) Reset() { - *x = SourceProperties{} - mi := &file_account_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SourceProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SourceProperties) ProtoMessage() {} - -func (x *SourceProperties) ProtoReflect() protoreflect.Message { - mi := &file_account_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 SourceProperties.ProtoReflect.Descriptor instead. -func (*SourceProperties) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{19} -} - -func (x *SourceProperties) GetDescriptiveName() string { - if x != nil { - return x.DescriptiveName - } - return "" -} - -func (x *SourceProperties) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *SourceProperties) GetConfig() *structpb.Struct { - if x != nil { - return x.Config - } - return nil -} - -func (x *SourceProperties) GetAdditionalConfig() *structpb.Struct { - if x != nil { - return x.AdditionalConfig - } - return nil -} - -type Account struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *AccountMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *AccountProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Account) Reset() { - *x = Account{} - mi := &file_account_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Account) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Account) ProtoMessage() {} - -func (x *Account) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[20] - 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 Account.ProtoReflect.Descriptor instead. -func (*Account) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{20} -} - -func (x *Account) GetMetadata() *AccountMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Account) GetProperties() *AccountProperties { - if x != nil { - return x.Properties - } - return nil -} - -type AccountMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The public Nkey which signs all NATS "user" tokens - PublicNkey string `protobuf:"bytes,2,opt,name=PublicNkey,proto3" json:"PublicNkey,omitempty"` - // Repositories that have been used in this account - Repositories []*Repository `protobuf:"bytes,3,rep,name=repositories,proto3" json:"repositories,omitempty"` - // The total number of repositories associated with this account - TotalRepositories uint32 `protobuf:"varint,4,opt,name=totalRepositories,proto3" json:"totalRepositories,omitempty"` - // The number of active repositories (for billing purposes) - ActiveRepositories uint32 `protobuf:"varint,5,opt,name=activeRepositories,proto3" json:"activeRepositories,omitempty"` - // The billing plan for this account - Plan AccountPlan `protobuf:"varint,6,opt,name=Plan,proto3,enum=account.AccountPlan" json:"Plan,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AccountMetadata) Reset() { - *x = AccountMetadata{} - mi := &file_account_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AccountMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AccountMetadata) ProtoMessage() {} - -func (x *AccountMetadata) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[21] - 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 AccountMetadata.ProtoReflect.Descriptor instead. -func (*AccountMetadata) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{21} -} - -func (x *AccountMetadata) GetPublicNkey() string { - if x != nil { - return x.PublicNkey - } - return "" -} - -func (x *AccountMetadata) GetRepositories() []*Repository { - if x != nil { - return x.Repositories - } - return nil -} - -func (x *AccountMetadata) GetTotalRepositories() uint32 { - if x != nil { - return x.TotalRepositories - } - return 0 -} - -func (x *AccountMetadata) GetActiveRepositories() uint32 { - if x != nil { - return x.ActiveRepositories - } - return 0 -} - -func (x *AccountMetadata) GetPlan() AccountPlan { - if x != nil { - return x.Plan - } - return AccountPlan_ACCOUNT_PLAN_UNSPECIFIED -} - -type Repository struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Repository identifier; can be a URL, name, or any string identifier. Not necessarily a URL. CLI attempts auto-population, but users can override. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // The number of changes that have been recorded in this repository - NumChanges int64 `protobuf:"varint,2,opt,name=numChanges,proto3" json:"numChanges,omitempty"` - // The last time a change was recorded in this repository - LastChangeAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastChangeAt,proto3" json:"lastChangeAt,omitempty"` - // The status of the repository (active or inactive). This is determined - // based on the last change that was recorded. - Status RepositoryStatus `protobuf:"varint,4,opt,name=status,proto3,enum=account.RepositoryStatus" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Repository) Reset() { - *x = Repository{} - mi := &file_account_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Repository) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Repository) ProtoMessage() {} - -func (x *Repository) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[22] - 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 Repository.ProtoReflect.Descriptor instead. -func (*Repository) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{22} -} - -func (x *Repository) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Repository) GetNumChanges() int64 { - if x != nil { - return x.NumChanges - } - return 0 -} - -func (x *Repository) GetLastChangeAt() *timestamppb.Timestamp { - if x != nil { - return x.LastChangeAt - } - return nil -} - -func (x *Repository) GetStatus() RepositoryStatus { - if x != nil { - return x.Status - } - return RepositoryStatus_REPOSITORY_STATUS_UNSPECIFIED -} - -type AccountProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the account - Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - // The Customer ID within Stripe - StripeCustomerID string `protobuf:"bytes,2,opt,name=StripeCustomerID,proto3" json:"StripeCustomerID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AccountProperties) Reset() { - *x = AccountProperties{} - mi := &file_account_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AccountProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AccountProperties) ProtoMessage() {} - -func (x *AccountProperties) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[23] - 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 AccountProperties.ProtoReflect.Descriptor instead. -func (*AccountProperties) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{23} -} - -func (x *AccountProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *AccountProperties) GetStripeCustomerID() string { - if x != nil { - return x.StripeCustomerID - } - return "" -} - -type GetAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAccountRequest) Reset() { - *x = GetAccountRequest{} - mi := &file_account_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAccountRequest) ProtoMessage() {} - -func (x *GetAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_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 GetAccountRequest.ProtoReflect.Descriptor instead. -func (*GetAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{24} -} - -type GetAccountResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAccountResponse) Reset() { - *x = GetAccountResponse{} - mi := &file_account_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAccountResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAccountResponse) ProtoMessage() {} - -func (x *GetAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[25] - 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 GetAccountResponse.ProtoReflect.Descriptor instead. -func (*GetAccountResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{25} -} - -func (x *GetAccountResponse) GetAccount() *Account { - if x != nil { - return x.Account - } - return nil -} - -type DeleteAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Set to true to confirm that the user is sure they want to delete their - // account. This is to prevent accidental deletions - IAmSure bool `protobuf:"varint,1,opt,name=iAmSure,proto3" json:"iAmSure,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteAccountRequest) Reset() { - *x = DeleteAccountRequest{} - mi := &file_account_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteAccountRequest) ProtoMessage() {} - -func (x *DeleteAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[26] - 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 DeleteAccountRequest.ProtoReflect.Descriptor instead. -func (*DeleteAccountRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{26} -} - -func (x *DeleteAccountRequest) GetIAmSure() bool { - if x != nil { - return x.IAmSure - } - return false -} - -type DeleteAccountResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteAccountResponse) Reset() { - *x = DeleteAccountResponse{} - mi := &file_account_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteAccountResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteAccountResponse) ProtoMessage() {} - -func (x *DeleteAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[27] - 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 DeleteAccountResponse.ProtoReflect.Descriptor instead. -func (*DeleteAccountResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{27} -} - -type ListSourcesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSourcesRequest) Reset() { - *x = ListSourcesRequest{} - mi := &file_account_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSourcesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSourcesRequest) ProtoMessage() {} - -func (x *ListSourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[28] - 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 ListSourcesRequest.ProtoReflect.Descriptor instead. -func (*ListSourcesRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{28} -} - -type ListSourcesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sources []*Source `protobuf:"bytes,1,rep,name=Sources,proto3" json:"Sources,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSourcesResponse) Reset() { - *x = ListSourcesResponse{} - mi := &file_account_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSourcesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSourcesResponse) ProtoMessage() {} - -func (x *ListSourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[29] - 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 ListSourcesResponse.ProtoReflect.Descriptor instead. -func (*ListSourcesResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{29} -} - -func (x *ListSourcesResponse) GetSources() []*Source { - if x != nil { - return x.Sources - } - return nil -} - -type CreateSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *SourceProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateSourceRequest) Reset() { - *x = CreateSourceRequest{} - mi := &file_account_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateSourceRequest) ProtoMessage() {} - -func (x *CreateSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[30] - 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 CreateSourceRequest.ProtoReflect.Descriptor instead. -func (*CreateSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{30} -} - -func (x *CreateSourceRequest) GetProperties() *SourceProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateSourceResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateSourceResponse) Reset() { - *x = CreateSourceResponse{} - mi := &file_account_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateSourceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateSourceResponse) ProtoMessage() {} - -func (x *CreateSourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[31] - 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 CreateSourceResponse.ProtoReflect.Descriptor instead. -func (*CreateSourceResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{31} -} - -func (x *CreateSourceResponse) GetSource() *Source { - if x != nil { - return x.Source - } - return nil -} - -type GetSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSourceRequest) Reset() { - *x = GetSourceRequest{} - mi := &file_account_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSourceRequest) ProtoMessage() {} - -func (x *GetSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[32] - 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 GetSourceRequest.ProtoReflect.Descriptor instead. -func (*GetSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{32} -} - -func (x *GetSourceRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type GetSourceResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSourceResponse) Reset() { - *x = GetSourceResponse{} - mi := &file_account_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSourceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSourceResponse) ProtoMessage() {} - -func (x *GetSourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[33] - 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 GetSourceResponse.ProtoReflect.Descriptor instead. -func (*GetSourceResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{33} -} - -func (x *GetSourceResponse) GetSource() *Source { - if x != nil { - return x.Source - } - return nil -} - -type UpdateSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // ID of the source to update - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Properties to update - Properties *SourceProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSourceRequest) Reset() { - *x = UpdateSourceRequest{} - mi := &file_account_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSourceRequest) ProtoMessage() {} - -func (x *UpdateSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[34] - 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 UpdateSourceRequest.ProtoReflect.Descriptor instead. -func (*UpdateSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{34} -} - -func (x *UpdateSourceRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *UpdateSourceRequest) GetProperties() *SourceProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateSourceResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSourceResponse) Reset() { - *x = UpdateSourceResponse{} - mi := &file_account_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSourceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSourceResponse) ProtoMessage() {} - -func (x *UpdateSourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[35] - 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 UpdateSourceResponse.ProtoReflect.Descriptor instead. -func (*UpdateSourceResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{35} -} - -func (x *UpdateSourceResponse) GetSource() *Source { - if x != nil { - return x.Source - } - return nil -} - -type DeleteSourceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // ID if the source to delete - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSourceRequest) Reset() { - *x = DeleteSourceRequest{} - mi := &file_account_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSourceRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSourceRequest) ProtoMessage() {} - -func (x *DeleteSourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[36] - 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 DeleteSourceRequest.ProtoReflect.Descriptor instead. -func (*DeleteSourceRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{36} -} - -func (x *DeleteSourceRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type DeleteSourceResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSourceResponse) Reset() { - *x = DeleteSourceResponse{} - mi := &file_account_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSourceResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSourceResponse) ProtoMessage() {} - -func (x *DeleteSourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[37] - 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 DeleteSourceResponse.ProtoReflect.Descriptor instead. -func (*DeleteSourceResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{37} -} - -type SourceKeepaliveResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the source that was kept alive - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The status of the source - Status SourceStatus `protobuf:"varint,2,opt,name=Status,proto3,enum=account.SourceStatus" json:"Status,omitempty"` - // The error message if the source is unhealthy - Error string `protobuf:"bytes,3,opt,name=Error,proto3" json:"Error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SourceKeepaliveResult) Reset() { - *x = SourceKeepaliveResult{} - mi := &file_account_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SourceKeepaliveResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SourceKeepaliveResult) ProtoMessage() {} - -func (x *SourceKeepaliveResult) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[38] - 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 SourceKeepaliveResult.ProtoReflect.Descriptor instead. -func (*SourceKeepaliveResult) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{38} -} - -func (x *SourceKeepaliveResult) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *SourceKeepaliveResult) GetStatus() SourceStatus { - if x != nil { - return x.Status - } - return SourceStatus_STATUS_UNSPECIFIED -} - -func (x *SourceKeepaliveResult) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type ListAllSourcesStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAllSourcesStatusRequest) Reset() { - *x = ListAllSourcesStatusRequest{} - mi := &file_account_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAllSourcesStatusRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAllSourcesStatusRequest) ProtoMessage() {} - -func (x *ListAllSourcesStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[39] - 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 ListAllSourcesStatusRequest.ProtoReflect.Descriptor instead. -func (*ListAllSourcesStatusRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{39} -} - -type SourceHealth struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the source - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The version of the source - Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - // The name of the source - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // The error message if the source is unhealthy - Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` - // The status of the source, this is calculated based on the last heartbeat received and if there is an error - Status SourceStatus `protobuf:"varint,5,opt,name=status,proto3,enum=account.SourceStatus" json:"status,omitempty"` - // Created at time - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - // The last time we received a heartbeat from the source - LastHeartbeat *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=lastHeartbeat,proto3" json:"lastHeartbeat,omitempty"` - // The next time we expect to receive a heartbeat from the source - NextHeartbeat *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=nextHeartbeat,proto3" json:"nextHeartbeat,omitempty"` - // The type of the source, AWS or Stdlib or Kubernetes - Type string `protobuf:"bytes,9,opt,name=type,proto3" json:"type,omitempty"` - // Whether the source is managed, or local - Managed SourceManaged `protobuf:"varint,10,opt,name=managed,proto3,enum=account.SourceManaged" json:"managed,omitempty"` - // The types of sources that this source can discover - AvailableTypes []string `protobuf:"bytes,11,rep,name=availableTypes,proto3" json:"availableTypes,omitempty"` - // The scopes that this source can discover - AvailableScopes []string `protobuf:"bytes,12,rep,name=availableScopes,proto3" json:"availableScopes,omitempty"` - // AdapterMetadata is a map of metadata that the source can send to the API - AdapterMetadata []*AdapterMetadata `protobuf:"bytes,13,rep,name=adapterMetadata,proto3" json:"adapterMetadata,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SourceHealth) Reset() { - *x = SourceHealth{} - mi := &file_account_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SourceHealth) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SourceHealth) ProtoMessage() {} - -func (x *SourceHealth) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[40] - 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 SourceHealth.ProtoReflect.Descriptor instead. -func (*SourceHealth) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{40} -} - -func (x *SourceHealth) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *SourceHealth) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -func (x *SourceHealth) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SourceHealth) GetError() string { - if x != nil && x.Error != nil { - return *x.Error - } - return "" -} - -func (x *SourceHealth) GetStatus() SourceStatus { - if x != nil { - return x.Status - } - return SourceStatus_STATUS_UNSPECIFIED -} - -func (x *SourceHealth) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *SourceHealth) GetLastHeartbeat() *timestamppb.Timestamp { - if x != nil { - return x.LastHeartbeat - } - return nil -} - -func (x *SourceHealth) GetNextHeartbeat() *timestamppb.Timestamp { - if x != nil { - return x.NextHeartbeat - } - return nil -} - -func (x *SourceHealth) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *SourceHealth) GetManaged() SourceManaged { - if x != nil { - return x.Managed - } - return SourceManaged_LOCAL -} - -func (x *SourceHealth) GetAvailableTypes() []string { - if x != nil { - return x.AvailableTypes - } - return nil -} - -func (x *SourceHealth) GetAvailableScopes() []string { - if x != nil { - return x.AvailableScopes - } - return nil -} - -func (x *SourceHealth) GetAdapterMetadata() []*AdapterMetadata { - if x != nil { - return x.AdapterMetadata - } - return nil -} - -type ListAllSourcesStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sources []*SourceHealth `protobuf:"bytes,1,rep,name=sources,proto3" json:"sources,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAllSourcesStatusResponse) Reset() { - *x = ListAllSourcesStatusResponse{} - mi := &file_account_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAllSourcesStatusResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAllSourcesStatusResponse) ProtoMessage() {} - -func (x *ListAllSourcesStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[41] - 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 ListAllSourcesStatusResponse.ProtoReflect.Descriptor instead. -func (*ListAllSourcesStatusResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{41} -} - -func (x *ListAllSourcesStatusResponse) GetSources() []*SourceHealth { - if x != nil { - return x.Sources - } - return nil -} - -// The source sends a heartbeat to the API to let it know that it is still alive, note it does not give a status. -type SubmitSourceHeartbeatRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the source that is sending the heartbeat - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The version of the source - Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` - // The name of the source - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // The error message if the source is unhealthy - Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` - // The maximum time between heartbeats that the source can send to the api-server. Otherwise, the source will be marked as unhealthy. eg 30s - NextHeartbeatMax *durationpb.Duration `protobuf:"bytes,5,opt,name=nextHeartbeatMax,proto3" json:"nextHeartbeatMax,omitempty"` - // The type of the source, AWS or Stdlib or Kubernetes - Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` - // Whether the source is managed, or local - Managed SourceManaged `protobuf:"varint,7,opt,name=managed,proto3,enum=account.SourceManaged" json:"managed,omitempty"` - // The scopes that this source can discover - AvailableScopes []string `protobuf:"bytes,9,rep,name=availableScopes,proto3" json:"availableScopes,omitempty"` - // AdapterMetadata is a map of metadata that the source can send to the API - AdapterMetadata []*AdapterMetadata `protobuf:"bytes,10,rep,name=adapterMetadata,proto3" json:"adapterMetadata,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SubmitSourceHeartbeatRequest) Reset() { - *x = SubmitSourceHeartbeatRequest{} - mi := &file_account_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SubmitSourceHeartbeatRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SubmitSourceHeartbeatRequest) ProtoMessage() {} - -func (x *SubmitSourceHeartbeatRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[42] - 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 SubmitSourceHeartbeatRequest.ProtoReflect.Descriptor instead. -func (*SubmitSourceHeartbeatRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{42} -} - -func (x *SubmitSourceHeartbeatRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *SubmitSourceHeartbeatRequest) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -func (x *SubmitSourceHeartbeatRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SubmitSourceHeartbeatRequest) GetError() string { - if x != nil && x.Error != nil { - return *x.Error - } - return "" -} - -func (x *SubmitSourceHeartbeatRequest) GetNextHeartbeatMax() *durationpb.Duration { - if x != nil { - return x.NextHeartbeatMax - } - return nil -} - -func (x *SubmitSourceHeartbeatRequest) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *SubmitSourceHeartbeatRequest) GetManaged() SourceManaged { - if x != nil { - return x.Managed - } - return SourceManaged_LOCAL -} - -func (x *SubmitSourceHeartbeatRequest) GetAvailableScopes() []string { - if x != nil { - return x.AvailableScopes - } - return nil -} - -func (x *SubmitSourceHeartbeatRequest) GetAdapterMetadata() []*AdapterMetadata { - if x != nil { - return x.AdapterMetadata - } - return nil -} - -type AdapterMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The type of item that this adapter returns e.g. eks-cluster - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - // The category that these items fall under - Category AdapterCategory `protobuf:"varint,2,opt,name=category,proto3,enum=account.AdapterCategory" json:"category,omitempty"` - // The list of other types that this can be linked to, eg eks-cluster -> - // eks-node-group - PotentialLinks []string `protobuf:"bytes,3,rep,name=potentialLinks,proto3" json:"potentialLinks,omitempty"` - // A descriptive name of the types of items that are returned by this - // adapter e.g. "EKS Cluster" - DescriptiveName string `protobuf:"bytes,4,opt,name=descriptiveName,proto3" json:"descriptiveName,omitempty"` - // The supported query methods for this adapter - SupportedQueryMethods *AdapterSupportedQueryMethods `protobuf:"bytes,5,opt,name=supportedQueryMethods,proto3" json:"supportedQueryMethods,omitempty"` - // The terraform mappings for this adapter, this is optional - TerraformMappings []*TerraformMapping `protobuf:"bytes,6,rep,name=terraformMappings,proto3" json:"terraformMappings,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdapterMetadata) Reset() { - *x = AdapterMetadata{} - mi := &file_account_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdapterMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdapterMetadata) ProtoMessage() {} - -func (x *AdapterMetadata) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[43] - 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 AdapterMetadata.ProtoReflect.Descriptor instead. -func (*AdapterMetadata) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{43} -} - -func (x *AdapterMetadata) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *AdapterMetadata) GetCategory() AdapterCategory { - if x != nil { - return x.Category - } - return AdapterCategory_ADAPTER_CATEGORY_OTHER -} - -func (x *AdapterMetadata) GetPotentialLinks() []string { - if x != nil { - return x.PotentialLinks - } - return nil -} - -func (x *AdapterMetadata) GetDescriptiveName() string { - if x != nil { - return x.DescriptiveName - } - return "" -} - -func (x *AdapterMetadata) GetSupportedQueryMethods() *AdapterSupportedQueryMethods { - if x != nil { - return x.SupportedQueryMethods - } - return nil -} - -func (x *AdapterMetadata) GetTerraformMappings() []*TerraformMapping { - if x != nil { - return x.TerraformMappings - } - return nil -} - -// The methods that this adapter supports, and the description of how to use -// them -type AdapterSupportedQueryMethods struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Whether or not the GET method is supported - Get bool `protobuf:"varint,1,opt,name=get,proto3" json:"get,omitempty"` - // Description of what data the GET query expects. - GetDescription string `protobuf:"bytes,2,opt,name=getDescription,proto3" json:"getDescription,omitempty"` - // Whether or not the LIST method is supported - List bool `protobuf:"varint,3,opt,name=list,proto3" json:"list,omitempty"` - // Description of how the LIST method works - ListDescription string `protobuf:"bytes,4,opt,name=listDescription,proto3" json:"listDescription,omitempty"` - // Whether or not the SEARCH method is supported - Search bool `protobuf:"varint,5,opt,name=search,proto3" json:"search,omitempty"` - // Description of the query that should be passed to the SEARCH method - SearchDescription string `protobuf:"bytes,6,opt,name=searchDescription,proto3" json:"searchDescription,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AdapterSupportedQueryMethods) Reset() { - *x = AdapterSupportedQueryMethods{} - mi := &file_account_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AdapterSupportedQueryMethods) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AdapterSupportedQueryMethods) ProtoMessage() {} - -func (x *AdapterSupportedQueryMethods) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[44] - 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 AdapterSupportedQueryMethods.ProtoReflect.Descriptor instead. -func (*AdapterSupportedQueryMethods) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{44} -} - -func (x *AdapterSupportedQueryMethods) GetGet() bool { - if x != nil { - return x.Get - } - return false -} - -func (x *AdapterSupportedQueryMethods) GetGetDescription() string { - if x != nil { - return x.GetDescription - } - return "" -} - -func (x *AdapterSupportedQueryMethods) GetList() bool { - if x != nil { - return x.List - } - return false -} - -func (x *AdapterSupportedQueryMethods) GetListDescription() string { - if x != nil { - return x.ListDescription - } - return "" -} - -func (x *AdapterSupportedQueryMethods) GetSearch() bool { - if x != nil { - return x.Search - } - return false -} - -func (x *AdapterSupportedQueryMethods) GetSearchDescription() string { - if x != nil { - return x.SearchDescription - } - return "" -} - -// When Overmind ingests Terraform changes, it needs to be able to map from a -// given Terraform resource, to that same resource in Overmind. This is achieved -// by using the TerraformMapping object. It translates the details of a Terraform -// resource into a query that Overmind can run. -// -// NOTE: The queries that are generated by this mapping use the wildcard scope -// `*` and therefore could return multiple items. Overmind will compare the -// attributes of these items to determine the most likely candidate for a mch -// and select that. -type TerraformMapping struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The method that the query should use - TerraformMethod QueryMethod `protobuf:"varint,1,opt,name=terraformMethod,proto3,enum=QueryMethod" json:"terraformMethod,omitempty"` - // How to map data from the terraform resource to the "query" field in the - // resulting mapping query. This uses HCL syntax e.g. - // resource_type.attribute_name - // - // Usually this will be the attribute that uniquely identifies the resource - // such as `aws_instance.id` or `aws_iam_role.arn`. You can also index into - // arrays e.g. `kubernetes_replication_controller.metadata[0].name` - TerraformQueryMap string `protobuf:"bytes,2,opt,name=terraformQueryMap,proto3" json:"terraformQueryMap,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TerraformMapping) Reset() { - *x = TerraformMapping{} - mi := &file_account_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TerraformMapping) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TerraformMapping) ProtoMessage() {} - -func (x *TerraformMapping) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[45] - 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 TerraformMapping.ProtoReflect.Descriptor instead. -func (*TerraformMapping) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{45} -} - -func (x *TerraformMapping) GetTerraformMethod() QueryMethod { - if x != nil { - return x.TerraformMethod - } - return QueryMethod_GET -} - -func (x *TerraformMapping) GetTerraformQueryMap() string { - if x != nil { - return x.TerraformQueryMap - } - return "" -} - -type SubmitSourceHeartbeatResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SubmitSourceHeartbeatResponse) Reset() { - *x = SubmitSourceHeartbeatResponse{} - mi := &file_account_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SubmitSourceHeartbeatResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SubmitSourceHeartbeatResponse) ProtoMessage() {} - -func (x *SubmitSourceHeartbeatResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[46] - 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 SubmitSourceHeartbeatResponse.ProtoReflect.Descriptor instead. -func (*SubmitSourceHeartbeatResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{46} -} - -type KeepaliveSourcesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Set to true to have the API call wait until the source is up and healthy - WaitForHealthy bool `protobuf:"varint,1,opt,name=waitForHealthy,proto3" json:"waitForHealthy,omitempty"` - // Maximum time to wait for sources to reach a final state. Only used when - // waitForHealthy is true. If not specified, defaults to 4 minutes. - // After this timeout, the API will return the current state of all sources - // regardless of whether they have reached a final state. - Timeout *durationpb.Duration `protobuf:"bytes,2,opt,name=timeout,proto3" json:"timeout,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *KeepaliveSourcesRequest) Reset() { - *x = KeepaliveSourcesRequest{} - mi := &file_account_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *KeepaliveSourcesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*KeepaliveSourcesRequest) ProtoMessage() {} - -func (x *KeepaliveSourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[47] - 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 KeepaliveSourcesRequest.ProtoReflect.Descriptor instead. -func (*KeepaliveSourcesRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{47} -} - -func (x *KeepaliveSourcesRequest) GetWaitForHealthy() bool { - if x != nil { - return x.WaitForHealthy - } - return false -} - -func (x *KeepaliveSourcesRequest) GetTimeout() *durationpb.Duration { - if x != nil { - return x.Timeout - } - return nil -} - -type KeepaliveSourcesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // If the user requested to wait for the sources to be healthy, this will - // contain information about the sources that came up. If the user did not - // request to wait, this will be empty - Sources []*SourceKeepaliveResult `protobuf:"bytes,1,rep,name=sources,proto3" json:"sources,omitempty"` - // If all sources are healthy, this will be true. If any source is unhealthy, - // this will be false. If the user did not request to wait for sources to - // become healthy, this will be false. - AllSourcesHealthy bool `protobuf:"varint,2,opt,name=allSourcesHealthy,proto3" json:"allSourcesHealthy,omitempty"` - // If any source is healthy, this will be true. If all sources are unhealthy, - // this will be false. If the user did not request to wait for sources to - // become healthy, this will be false. - AnySourcesHealthy bool `protobuf:"varint,3,opt,name=anySourcesHealthy,proto3" json:"anySourcesHealthy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *KeepaliveSourcesResponse) Reset() { - *x = KeepaliveSourcesResponse{} - mi := &file_account_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *KeepaliveSourcesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*KeepaliveSourcesResponse) ProtoMessage() {} - -func (x *KeepaliveSourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[48] - 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 KeepaliveSourcesResponse.ProtoReflect.Descriptor instead. -func (*KeepaliveSourcesResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{48} -} - -func (x *KeepaliveSourcesResponse) GetSources() []*SourceKeepaliveResult { - if x != nil { - return x.Sources - } - return nil -} - -func (x *KeepaliveSourcesResponse) GetAllSourcesHealthy() bool { - if x != nil { - return x.AllSourcesHealthy - } - return false -} - -func (x *KeepaliveSourcesResponse) GetAnySourcesHealthy() bool { - if x != nil { - return x.AnySourcesHealthy - } - return false -} - -type CreateTokenRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The Public NKey of the user that is requesting a token - UserPublicNkey string `protobuf:"bytes,1,opt,name=userPublicNkey,proto3" json:"userPublicNkey,omitempty"` - // Friendly user name - UserName string `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateTokenRequest) Reset() { - *x = CreateTokenRequest{} - mi := &file_account_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateTokenRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateTokenRequest) ProtoMessage() {} - -func (x *CreateTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[49] - 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 CreateTokenRequest.ProtoReflect.Descriptor instead. -func (*CreateTokenRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{49} -} - -func (x *CreateTokenRequest) GetUserPublicNkey() string { - if x != nil { - return x.UserPublicNkey - } - return "" -} - -func (x *CreateTokenRequest) GetUserName() string { - if x != nil { - return x.UserName - } - return "" -} - -type CreateTokenResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The JWT as a raw string - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateTokenResponse) Reset() { - *x = CreateTokenResponse{} - mi := &file_account_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateTokenResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateTokenResponse) ProtoMessage() {} - -func (x *CreateTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[50] - 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 CreateTokenResponse.ProtoReflect.Descriptor instead. -func (*CreateTokenResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{50} -} - -func (x *CreateTokenResponse) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -type RevlinkWarmupRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RevlinkWarmupRequest) Reset() { - *x = RevlinkWarmupRequest{} - mi := &file_account_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RevlinkWarmupRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RevlinkWarmupRequest) ProtoMessage() {} - -func (x *RevlinkWarmupRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[51] - 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 RevlinkWarmupRequest.ProtoReflect.Descriptor instead. -func (*RevlinkWarmupRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{51} -} - -type RevlinkWarmupResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` - Items int32 `protobuf:"varint,2,opt,name=items,proto3" json:"items,omitempty"` - Edges int32 `protobuf:"varint,3,opt,name=edges,proto3" json:"edges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RevlinkWarmupResponse) Reset() { - *x = RevlinkWarmupResponse{} - mi := &file_account_proto_msgTypes[52] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RevlinkWarmupResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RevlinkWarmupResponse) ProtoMessage() {} - -func (x *RevlinkWarmupResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[52] - 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 RevlinkWarmupResponse.ProtoReflect.Descriptor instead. -func (*RevlinkWarmupResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{52} -} - -func (x *RevlinkWarmupResponse) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *RevlinkWarmupResponse) GetItems() int32 { - if x != nil { - return x.Items - } - return 0 -} - -func (x *RevlinkWarmupResponse) GetEdges() int32 { - if x != nil { - return x.Edges - } - return 0 -} - -type AvailableItemType struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The type of item that this adapter returns e.g. eks-cluster - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - // The category that these items fall under - Category AdapterCategory `protobuf:"varint,2,opt,name=category,proto3,enum=account.AdapterCategory" json:"category,omitempty"` - // A descriptive name of the types of items that are returned by this - // adapter e.g. "EKS Cluster" - DescriptiveName string `protobuf:"bytes,3,opt,name=descriptiveName,proto3" json:"descriptiveName,omitempty"` - // The supported query methods for this adapter - SupportedQueryMethods *AdapterSupportedQueryMethods `protobuf:"bytes,4,opt,name=supportedQueryMethods,proto3" json:"supportedQueryMethods,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AvailableItemType) Reset() { - *x = AvailableItemType{} - mi := &file_account_proto_msgTypes[53] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AvailableItemType) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AvailableItemType) ProtoMessage() {} - -func (x *AvailableItemType) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[53] - 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 AvailableItemType.ProtoReflect.Descriptor instead. -func (*AvailableItemType) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{53} -} - -func (x *AvailableItemType) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *AvailableItemType) GetCategory() AdapterCategory { - if x != nil { - return x.Category - } - return AdapterCategory_ADAPTER_CATEGORY_OTHER -} - -func (x *AvailableItemType) GetDescriptiveName() string { - if x != nil { - return x.DescriptiveName - } - return "" -} - -func (x *AvailableItemType) GetSupportedQueryMethods() *AdapterSupportedQueryMethods { - if x != nil { - return x.SupportedQueryMethods - } - return nil -} - -type ListAvailableItemTypesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAvailableItemTypesRequest) Reset() { - *x = ListAvailableItemTypesRequest{} - mi := &file_account_proto_msgTypes[54] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAvailableItemTypesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAvailableItemTypesRequest) ProtoMessage() {} - -func (x *ListAvailableItemTypesRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[54] - 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 ListAvailableItemTypesRequest.ProtoReflect.Descriptor instead. -func (*ListAvailableItemTypesRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{54} -} - -type ListAvailableItemTypesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Types []*AvailableItemType `protobuf:"bytes,1,rep,name=types,proto3" json:"types,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAvailableItemTypesResponse) Reset() { - *x = ListAvailableItemTypesResponse{} - mi := &file_account_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAvailableItemTypesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAvailableItemTypesResponse) ProtoMessage() {} - -func (x *ListAvailableItemTypesResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[55] - 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 ListAvailableItemTypesResponse.ProtoReflect.Descriptor instead. -func (*ListAvailableItemTypesResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{55} -} - -func (x *ListAvailableItemTypesResponse) GetTypes() []*AvailableItemType { - if x != nil { - return x.Types - } - return nil -} - -type GetSourceStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // UUID of the source to get status for - SourceUuid []byte `protobuf:"bytes,1,opt,name=source_uuid,json=sourceUuid,proto3" json:"source_uuid,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSourceStatusRequest) Reset() { - *x = GetSourceStatusRequest{} - mi := &file_account_proto_msgTypes[56] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSourceStatusRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSourceStatusRequest) ProtoMessage() {} - -func (x *GetSourceStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[56] - 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 GetSourceStatusRequest.ProtoReflect.Descriptor instead. -func (*GetSourceStatusRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{56} -} - -func (x *GetSourceStatusRequest) GetSourceUuid() []byte { - if x != nil { - return x.SourceUuid - } - return nil -} - -type GetSourceStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source *SourceHealth `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSourceStatusResponse) Reset() { - *x = GetSourceStatusResponse{} - mi := &file_account_proto_msgTypes[57] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSourceStatusResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSourceStatusResponse) ProtoMessage() {} - -func (x *GetSourceStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[57] - 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 GetSourceStatusResponse.ProtoReflect.Descriptor instead. -func (*GetSourceStatusResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{57} -} - -func (x *GetSourceStatusResponse) GetSource() *SourceHealth { - if x != nil { - return x.Source - } - return nil -} - -type GetUserOnboardingStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetUserOnboardingStatusRequest) Reset() { - *x = GetUserOnboardingStatusRequest{} - mi := &file_account_proto_msgTypes[58] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetUserOnboardingStatusRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetUserOnboardingStatusRequest) ProtoMessage() {} - -func (x *GetUserOnboardingStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[58] - 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 GetUserOnboardingStatusRequest.ProtoReflect.Descriptor instead. -func (*GetUserOnboardingStatusRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{58} -} - -type GetUserOnboardingStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - OnboardingComplete bool `protobuf:"varint,1,opt,name=onboarding_complete,json=onboardingComplete,proto3" json:"onboarding_complete,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetUserOnboardingStatusResponse) Reset() { - *x = GetUserOnboardingStatusResponse{} - mi := &file_account_proto_msgTypes[59] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetUserOnboardingStatusResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetUserOnboardingStatusResponse) ProtoMessage() {} - -func (x *GetUserOnboardingStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[59] - 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 GetUserOnboardingStatusResponse.ProtoReflect.Descriptor instead. -func (*GetUserOnboardingStatusResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{59} -} - -func (x *GetUserOnboardingStatusResponse) GetOnboardingComplete() bool { - if x != nil { - return x.OnboardingComplete - } - return false -} - -type SetUserOnboardingStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OnboardingComplete bool `protobuf:"varint,1,opt,name=onboarding_complete,json=onboardingComplete,proto3" json:"onboarding_complete,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetUserOnboardingStatusRequest) Reset() { - *x = SetUserOnboardingStatusRequest{} - mi := &file_account_proto_msgTypes[60] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetUserOnboardingStatusRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetUserOnboardingStatusRequest) ProtoMessage() {} - -func (x *SetUserOnboardingStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[60] - 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 SetUserOnboardingStatusRequest.ProtoReflect.Descriptor instead. -func (*SetUserOnboardingStatusRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{60} -} - -func (x *SetUserOnboardingStatusRequest) GetOnboardingComplete() bool { - if x != nil { - return x.OnboardingComplete - } - return false -} - -type SetUserOnboardingStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetUserOnboardingStatusResponse) Reset() { - *x = SetUserOnboardingStatusResponse{} - mi := &file_account_proto_msgTypes[61] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetUserOnboardingStatusResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetUserOnboardingStatusResponse) ProtoMessage() {} - -func (x *SetUserOnboardingStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[61] - 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 SetUserOnboardingStatusResponse.ProtoReflect.Descriptor instead. -func (*SetUserOnboardingStatusResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{61} -} - -type SetGithubInstallationIDRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - GithubInstallationId int64 `protobuf:"varint,1,opt,name=github_installation_id,json=githubInstallationId,proto3" json:"github_installation_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetGithubInstallationIDRequest) Reset() { - *x = SetGithubInstallationIDRequest{} - mi := &file_account_proto_msgTypes[62] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetGithubInstallationIDRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetGithubInstallationIDRequest) ProtoMessage() {} - -func (x *SetGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[62] - 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 SetGithubInstallationIDRequest.ProtoReflect.Descriptor instead. -func (*SetGithubInstallationIDRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{62} -} - -func (x *SetGithubInstallationIDRequest) GetGithubInstallationId() int64 { - if x != nil { - return x.GithubInstallationId - } - return 0 -} - -type SetGithubInstallationIDResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetGithubInstallationIDResponse) Reset() { - *x = SetGithubInstallationIDResponse{} - mi := &file_account_proto_msgTypes[63] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetGithubInstallationIDResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetGithubInstallationIDResponse) ProtoMessage() {} - -func (x *SetGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[63] - 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 SetGithubInstallationIDResponse.ProtoReflect.Descriptor instead. -func (*SetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{63} -} - -type UnsetGithubInstallationIDRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UnsetGithubInstallationIDRequest) Reset() { - *x = UnsetGithubInstallationIDRequest{} - mi := &file_account_proto_msgTypes[64] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UnsetGithubInstallationIDRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UnsetGithubInstallationIDRequest) ProtoMessage() {} - -func (x *UnsetGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[64] - 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 UnsetGithubInstallationIDRequest.ProtoReflect.Descriptor instead. -func (*UnsetGithubInstallationIDRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{64} -} - -type UnsetGithubInstallationIDResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UnsetGithubInstallationIDResponse) Reset() { - *x = UnsetGithubInstallationIDResponse{} - mi := &file_account_proto_msgTypes[65] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UnsetGithubInstallationIDResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UnsetGithubInstallationIDResponse) ProtoMessage() {} - -func (x *UnsetGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[65] - 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 UnsetGithubInstallationIDResponse.ProtoReflect.Descriptor instead. -func (*UnsetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{65} -} - -// Team member related messages -type ListTeamMembersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListTeamMembersRequest) Reset() { - *x = ListTeamMembersRequest{} - mi := &file_account_proto_msgTypes[66] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListTeamMembersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListTeamMembersRequest) ProtoMessage() {} - -func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[66] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListTeamMembersRequest.ProtoReflect.Descriptor instead. -func (*ListTeamMembersRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{66} -} - -type ListTeamMembersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Members []*TeamMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListTeamMembersResponse) Reset() { - *x = ListTeamMembersResponse{} - mi := &file_account_proto_msgTypes[67] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListTeamMembersResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListTeamMembersResponse) ProtoMessage() {} - -func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[67] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListTeamMembersResponse.ProtoReflect.Descriptor instead. -func (*ListTeamMembersResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{67} -} - -func (x *ListTeamMembersResponse) GetMembers() []*TeamMember { - if x != nil { - return x.Members - } - return nil -} - -type TeamMember struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Unique identifier for the team member - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Team member's display name - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // URL to the team member's profile picture - PictureUrl string `protobuf:"bytes,3,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TeamMember) Reset() { - *x = TeamMember{} - mi := &file_account_proto_msgTypes[68] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TeamMember) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TeamMember) ProtoMessage() {} - -func (x *TeamMember) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[68] - 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 TeamMember.ProtoReflect.Descriptor instead. -func (*TeamMember) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{68} -} - -func (x *TeamMember) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *TeamMember) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *TeamMember) GetPictureUrl() string { - if x != nil { - return x.PictureUrl - } - return "" -} - -type GetWelcomeScreenInformationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetWelcomeScreenInformationRequest) Reset() { - *x = GetWelcomeScreenInformationRequest{} - mi := &file_account_proto_msgTypes[69] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetWelcomeScreenInformationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetWelcomeScreenInformationRequest) ProtoMessage() {} - -func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[69] - 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 GetWelcomeScreenInformationRequest.ProtoReflect.Descriptor instead. -func (*GetWelcomeScreenInformationRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{69} -} - -type InviterInformation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - PictureUrl string `protobuf:"bytes,3,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *InviterInformation) Reset() { - *x = InviterInformation{} - mi := &file_account_proto_msgTypes[70] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *InviterInformation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InviterInformation) ProtoMessage() {} - -func (x *InviterInformation) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[70] - 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 InviterInformation.ProtoReflect.Descriptor instead. -func (*InviterInformation) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{70} -} - -func (x *InviterInformation) GetEmail() string { - if x != nil { - return x.Email - } - return "" -} - -func (x *InviterInformation) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *InviterInformation) GetPictureUrl() string { - if x != nil { - return x.PictureUrl - } - return "" -} - -type GetWelcomeScreenInformationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - InviterInformation *InviterInformation `protobuf:"bytes,1,opt,name=inviter_information,json=inviterInformation,proto3" json:"inviter_information,omitempty"` // potentially we can return account / organisation information here - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetWelcomeScreenInformationResponse) Reset() { - *x = GetWelcomeScreenInformationResponse{} - mi := &file_account_proto_msgTypes[71] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetWelcomeScreenInformationResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetWelcomeScreenInformationResponse) ProtoMessage() {} - -func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[71] - 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 GetWelcomeScreenInformationResponse.ProtoReflect.Descriptor instead. -func (*GetWelcomeScreenInformationResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{71} -} - -func (x *GetWelcomeScreenInformationResponse) GetInviterInformation() *InviterInformation { - if x != nil { - return x.InviterInformation - } - return nil -} - -var File_account_proto protoreflect.FileDescriptor - -const file_account_proto_rawDesc = "" + - "\n" + - "\raccount.proto\x12\aaccount\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vitems.proto\x1a\x1bbuf/validate/validate.proto\"\x15\n" + - "\x13ListAccountsRequest\"D\n" + - "\x14ListAccountsResponse\x12,\n" + - "\baccounts\x18\x01 \x03(\v2\x10.account.AccountR\baccounts\"R\n" + - "\x14CreateAccountRequest\x12:\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1a.account.AccountPropertiesR\n" + - "properties\"C\n" + - "\x15CreateAccountResponse\x12*\n" + - "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"R\n" + - "\x14UpdateAccountRequest\x12:\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1a.account.AccountPropertiesR\n" + - "properties\"C\n" + - "\x15UpdateAccountResponse\x12*\n" + - "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"h\n" + - "\x19AdminUpdateAccountRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x127\n" + - "\arequest\x18\x02 \x01(\v2\x1d.account.UpdateAccountRequestR\arequest\",\n" + - "\x16AdminGetAccountRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"/\n" + - "\x19AdminDeleteAccountRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"\x1c\n" + - "\x1aAdminDeleteAccountResponse\"j\n" + - "\x17AdminListSourcesRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x125\n" + - "\arequest\x18\x02 \x01(\v2\x1b.account.ListSourcesRequestR\arequest\"l\n" + - "\x18AdminCreateSourceRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + - "\arequest\x18\x02 \x01(\v2\x1c.account.CreateSourceRequestR\arequest\"f\n" + - "\x15AdminGetSourceRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x123\n" + - "\arequest\x18\x02 \x01(\v2\x19.account.GetSourceRequestR\arequest\"l\n" + - "\x18AdminUpdateSourceRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + - "\arequest\x18\x02 \x01(\v2\x1c.account.UpdateSourceRequestR\arequest\"l\n" + - "\x18AdminDeleteSourceRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + - "\arequest\x18\x02 \x01(\v2\x1c.account.DeleteSourceRequestR\arequest\"t\n" + - "\x1cAdminKeepaliveSourcesRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x12:\n" + - "\arequest\x18\x02 \x01(\v2 .account.KeepaliveSourcesRequestR\arequest\"j\n" + - "\x17AdminCreateTokenRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x125\n" + - "\arequest\x18\x02 \x01(\v2\x1b.account.CreateTokenRequestR\arequest\"x\n" + - "\x06Source\x123\n" + - "\bmetadata\x18\x01 \x01(\v2\x17.account.SourceMetadataR\bmetadata\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.account.SourcePropertiesR\n" + - "properties\"\xe5\x01\n" + - "\x0eSourceMetadata\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x1c\n" + - "\tTokenName\x18\x02 \x01(\tR\tTokenName\x12<\n" + - "\vTokenExpiry\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\vTokenExpiry\x12\x1e\n" + - "\n" + - "PublicNkey\x18\x05 \x01(\tR\n" + - "PublicNkey\x12-\n" + - "\x06Status\x18\t \x01(\x0e2\x15.account.SourceStatusR\x06Status\x12\x14\n" + - "\x05Error\x18\n" + - " \x01(\tR\x05Error\"\xc6\x01\n" + - "\x10SourceProperties\x12(\n" + - "\x0fDescriptiveName\x18\x01 \x01(\tR\x0fDescriptiveName\x12\x12\n" + - "\x04Type\x18\x02 \x01(\tR\x04Type\x12/\n" + - "\x06Config\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x06Config\x12C\n" + - "\x10AdditionalConfig\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x10AdditionalConfig\"{\n" + - "\aAccount\x124\n" + - "\bmetadata\x18\x01 \x01(\v2\x18.account.AccountMetadataR\bmetadata\x12:\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1a.account.AccountPropertiesR\n" + - "properties\"\xfc\x01\n" + - "\x0fAccountMetadata\x12\x1e\n" + - "\n" + - "PublicNkey\x18\x02 \x01(\tR\n" + - "PublicNkey\x127\n" + - "\frepositories\x18\x03 \x03(\v2\x13.account.RepositoryR\frepositories\x12,\n" + - "\x11totalRepositories\x18\x04 \x01(\rR\x11totalRepositories\x12.\n" + - "\x12activeRepositories\x18\x05 \x01(\rR\x12activeRepositories\x122\n" + - "\x04Plan\x18\x06 \x01(\x0e2\x14.account.AccountPlanB\b\xbaH\x05\x82\x01\x02\x10\x01R\x04Plan\"\xbd\x01\n" + - "\n" + - "Repository\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + - "\n" + - "numChanges\x18\x02 \x01(\x03R\n" + - "numChanges\x12>\n" + - "\flastChangeAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\flastChangeAt\x12;\n" + - "\x06status\x18\x04 \x01(\x0e2\x19.account.RepositoryStatusB\b\xbaH\x05\x82\x01\x02\x10\x01R\x06status\"S\n" + - "\x11AccountProperties\x12\x12\n" + - "\x04Name\x18\x01 \x01(\tR\x04Name\x12*\n" + - "\x10StripeCustomerID\x18\x02 \x01(\tR\x10StripeCustomerID\"\x13\n" + - "\x11GetAccountRequest\"@\n" + - "\x12GetAccountResponse\x12*\n" + - "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"0\n" + - "\x14DeleteAccountRequest\x12\x18\n" + - "\aiAmSure\x18\x01 \x01(\bR\aiAmSure\"\x17\n" + - "\x15DeleteAccountResponse\"\x14\n" + - "\x12ListSourcesRequest\"@\n" + - "\x13ListSourcesResponse\x12)\n" + - "\aSources\x18\x01 \x03(\v2\x0f.account.SourceR\aSources\"P\n" + - "\x13CreateSourceRequest\x129\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x19.account.SourcePropertiesR\n" + - "properties\"?\n" + - "\x14CreateSourceResponse\x12'\n" + - "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\"&\n" + - "\x10GetSourceRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"<\n" + - "\x11GetSourceResponse\x12'\n" + - "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\"d\n" + - "\x13UpdateSourceRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.account.SourcePropertiesR\n" + - "properties\"?\n" + - "\x14UpdateSourceResponse\x12'\n" + - "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\")\n" + - "\x13DeleteSourceRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x16\n" + - "\x14DeleteSourceResponse\"p\n" + - "\x15SourceKeepaliveResult\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12-\n" + - "\x06Status\x18\x02 \x01(\x0e2\x15.account.SourceStatusR\x06Status\x12\x14\n" + - "\x05Error\x18\x03 \x01(\tR\x05Error\"\x1d\n" + - "\x1bListAllSourcesStatusRequest\"\xbe\x04\n" + - "\fSourceHealth\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x18\n" + - "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + - "\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01\x12-\n" + - "\x06status\x18\x05 \x01(\x0e2\x15.account.SourceStatusR\x06status\x128\n" + - "\tcreatedAt\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12@\n" + - "\rlastHeartbeat\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\rlastHeartbeat\x12@\n" + - "\rnextHeartbeat\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\rnextHeartbeat\x12\x12\n" + - "\x04type\x18\t \x01(\tR\x04type\x120\n" + - "\amanaged\x18\n" + - " \x01(\x0e2\x16.account.SourceManagedR\amanaged\x12&\n" + - "\x0eavailableTypes\x18\v \x03(\tR\x0eavailableTypes\x12(\n" + - "\x0favailableScopes\x18\f \x03(\tR\x0favailableScopes\x12B\n" + - "\x0fadapterMetadata\x18\r \x03(\v2\x18.account.AdapterMetadataR\x0fadapterMetadataB\b\n" + - "\x06_error\"O\n" + - "\x1cListAllSourcesStatusResponse\x12/\n" + - "\asources\x18\x01 \x03(\v2\x15.account.SourceHealthR\asources\"\x86\x03\n" + - "\x1cSubmitSourceHeartbeatRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x18\n" + - "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + - "\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01\x12E\n" + - "\x10nextHeartbeatMax\x18\x05 \x01(\v2\x19.google.protobuf.DurationR\x10nextHeartbeatMax\x12\x12\n" + - "\x04type\x18\x06 \x01(\tR\x04type\x120\n" + - "\amanaged\x18\a \x01(\x0e2\x16.account.SourceManagedR\amanaged\x12(\n" + - "\x0favailableScopes\x18\t \x03(\tR\x0favailableScopes\x12B\n" + - "\x0fadapterMetadata\x18\n" + - " \x03(\v2\x18.account.AdapterMetadataR\x0fadapterMetadataB\b\n" + - "\x06_errorJ\x04\b\b\x10\t\"\x9d\x05\n" + - "\x0fAdapterMetadata\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x12>\n" + - "\bcategory\x18\x02 \x01(\x0e2\x18.account.AdapterCategoryB\b\xbaH\x05\x82\x01\x02\x10\x01R\bcategory\x12\xc9\x01\n" + - "\x0epotentialLinks\x18\x03 \x03(\tB\xa0\x01\xbaH\x9c\x01\xba\x01\x98\x01\n" + - "\x18potentialLinksValidation\x12MIf 'potentialLinks' is not empty, none of its members should be empty strings\x1a-this.size() == 0 || this.all(x, x.size() > 0)R\x0epotentialLinks\x124\n" + - "\x0fdescriptiveName\x18\x04 \x01(\tB\n" + - "\xbaH\a\xc8\x01\x01r\x02\x10\x01R\x0fdescriptiveName\x12c\n" + - "\x15supportedQueryMethods\x18\x05 \x01(\v2%.account.AdapterSupportedQueryMethodsB\x06\xbaH\x03\xc8\x01\x01R\x15supportedQueryMethods\x12\xce\x01\n" + - "\x11terraformMappings\x18\x06 \x03(\v2\x19.account.TerraformMappingB\x84\x01\xbaH\x80\x01\xba\x01}\n" + - "\x1bterraformMappingsValidation\x12FIf 'terraformMappings' is not empty, none of its members should be nil\x1a\x16this.all(x, x != null)R\x11terraformMappings\"\xd7\x05\n" + - "\x1cAdapterSupportedQueryMethods\x12\x10\n" + - "\x03get\x18\x01 \x01(\bR\x03get\x12&\n" + - "\x0egetDescription\x18\x02 \x01(\tR\x0egetDescription\x12\x12\n" + - "\x04list\x18\x03 \x01(\bR\x04list\x12(\n" + - "\x0flistDescription\x18\x04 \x01(\tR\x0flistDescription\x12\x16\n" + - "\x06search\x18\x05 \x01(\bR\x06search\x12,\n" + - "\x11searchDescription\x18\x06 \x01(\tR\x11searchDescription:\xf8\x03\xbaH\xf4\x03\x1a\x9d\x01\n" + - "*AdapterSupportedQueryMethods.getValidation\x12BIf 'get' is true, 'getDescription' must have more than 1 character\x1a+!this.get || this.getDescription.size() > 1\x1a\xa2\x01\n" + - "+AdapterSupportedQueryMethods.listValidation\x12DIf 'list' is true, 'listDescription' must have more than 1 character\x1a-!this.list || this.listDescription.size() > 1\x1a\xac\x01\n" + - "-AdapterSupportedQueryMethods.searchValidation\x12HIf 'search' is true, 'searchDescription' must have more than 1 character\x1a1!this.search || this.searchDescription.size() > 1\"\xad\x02\n" + - "\x10TerraformMapping\x12@\n" + - "\x0fterraformMethod\x18\x01 \x01(\x0e2\f.QueryMethodB\b\xbaH\x05\x82\x01\x02\x10\x01R\x0fterraformMethod\x12\xd0\x01\n" + - "\x11terraformQueryMap\x18\x02 \x01(\tB\xa1\x01\xbaH\x9d\x01\xba\x01\x92\x01\n" + - "\x17terraformQueryMapFormat\x12ZThe value must be in the format '.' (dot notation with exactly two items)\x1a\x1bthis.split('.').size() == 2\xc8\x01\x01r\x02\x10\x03R\x11terraformQueryMapJ\x04\b\x03\x10\x04\"\x1f\n" + - "\x1dSubmitSourceHeartbeatResponse\"v\n" + - "\x17KeepaliveSourcesRequest\x12&\n" + - "\x0ewaitForHealthy\x18\x01 \x01(\bR\x0ewaitForHealthy\x123\n" + - "\atimeout\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xb0\x01\n" + - "\x18KeepaliveSourcesResponse\x128\n" + - "\asources\x18\x01 \x03(\v2\x1e.account.SourceKeepaliveResultR\asources\x12,\n" + - "\x11allSourcesHealthy\x18\x02 \x01(\bR\x11allSourcesHealthy\x12,\n" + - "\x11anySourcesHealthy\x18\x03 \x01(\bR\x11anySourcesHealthy\"X\n" + - "\x12CreateTokenRequest\x12&\n" + - "\x0euserPublicNkey\x18\x01 \x01(\tR\x0euserPublicNkey\x12\x1a\n" + - "\buserName\x18\x02 \x01(\tR\buserName\"+\n" + - "\x13CreateTokenResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\"\x16\n" + - "\x14RevlinkWarmupRequest\"[\n" + - "\x15RevlinkWarmupResponse\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + - "\x05items\x18\x02 \x01(\x05R\x05items\x12\x14\n" + - "\x05edges\x18\x03 \x01(\x05R\x05edges\"\xe4\x01\n" + - "\x11AvailableItemType\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x124\n" + - "\bcategory\x18\x02 \x01(\x0e2\x18.account.AdapterCategoryR\bcategory\x12(\n" + - "\x0fdescriptiveName\x18\x03 \x01(\tR\x0fdescriptiveName\x12[\n" + - "\x15supportedQueryMethods\x18\x04 \x01(\v2%.account.AdapterSupportedQueryMethodsR\x15supportedQueryMethods\"\x1f\n" + - "\x1dListAvailableItemTypesRequest\"R\n" + - "\x1eListAvailableItemTypesResponse\x120\n" + - "\x05types\x18\x01 \x03(\v2\x1a.account.AvailableItemTypeR\x05types\"9\n" + - "\x16GetSourceStatusRequest\x12\x1f\n" + - "\vsource_uuid\x18\x01 \x01(\fR\n" + - "sourceUuid\"H\n" + - "\x17GetSourceStatusResponse\x12-\n" + - "\x06source\x18\x01 \x01(\v2\x15.account.SourceHealthR\x06source\" \n" + - "\x1eGetUserOnboardingStatusRequest\"R\n" + - "\x1fGetUserOnboardingStatusResponse\x12/\n" + - "\x13onboarding_complete\x18\x01 \x01(\bR\x12onboardingComplete\"Q\n" + - "\x1eSetUserOnboardingStatusRequest\x12/\n" + - "\x13onboarding_complete\x18\x01 \x01(\bR\x12onboardingComplete\"!\n" + - "\x1fSetUserOnboardingStatusResponse\"V\n" + - "\x1eSetGithubInstallationIDRequest\x124\n" + - "\x16github_installation_id\x18\x01 \x01(\x03R\x14githubInstallationId\"!\n" + - "\x1fSetGithubInstallationIDResponse\"\"\n" + - " UnsetGithubInstallationIDRequest\"#\n" + - "!UnsetGithubInstallationIDResponse\"\x18\n" + - "\x16ListTeamMembersRequest\"H\n" + - "\x17ListTeamMembersResponse\x12-\n" + - "\amembers\x18\x01 \x03(\v2\x13.account.TeamMemberR\amembers\"U\n" + - "\n" + - "TeamMember\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + - "\vpicture_url\x18\x03 \x01(\tR\n" + - "pictureUrl\"$\n" + - "\"GetWelcomeScreenInformationRequest\"_\n" + - "\x12InviterInformation\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + - "\vpicture_url\x18\x03 \x01(\tR\n" + - "pictureUrl\"s\n" + - "#GetWelcomeScreenInformationResponse\x12L\n" + - "\x13inviter_information\x18\x01 \x01(\v2\x1b.account.InviterInformationR\x12inviterInformation*\x96\x01\n" + - "\fSourceStatus\x12\x16\n" + - "\x12STATUS_UNSPECIFIED\x10\x00\x12\x16\n" + - "\x12STATUS_PROGRESSING\x10\x01\x12\x12\n" + - "\x0eSTATUS_HEALTHY\x10\x02\x12\x14\n" + - "\x10STATUS_UNHEALTHY\x10\x03\x12\x13\n" + - "\x0fSTATUS_SLEEPING\x10\x04\x12\x17\n" + - "\x13STATUS_DISCONNECTED\x10\x05*s\n" + - "\x10RepositoryStatus\x12!\n" + - "\x1dREPOSITORY_STATUS_UNSPECIFIED\x10\x00\x12\x1c\n" + - "\x18REPOSITORY_STATUS_ACTIVE\x10\x01\x12\x1e\n" + - "\x1aREPOSITORY_STATUS_INACTIVE\x10\x02*_\n" + - "\vAccountPlan\x12\x1c\n" + - "\x18ACCOUNT_PLAN_UNSPECIFIED\x10\x00\x12\x15\n" + - "\x11ACCOUNT_PLAN_FREE\x10\x01\x12\x1b\n" + - "\x17ACCOUNT_PLAN_ENTERPRISE\x10\x02*'\n" + - "\rSourceManaged\x12\t\n" + - "\x05LOCAL\x10\x00\x12\v\n" + - "\aMANAGED\x10\x01*\xb2\x02\n" + - "\x0fAdapterCategory\x12\x1a\n" + - "\x16ADAPTER_CATEGORY_OTHER\x10\x00\x12(\n" + - "$ADAPTER_CATEGORY_COMPUTE_APPLICATION\x10\x01\x12\x1c\n" + - "\x18ADAPTER_CATEGORY_STORAGE\x10\x02\x12\x1c\n" + - "\x18ADAPTER_CATEGORY_NETWORK\x10\x03\x12\x1d\n" + - "\x19ADAPTER_CATEGORY_SECURITY\x10\x04\x12\"\n" + - "\x1eADAPTER_CATEGORY_OBSERVABILITY\x10\x05\x12\x1d\n" + - "\x19ADAPTER_CATEGORY_DATABASE\x10\x06\x12\"\n" + - "\x1eADAPTER_CATEGORY_CONFIGURATION\x10\a\x12\x17\n" + - "\x13ADAPTER_CATEGORY_AI\x10\b2\xe1\a\n" + - "\fAdminService\x12K\n" + - "\fListAccounts\x12\x1c.account.ListAccountsRequest\x1a\x1d.account.ListAccountsResponse\x12N\n" + - "\rCreateAccount\x12\x1d.account.CreateAccountRequest\x1a\x1e.account.CreateAccountResponse\x12S\n" + - "\rUpdateAccount\x12\".account.AdminUpdateAccountRequest\x1a\x1e.account.UpdateAccountResponse\x12J\n" + - "\n" + - "GetAccount\x12\x1f.account.AdminGetAccountRequest\x1a\x1b.account.GetAccountResponse\x12X\n" + - "\rDeleteAccount\x12\".account.AdminDeleteAccountRequest\x1a#.account.AdminDeleteAccountResponse\x12M\n" + - "\vListSources\x12 .account.AdminListSourcesRequest\x1a\x1c.account.ListSourcesResponse\x12P\n" + - "\fCreateSource\x12!.account.AdminCreateSourceRequest\x1a\x1d.account.CreateSourceResponse\x12G\n" + - "\tGetSource\x12\x1e.account.AdminGetSourceRequest\x1a\x1a.account.GetSourceResponse\x12P\n" + - "\fUpdateSource\x12!.account.AdminUpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12P\n" + - "\fDeleteSource\x12!.account.AdminDeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12\\\n" + - "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12M\n" + - "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x98\x0f\n" + - "\x11ManagementService\x12E\n" + - "\n" + - "GetAccount\x12\x1a.account.GetAccountRequest\x1a\x1b.account.GetAccountResponse\x12N\n" + - "\rDeleteAccount\x12\x1d.account.DeleteAccountRequest\x1a\x1e.account.DeleteAccountResponse\x12H\n" + - "\vListSources\x12\x1b.account.ListSourcesRequest\x1a\x1c.account.ListSourcesResponse\x12K\n" + - "\fCreateSource\x12\x1c.account.CreateSourceRequest\x1a\x1d.account.CreateSourceResponse\x12B\n" + - "\tGetSource\x12\x19.account.GetSourceRequest\x1a\x1a.account.GetSourceResponse\x12K\n" + - "\fUpdateSource\x12\x1c.account.UpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12K\n" + - "\fDeleteSource\x12\x1c.account.DeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12c\n" + - "\x14ListAllSourcesStatus\x12$.account.ListAllSourcesStatusRequest\x1a%.account.ListAllSourcesStatusResponse\x12f\n" + - "\x17ListActiveSourcesStatus\x12$.account.ListAllSourcesStatusRequest\x1a%.account.ListAllSourcesStatusResponse\x12f\n" + - "\x15SubmitSourceHeartbeat\x12%.account.SubmitSourceHeartbeatRequest\x1a&.account.SubmitSourceHeartbeatResponse\x12W\n" + - "\x10KeepaliveSources\x12 .account.KeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12H\n" + - "\vCreateToken\x12\x1b.account.CreateTokenRequest\x1a\x1c.account.CreateTokenResponse\x12P\n" + - "\rRevlinkWarmup\x12\x1d.account.RevlinkWarmupRequest\x1a\x1e.account.RevlinkWarmupResponse0\x01\x12i\n" + - "\x16ListAvailableItemTypes\x12&.account.ListAvailableItemTypesRequest\x1a'.account.ListAvailableItemTypesResponse\x12T\n" + - "\x0fGetSourceStatus\x12\x1f.account.GetSourceStatusRequest\x1a .account.GetSourceStatusResponse\x12l\n" + - "\x17GetUserOnboardingStatus\x12'.account.GetUserOnboardingStatusRequest\x1a(.account.GetUserOnboardingStatusResponse\x12l\n" + - "\x17SetUserOnboardingStatus\x12'.account.SetUserOnboardingStatusRequest\x1a(.account.SetUserOnboardingStatusResponse\x12T\n" + - "\x0fListTeamMembers\x12\x1f.account.ListTeamMembersRequest\x1a .account.ListTeamMembersResponse\x12x\n" + - "\x1bGetWelcomeScreenInformation\x12+.account.GetWelcomeScreenInformationRequest\x1a,.account.GetWelcomeScreenInformationResponse\x12l\n" + - "\x17SetGithubInstallationID\x12'.account.SetGithubInstallationIDRequest\x1a(.account.SetGithubInstallationIDResponse\x12r\n" + - "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_account_proto_rawDescOnce sync.Once - file_account_proto_rawDescData []byte -) - -func file_account_proto_rawDescGZIP() []byte { - file_account_proto_rawDescOnce.Do(func() { - file_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc))) - }) - return file_account_proto_rawDescData -} - -var file_account_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 72) -var file_account_proto_goTypes = []any{ - (SourceStatus)(0), // 0: account.SourceStatus - (RepositoryStatus)(0), // 1: account.RepositoryStatus - (AccountPlan)(0), // 2: account.AccountPlan - (SourceManaged)(0), // 3: account.SourceManaged - (AdapterCategory)(0), // 4: account.AdapterCategory - (*ListAccountsRequest)(nil), // 5: account.ListAccountsRequest - (*ListAccountsResponse)(nil), // 6: account.ListAccountsResponse - (*CreateAccountRequest)(nil), // 7: account.CreateAccountRequest - (*CreateAccountResponse)(nil), // 8: account.CreateAccountResponse - (*UpdateAccountRequest)(nil), // 9: account.UpdateAccountRequest - (*UpdateAccountResponse)(nil), // 10: account.UpdateAccountResponse - (*AdminUpdateAccountRequest)(nil), // 11: account.AdminUpdateAccountRequest - (*AdminGetAccountRequest)(nil), // 12: account.AdminGetAccountRequest - (*AdminDeleteAccountRequest)(nil), // 13: account.AdminDeleteAccountRequest - (*AdminDeleteAccountResponse)(nil), // 14: account.AdminDeleteAccountResponse - (*AdminListSourcesRequest)(nil), // 15: account.AdminListSourcesRequest - (*AdminCreateSourceRequest)(nil), // 16: account.AdminCreateSourceRequest - (*AdminGetSourceRequest)(nil), // 17: account.AdminGetSourceRequest - (*AdminUpdateSourceRequest)(nil), // 18: account.AdminUpdateSourceRequest - (*AdminDeleteSourceRequest)(nil), // 19: account.AdminDeleteSourceRequest - (*AdminKeepaliveSourcesRequest)(nil), // 20: account.AdminKeepaliveSourcesRequest - (*AdminCreateTokenRequest)(nil), // 21: account.AdminCreateTokenRequest - (*Source)(nil), // 22: account.Source - (*SourceMetadata)(nil), // 23: account.SourceMetadata - (*SourceProperties)(nil), // 24: account.SourceProperties - (*Account)(nil), // 25: account.Account - (*AccountMetadata)(nil), // 26: account.AccountMetadata - (*Repository)(nil), // 27: account.Repository - (*AccountProperties)(nil), // 28: account.AccountProperties - (*GetAccountRequest)(nil), // 29: account.GetAccountRequest - (*GetAccountResponse)(nil), // 30: account.GetAccountResponse - (*DeleteAccountRequest)(nil), // 31: account.DeleteAccountRequest - (*DeleteAccountResponse)(nil), // 32: account.DeleteAccountResponse - (*ListSourcesRequest)(nil), // 33: account.ListSourcesRequest - (*ListSourcesResponse)(nil), // 34: account.ListSourcesResponse - (*CreateSourceRequest)(nil), // 35: account.CreateSourceRequest - (*CreateSourceResponse)(nil), // 36: account.CreateSourceResponse - (*GetSourceRequest)(nil), // 37: account.GetSourceRequest - (*GetSourceResponse)(nil), // 38: account.GetSourceResponse - (*UpdateSourceRequest)(nil), // 39: account.UpdateSourceRequest - (*UpdateSourceResponse)(nil), // 40: account.UpdateSourceResponse - (*DeleteSourceRequest)(nil), // 41: account.DeleteSourceRequest - (*DeleteSourceResponse)(nil), // 42: account.DeleteSourceResponse - (*SourceKeepaliveResult)(nil), // 43: account.SourceKeepaliveResult - (*ListAllSourcesStatusRequest)(nil), // 44: account.ListAllSourcesStatusRequest - (*SourceHealth)(nil), // 45: account.SourceHealth - (*ListAllSourcesStatusResponse)(nil), // 46: account.ListAllSourcesStatusResponse - (*SubmitSourceHeartbeatRequest)(nil), // 47: account.SubmitSourceHeartbeatRequest - (*AdapterMetadata)(nil), // 48: account.AdapterMetadata - (*AdapterSupportedQueryMethods)(nil), // 49: account.AdapterSupportedQueryMethods - (*TerraformMapping)(nil), // 50: account.TerraformMapping - (*SubmitSourceHeartbeatResponse)(nil), // 51: account.SubmitSourceHeartbeatResponse - (*KeepaliveSourcesRequest)(nil), // 52: account.KeepaliveSourcesRequest - (*KeepaliveSourcesResponse)(nil), // 53: account.KeepaliveSourcesResponse - (*CreateTokenRequest)(nil), // 54: account.CreateTokenRequest - (*CreateTokenResponse)(nil), // 55: account.CreateTokenResponse - (*RevlinkWarmupRequest)(nil), // 56: account.RevlinkWarmupRequest - (*RevlinkWarmupResponse)(nil), // 57: account.RevlinkWarmupResponse - (*AvailableItemType)(nil), // 58: account.AvailableItemType - (*ListAvailableItemTypesRequest)(nil), // 59: account.ListAvailableItemTypesRequest - (*ListAvailableItemTypesResponse)(nil), // 60: account.ListAvailableItemTypesResponse - (*GetSourceStatusRequest)(nil), // 61: account.GetSourceStatusRequest - (*GetSourceStatusResponse)(nil), // 62: account.GetSourceStatusResponse - (*GetUserOnboardingStatusRequest)(nil), // 63: account.GetUserOnboardingStatusRequest - (*GetUserOnboardingStatusResponse)(nil), // 64: account.GetUserOnboardingStatusResponse - (*SetUserOnboardingStatusRequest)(nil), // 65: account.SetUserOnboardingStatusRequest - (*SetUserOnboardingStatusResponse)(nil), // 66: account.SetUserOnboardingStatusResponse - (*SetGithubInstallationIDRequest)(nil), // 67: account.SetGithubInstallationIDRequest - (*SetGithubInstallationIDResponse)(nil), // 68: account.SetGithubInstallationIDResponse - (*UnsetGithubInstallationIDRequest)(nil), // 69: account.UnsetGithubInstallationIDRequest - (*UnsetGithubInstallationIDResponse)(nil), // 70: account.UnsetGithubInstallationIDResponse - (*ListTeamMembersRequest)(nil), // 71: account.ListTeamMembersRequest - (*ListTeamMembersResponse)(nil), // 72: account.ListTeamMembersResponse - (*TeamMember)(nil), // 73: account.TeamMember - (*GetWelcomeScreenInformationRequest)(nil), // 74: account.GetWelcomeScreenInformationRequest - (*InviterInformation)(nil), // 75: account.InviterInformation - (*GetWelcomeScreenInformationResponse)(nil), // 76: account.GetWelcomeScreenInformationResponse - (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 78: google.protobuf.Struct - (*durationpb.Duration)(nil), // 79: google.protobuf.Duration - (QueryMethod)(0), // 80: QueryMethod -} -var file_account_proto_depIdxs = []int32{ - 25, // 0: account.ListAccountsResponse.accounts:type_name -> account.Account - 28, // 1: account.CreateAccountRequest.properties:type_name -> account.AccountProperties - 25, // 2: account.CreateAccountResponse.account:type_name -> account.Account - 28, // 3: account.UpdateAccountRequest.properties:type_name -> account.AccountProperties - 25, // 4: account.UpdateAccountResponse.account:type_name -> account.Account - 9, // 5: account.AdminUpdateAccountRequest.request:type_name -> account.UpdateAccountRequest - 33, // 6: account.AdminListSourcesRequest.request:type_name -> account.ListSourcesRequest - 35, // 7: account.AdminCreateSourceRequest.request:type_name -> account.CreateSourceRequest - 37, // 8: account.AdminGetSourceRequest.request:type_name -> account.GetSourceRequest - 39, // 9: account.AdminUpdateSourceRequest.request:type_name -> account.UpdateSourceRequest - 41, // 10: account.AdminDeleteSourceRequest.request:type_name -> account.DeleteSourceRequest - 52, // 11: account.AdminKeepaliveSourcesRequest.request:type_name -> account.KeepaliveSourcesRequest - 54, // 12: account.AdminCreateTokenRequest.request:type_name -> account.CreateTokenRequest - 23, // 13: account.Source.metadata:type_name -> account.SourceMetadata - 24, // 14: account.Source.properties:type_name -> account.SourceProperties - 77, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp - 0, // 16: account.SourceMetadata.Status:type_name -> account.SourceStatus - 78, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct - 78, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct - 26, // 19: account.Account.metadata:type_name -> account.AccountMetadata - 28, // 20: account.Account.properties:type_name -> account.AccountProperties - 27, // 21: account.AccountMetadata.repositories:type_name -> account.Repository - 2, // 22: account.AccountMetadata.Plan:type_name -> account.AccountPlan - 77, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp - 1, // 24: account.Repository.status:type_name -> account.RepositoryStatus - 25, // 25: account.GetAccountResponse.account:type_name -> account.Account - 22, // 26: account.ListSourcesResponse.Sources:type_name -> account.Source - 24, // 27: account.CreateSourceRequest.properties:type_name -> account.SourceProperties - 22, // 28: account.CreateSourceResponse.source:type_name -> account.Source - 22, // 29: account.GetSourceResponse.source:type_name -> account.Source - 24, // 30: account.UpdateSourceRequest.properties:type_name -> account.SourceProperties - 22, // 31: account.UpdateSourceResponse.source:type_name -> account.Source - 0, // 32: account.SourceKeepaliveResult.Status:type_name -> account.SourceStatus - 0, // 33: account.SourceHealth.status:type_name -> account.SourceStatus - 77, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp - 77, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp - 77, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp - 3, // 37: account.SourceHealth.managed:type_name -> account.SourceManaged - 48, // 38: account.SourceHealth.adapterMetadata:type_name -> account.AdapterMetadata - 45, // 39: account.ListAllSourcesStatusResponse.sources:type_name -> account.SourceHealth - 79, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration - 3, // 41: account.SubmitSourceHeartbeatRequest.managed:type_name -> account.SourceManaged - 48, // 42: account.SubmitSourceHeartbeatRequest.adapterMetadata:type_name -> account.AdapterMetadata - 4, // 43: account.AdapterMetadata.category:type_name -> account.AdapterCategory - 49, // 44: account.AdapterMetadata.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods - 50, // 45: account.AdapterMetadata.terraformMappings:type_name -> account.TerraformMapping - 80, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod - 79, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration - 43, // 48: account.KeepaliveSourcesResponse.sources:type_name -> account.SourceKeepaliveResult - 4, // 49: account.AvailableItemType.category:type_name -> account.AdapterCategory - 49, // 50: account.AvailableItemType.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods - 58, // 51: account.ListAvailableItemTypesResponse.types:type_name -> account.AvailableItemType - 45, // 52: account.GetSourceStatusResponse.source:type_name -> account.SourceHealth - 73, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember - 75, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation - 5, // 55: account.AdminService.ListAccounts:input_type -> account.ListAccountsRequest - 7, // 56: account.AdminService.CreateAccount:input_type -> account.CreateAccountRequest - 11, // 57: account.AdminService.UpdateAccount:input_type -> account.AdminUpdateAccountRequest - 12, // 58: account.AdminService.GetAccount:input_type -> account.AdminGetAccountRequest - 13, // 59: account.AdminService.DeleteAccount:input_type -> account.AdminDeleteAccountRequest - 15, // 60: account.AdminService.ListSources:input_type -> account.AdminListSourcesRequest - 16, // 61: account.AdminService.CreateSource:input_type -> account.AdminCreateSourceRequest - 17, // 62: account.AdminService.GetSource:input_type -> account.AdminGetSourceRequest - 18, // 63: account.AdminService.UpdateSource:input_type -> account.AdminUpdateSourceRequest - 19, // 64: account.AdminService.DeleteSource:input_type -> account.AdminDeleteSourceRequest - 20, // 65: account.AdminService.KeepaliveSources:input_type -> account.AdminKeepaliveSourcesRequest - 21, // 66: account.AdminService.CreateToken:input_type -> account.AdminCreateTokenRequest - 29, // 67: account.ManagementService.GetAccount:input_type -> account.GetAccountRequest - 31, // 68: account.ManagementService.DeleteAccount:input_type -> account.DeleteAccountRequest - 33, // 69: account.ManagementService.ListSources:input_type -> account.ListSourcesRequest - 35, // 70: account.ManagementService.CreateSource:input_type -> account.CreateSourceRequest - 37, // 71: account.ManagementService.GetSource:input_type -> account.GetSourceRequest - 39, // 72: account.ManagementService.UpdateSource:input_type -> account.UpdateSourceRequest - 41, // 73: account.ManagementService.DeleteSource:input_type -> account.DeleteSourceRequest - 44, // 74: account.ManagementService.ListAllSourcesStatus:input_type -> account.ListAllSourcesStatusRequest - 44, // 75: account.ManagementService.ListActiveSourcesStatus:input_type -> account.ListAllSourcesStatusRequest - 47, // 76: account.ManagementService.SubmitSourceHeartbeat:input_type -> account.SubmitSourceHeartbeatRequest - 52, // 77: account.ManagementService.KeepaliveSources:input_type -> account.KeepaliveSourcesRequest - 54, // 78: account.ManagementService.CreateToken:input_type -> account.CreateTokenRequest - 56, // 79: account.ManagementService.RevlinkWarmup:input_type -> account.RevlinkWarmupRequest - 59, // 80: account.ManagementService.ListAvailableItemTypes:input_type -> account.ListAvailableItemTypesRequest - 61, // 81: account.ManagementService.GetSourceStatus:input_type -> account.GetSourceStatusRequest - 63, // 82: account.ManagementService.GetUserOnboardingStatus:input_type -> account.GetUserOnboardingStatusRequest - 65, // 83: account.ManagementService.SetUserOnboardingStatus:input_type -> account.SetUserOnboardingStatusRequest - 71, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest - 74, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest - 67, // 86: account.ManagementService.SetGithubInstallationID:input_type -> account.SetGithubInstallationIDRequest - 69, // 87: account.ManagementService.UnsetGithubInstallationID:input_type -> account.UnsetGithubInstallationIDRequest - 6, // 88: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse - 8, // 89: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse - 10, // 90: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse - 30, // 91: account.AdminService.GetAccount:output_type -> account.GetAccountResponse - 14, // 92: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse - 34, // 93: account.AdminService.ListSources:output_type -> account.ListSourcesResponse - 36, // 94: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 95: account.AdminService.GetSource:output_type -> account.GetSourceResponse - 40, // 96: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 97: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse - 53, // 98: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 99: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse - 30, // 100: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse - 32, // 101: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse - 34, // 102: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse - 36, // 103: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 104: account.ManagementService.GetSource:output_type -> account.GetSourceResponse - 40, // 105: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 106: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse - 46, // 107: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 46, // 108: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 51, // 109: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse - 53, // 110: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 111: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse - 57, // 112: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse - 60, // 113: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse - 62, // 114: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse - 64, // 115: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse - 66, // 116: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse - 72, // 117: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse - 76, // 118: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse - 68, // 119: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse - 70, // 120: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse - 88, // [88:121] is the sub-list for method output_type - 55, // [55:88] is the sub-list for method input_type - 55, // [55:55] is the sub-list for extension type_name - 55, // [55:55] is the sub-list for extension extendee - 0, // [0:55] is the sub-list for field type_name -} - -func init() { file_account_proto_init() } -func file_account_proto_init() { - if File_account_proto != nil { - return - } - file_items_proto_init() - file_account_proto_msgTypes[40].OneofWrappers = []any{} - file_account_proto_msgTypes[42].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc)), - NumEnums: 5, - NumMessages: 72, - NumExtensions: 0, - NumServices: 2, - }, - GoTypes: file_account_proto_goTypes, - DependencyIndexes: file_account_proto_depIdxs, - EnumInfos: file_account_proto_enumTypes, - MessageInfos: file_account_proto_msgTypes, - }.Build() - File_account_proto = out.File - file_account_proto_goTypes = nil - file_account_proto_depIdxs = nil -} diff --git a/sdp-go/apikey.go b/sdp-go/apikey.go deleted file mode 100644 index f445a9ea..00000000 --- a/sdp-go/apikey.go +++ /dev/null @@ -1,11 +0,0 @@ -package sdp - -import "github.com/google/uuid" - -func (a *APIKeyMetadata) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUuid()) - if err != nil { - return nil - } - return &u -} diff --git a/sdp-go/apikeys.pb.go b/sdp-go/apikeys.pb.go deleted file mode 100644 index d303f4b3..00000000 --- a/sdp-go/apikeys.pb.go +++ /dev/null @@ -1,1111 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: apikeys.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type KeyStatus int32 - -const ( - KeyStatus_KEY_STATUS_UNKNOWN KeyStatus = 0 - // This means the key has been created but we have not yet received the - // callback from Auth0 which allows us to fetch the access token - KeyStatus_KEY_STATUS_UNAUTHORIZED KeyStatus = 1 - // Key is ready for use - KeyStatus_KEY_STATUS_READY KeyStatus = 2 - // There was an error getting the access token from Auth0 - KeyStatus_KEY_STATUS_ERROR KeyStatus = 3 - // The API key has been revoked - KeyStatus_KEY_STATUS_REVOKED KeyStatus = 4 -) - -// Enum value maps for KeyStatus. -var ( - KeyStatus_name = map[int32]string{ - 0: "KEY_STATUS_UNKNOWN", - 1: "KEY_STATUS_UNAUTHORIZED", - 2: "KEY_STATUS_READY", - 3: "KEY_STATUS_ERROR", - 4: "KEY_STATUS_REVOKED", - } - KeyStatus_value = map[string]int32{ - "KEY_STATUS_UNKNOWN": 0, - "KEY_STATUS_UNAUTHORIZED": 1, - "KEY_STATUS_READY": 2, - "KEY_STATUS_ERROR": 3, - "KEY_STATUS_REVOKED": 4, - } -) - -func (x KeyStatus) Enum() *KeyStatus { - p := new(KeyStatus) - *p = x - return p -} - -func (x KeyStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (KeyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_apikeys_proto_enumTypes[0].Descriptor() -} - -func (KeyStatus) Type() protoreflect.EnumType { - return &file_apikeys_proto_enumTypes[0] -} - -func (x KeyStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use KeyStatus.Descriptor instead. -func (KeyStatus) EnumDescriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{0} -} - -type APIKey struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *APIKeyMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *APIKeyProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *APIKey) Reset() { - *x = APIKey{} - mi := &file_apikeys_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *APIKey) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*APIKey) ProtoMessage() {} - -func (x *APIKey) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[0] - 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 APIKey.ProtoReflect.Descriptor instead. -func (*APIKey) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{0} -} - -func (x *APIKey) GetMetadata() *APIKeyMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *APIKey) GetProperties() *APIKeyProperties { - if x != nil { - return x.Properties - } - return nil -} - -type APIKeyMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The ID of this API key - Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` - // When the API Key was created - Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` - // The last time the API key was exchanged for an access token - LastUsed *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastUsed,proto3" json:"lastUsed,omitempty"` - // The actual API key - Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` - // The list of scopes that this token has access to - Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` - // The status of the key - Status KeyStatus `protobuf:"varint,6,opt,name=status,proto3,enum=apikeys.KeyStatus" json:"status,omitempty"` - // The error encountered when authorizing the key. This will only be set if - // the status is ERROR - Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *APIKeyMetadata) Reset() { - *x = APIKeyMetadata{} - mi := &file_apikeys_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *APIKeyMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*APIKeyMetadata) ProtoMessage() {} - -func (x *APIKeyMetadata) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[1] - 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 APIKeyMetadata.ProtoReflect.Descriptor instead. -func (*APIKeyMetadata) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{1} -} - -func (x *APIKeyMetadata) GetUuid() []byte { - if x != nil { - return x.Uuid - } - return nil -} - -func (x *APIKeyMetadata) GetCreated() *timestamppb.Timestamp { - if x != nil { - return x.Created - } - return nil -} - -func (x *APIKeyMetadata) GetLastUsed() *timestamppb.Timestamp { - if x != nil { - return x.LastUsed - } - return nil -} - -func (x *APIKeyMetadata) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *APIKeyMetadata) GetScopes() []string { - if x != nil { - return x.Scopes - } - return nil -} - -func (x *APIKeyMetadata) GetStatus() KeyStatus { - if x != nil { - return x.Status - } - return KeyStatus_KEY_STATUS_UNKNOWN -} - -func (x *APIKeyMetadata) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type APIKeyProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the API key - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *APIKeyProperties) Reset() { - *x = APIKeyProperties{} - mi := &file_apikeys_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *APIKeyProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*APIKeyProperties) ProtoMessage() {} - -func (x *APIKeyProperties) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[2] - 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 APIKeyProperties.ProtoReflect.Descriptor instead. -func (*APIKeyProperties) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{2} -} - -func (x *APIKeyProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type CreateAPIKeyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the key to create - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // The scopes that the key should have - Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` - // The URL that the user should be redirected to after the whole process is - // over. This should be a page in the frontend, probably the one they - // started from, but could also be a detail page for this particular API key - FinalFrontendRedirect string `protobuf:"bytes,3,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateAPIKeyRequest) Reset() { - *x = CreateAPIKeyRequest{} - mi := &file_apikeys_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateAPIKeyRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateAPIKeyRequest) ProtoMessage() {} - -func (x *CreateAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[3] - 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 CreateAPIKeyRequest.ProtoReflect.Descriptor instead. -func (*CreateAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{3} -} - -func (x *CreateAPIKeyRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *CreateAPIKeyRequest) GetScopes() []string { - if x != nil { - return x.Scopes - } - return nil -} - -func (x *CreateAPIKeyRequest) GetFinalFrontendRedirect() string { - if x != nil { - return x.FinalFrontendRedirect - } - return "" -} - -type CreateAPIKeyResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Details of the newly created API Key - Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - // The URL that the user should visit in order to authorize the newly - // created key. This will allow Auth0 to generate a code that will be passed - // to the API server via a callback. This code is then exchanged by the API - // server for an access token and refresh token. The user will be redirected - // back to the frontend once this process is complete. - // - // The authorizeURL will contain a `state` paremeter which is a UUID that - // can be used to look up the API key in the database once the callback is - // received - AuthorizeURL string `protobuf:"bytes,2,opt,name=authorizeURL,proto3" json:"authorizeURL,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateAPIKeyResponse) Reset() { - *x = CreateAPIKeyResponse{} - mi := &file_apikeys_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateAPIKeyResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateAPIKeyResponse) ProtoMessage() {} - -func (x *CreateAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[4] - 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 CreateAPIKeyResponse.ProtoReflect.Descriptor instead. -func (*CreateAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{4} -} - -func (x *CreateAPIKeyResponse) GetKey() *APIKey { - if x != nil { - return x.Key - } - return nil -} - -func (x *CreateAPIKeyResponse) GetAuthorizeURL() string { - if x != nil { - return x.AuthorizeURL - } - return "" -} - -type RefreshAPIKeyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the API key to refresh - Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` - // The URL that the user should be redirected to after the whole process is - // over. This should be a page in the frontend, probably the one they - // started from, but could also be a detail page for this particular API key - FinalFrontendRedirect string `protobuf:"bytes,2,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RefreshAPIKeyRequest) Reset() { - *x = RefreshAPIKeyRequest{} - mi := &file_apikeys_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RefreshAPIKeyRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RefreshAPIKeyRequest) ProtoMessage() {} - -func (x *RefreshAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[5] - 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 RefreshAPIKeyRequest.ProtoReflect.Descriptor instead. -func (*RefreshAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{5} -} - -func (x *RefreshAPIKeyRequest) GetUuid() []byte { - if x != nil { - return x.Uuid - } - return nil -} - -func (x *RefreshAPIKeyRequest) GetFinalFrontendRedirect() string { - if x != nil { - return x.FinalFrontendRedirect - } - return "" -} - -type RefreshAPIKeyResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Refreshing the API key will return the same response as CreateAPIKey, as - // it is basically the a new Key, just under the same UUID and reusing the - // old info. - Response *CreateAPIKeyResponse `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RefreshAPIKeyResponse) Reset() { - *x = RefreshAPIKeyResponse{} - mi := &file_apikeys_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RefreshAPIKeyResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RefreshAPIKeyResponse) ProtoMessage() {} - -func (x *RefreshAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[6] - 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 RefreshAPIKeyResponse.ProtoReflect.Descriptor instead. -func (*RefreshAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{6} -} - -func (x *RefreshAPIKeyResponse) GetResponse() *CreateAPIKeyResponse { - if x != nil { - return x.Response - } - return nil -} - -type GetAPIKeyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the API Key to get - Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAPIKeyRequest) Reset() { - *x = GetAPIKeyRequest{} - mi := &file_apikeys_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAPIKeyRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAPIKeyRequest) ProtoMessage() {} - -func (x *GetAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[7] - 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 GetAPIKeyRequest.ProtoReflect.Descriptor instead. -func (*GetAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{7} -} - -func (x *GetAPIKeyRequest) GetUuid() []byte { - if x != nil { - return x.Uuid - } - return nil -} - -type GetAPIKeyResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAPIKeyResponse) Reset() { - *x = GetAPIKeyResponse{} - mi := &file_apikeys_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAPIKeyResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAPIKeyResponse) ProtoMessage() {} - -func (x *GetAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[8] - 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 GetAPIKeyResponse.ProtoReflect.Descriptor instead. -func (*GetAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{8} -} - -func (x *GetAPIKeyResponse) GetKey() *APIKey { - if x != nil { - return x.Key - } - return nil -} - -type UpdateAPIKeyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the API key to update - Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` - // New properties to update - Properties *APIKeyProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAPIKeyRequest) Reset() { - *x = UpdateAPIKeyRequest{} - mi := &file_apikeys_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAPIKeyRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAPIKeyRequest) ProtoMessage() {} - -func (x *UpdateAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[9] - 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 UpdateAPIKeyRequest.ProtoReflect.Descriptor instead. -func (*UpdateAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{9} -} - -func (x *UpdateAPIKeyRequest) GetUuid() []byte { - if x != nil { - return x.Uuid - } - return nil -} - -func (x *UpdateAPIKeyRequest) GetProperties() *APIKeyProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateAPIKeyResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The updated API key - Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAPIKeyResponse) Reset() { - *x = UpdateAPIKeyResponse{} - mi := &file_apikeys_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAPIKeyResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAPIKeyResponse) ProtoMessage() {} - -func (x *UpdateAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[10] - 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 UpdateAPIKeyResponse.ProtoReflect.Descriptor instead. -func (*UpdateAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{10} -} - -func (x *UpdateAPIKeyResponse) GetKey() *APIKey { - if x != nil { - return x.Key - } - return nil -} - -type ListAPIKeysRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAPIKeysRequest) Reset() { - *x = ListAPIKeysRequest{} - mi := &file_apikeys_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAPIKeysRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAPIKeysRequest) ProtoMessage() {} - -func (x *ListAPIKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[11] - 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 ListAPIKeysRequest.ProtoReflect.Descriptor instead. -func (*ListAPIKeysRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{11} -} - -type ListAPIKeysResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Keys []*APIKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListAPIKeysResponse) Reset() { - *x = ListAPIKeysResponse{} - mi := &file_apikeys_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListAPIKeysResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListAPIKeysResponse) ProtoMessage() {} - -func (x *ListAPIKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[12] - 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 ListAPIKeysResponse.ProtoReflect.Descriptor instead. -func (*ListAPIKeysResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{12} -} - -func (x *ListAPIKeysResponse) GetKeys() []*APIKey { - if x != nil { - return x.Keys - } - return nil -} - -type DeleteAPIKeyRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the API key to delete - Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteAPIKeyRequest) Reset() { - *x = DeleteAPIKeyRequest{} - mi := &file_apikeys_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteAPIKeyRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteAPIKeyRequest) ProtoMessage() {} - -func (x *DeleteAPIKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteAPIKeyRequest.ProtoReflect.Descriptor instead. -func (*DeleteAPIKeyRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{13} -} - -func (x *DeleteAPIKeyRequest) GetUuid() []byte { - if x != nil { - return x.Uuid - } - return nil -} - -type DeleteAPIKeyResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteAPIKeyResponse) Reset() { - *x = DeleteAPIKeyResponse{} - mi := &file_apikeys_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteAPIKeyResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteAPIKeyResponse) ProtoMessage() {} - -func (x *DeleteAPIKeyResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteAPIKeyResponse.ProtoReflect.Descriptor instead. -func (*DeleteAPIKeyResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{14} -} - -type ExchangeKeyForTokenRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The API Key that you want to exchange for an Oauth access token - ApiKey string `protobuf:"bytes,1,opt,name=apiKey,proto3" json:"apiKey,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExchangeKeyForTokenRequest) Reset() { - *x = ExchangeKeyForTokenRequest{} - mi := &file_apikeys_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExchangeKeyForTokenRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExchangeKeyForTokenRequest) ProtoMessage() {} - -func (x *ExchangeKeyForTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[15] - 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 ExchangeKeyForTokenRequest.ProtoReflect.Descriptor instead. -func (*ExchangeKeyForTokenRequest) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{15} -} - -func (x *ExchangeKeyForTokenRequest) GetApiKey() string { - if x != nil { - return x.ApiKey - } - return "" -} - -type ExchangeKeyForTokenResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The access token that can now be use to authenticate to Overmind and its - // APIs - AccessToken string `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ExchangeKeyForTokenResponse) Reset() { - *x = ExchangeKeyForTokenResponse{} - mi := &file_apikeys_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExchangeKeyForTokenResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExchangeKeyForTokenResponse) ProtoMessage() {} - -func (x *ExchangeKeyForTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_apikeys_proto_msgTypes[16] - 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 ExchangeKeyForTokenResponse.ProtoReflect.Descriptor instead. -func (*ExchangeKeyForTokenResponse) Descriptor() ([]byte, []int) { - return file_apikeys_proto_rawDescGZIP(), []int{16} -} - -func (x *ExchangeKeyForTokenResponse) GetAccessToken() string { - if x != nil { - return x.AccessToken - } - return "" -} - -var File_apikeys_proto protoreflect.FileDescriptor - -const file_apikeys_proto_rawDesc = "" + - "\n" + - "\rapikeys.proto\x12\aapikeys\x1a\x1fgoogle/protobuf/timestamp.proto\"x\n" + - "\x06APIKey\x123\n" + - "\bmetadata\x18\x01 \x01(\v2\x17.apikeys.APIKeyMetadataR\bmetadata\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.apikeys.APIKeyPropertiesR\n" + - "properties\"\xfe\x01\n" + - "\x0eAPIKeyMetadata\x12\x12\n" + - "\x04uuid\x18\x01 \x01(\fR\x04uuid\x124\n" + - "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x126\n" + - "\blastUsed\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\blastUsed\x12\x10\n" + - "\x03key\x18\x04 \x01(\tR\x03key\x12\x16\n" + - "\x06scopes\x18\x05 \x03(\tR\x06scopes\x12*\n" + - "\x06status\x18\x06 \x01(\x0e2\x12.apikeys.KeyStatusR\x06status\x12\x14\n" + - "\x05error\x18\a \x01(\tR\x05error\"&\n" + - "\x10APIKeyProperties\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"w\n" + - "\x13CreateAPIKeyRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + - "\x06scopes\x18\x02 \x03(\tR\x06scopes\x124\n" + - "\x15finalFrontendRedirect\x18\x03 \x01(\tR\x15finalFrontendRedirect\"]\n" + - "\x14CreateAPIKeyResponse\x12!\n" + - "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\x12\"\n" + - "\fauthorizeURL\x18\x02 \x01(\tR\fauthorizeURL\"`\n" + - "\x14RefreshAPIKeyRequest\x12\x12\n" + - "\x04uuid\x18\x01 \x01(\fR\x04uuid\x124\n" + - "\x15finalFrontendRedirect\x18\x02 \x01(\tR\x15finalFrontendRedirect\"R\n" + - "\x15RefreshAPIKeyResponse\x129\n" + - "\bresponse\x18\x01 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\bresponse\"&\n" + - "\x10GetAPIKeyRequest\x12\x12\n" + - "\x04uuid\x18\x01 \x01(\fR\x04uuid\"6\n" + - "\x11GetAPIKeyResponse\x12!\n" + - "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\"d\n" + - "\x13UpdateAPIKeyRequest\x12\x12\n" + - "\x04uuid\x18\x01 \x01(\fR\x04uuid\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.apikeys.APIKeyPropertiesR\n" + - "properties\"9\n" + - "\x14UpdateAPIKeyResponse\x12!\n" + - "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\"\x14\n" + - "\x12ListAPIKeysRequest\":\n" + - "\x13ListAPIKeysResponse\x12#\n" + - "\x04keys\x18\x01 \x03(\v2\x0f.apikeys.APIKeyR\x04keys\")\n" + - "\x13DeleteAPIKeyRequest\x12\x12\n" + - "\x04uuid\x18\x01 \x01(\fR\x04uuid\"\x16\n" + - "\x14DeleteAPIKeyResponse\"4\n" + - "\x1aExchangeKeyForTokenRequest\x12\x16\n" + - "\x06apiKey\x18\x01 \x01(\tR\x06apiKey\"?\n" + - "\x1bExchangeKeyForTokenResponse\x12 \n" + - "\vaccessToken\x18\x01 \x01(\tR\vaccessToken*\x84\x01\n" + - "\tKeyStatus\x12\x16\n" + - "\x12KEY_STATUS_UNKNOWN\x10\x00\x12\x1b\n" + - "\x17KEY_STATUS_UNAUTHORIZED\x10\x01\x12\x14\n" + - "\x10KEY_STATUS_READY\x10\x02\x12\x14\n" + - "\x10KEY_STATUS_ERROR\x10\x03\x12\x16\n" + - "\x12KEY_STATUS_REVOKED\x10\x042\xb6\x04\n" + - "\rApiKeyService\x12K\n" + - "\fCreateAPIKey\x12\x1c.apikeys.CreateAPIKeyRequest\x1a\x1d.apikeys.CreateAPIKeyResponse\x12N\n" + - "\rRefreshAPIKey\x12\x1d.apikeys.RefreshAPIKeyRequest\x1a\x1e.apikeys.RefreshAPIKeyResponse\x12B\n" + - "\tGetAPIKey\x12\x19.apikeys.GetAPIKeyRequest\x1a\x1a.apikeys.GetAPIKeyResponse\x12K\n" + - "\fUpdateAPIKey\x12\x1c.apikeys.UpdateAPIKeyRequest\x1a\x1d.apikeys.UpdateAPIKeyResponse\x12H\n" + - "\vListAPIKeys\x12\x1b.apikeys.ListAPIKeysRequest\x1a\x1c.apikeys.ListAPIKeysResponse\x12K\n" + - "\fDeleteAPIKey\x12\x1c.apikeys.DeleteAPIKeyRequest\x1a\x1d.apikeys.DeleteAPIKeyResponse\x12`\n" + - "\x13ExchangeKeyForToken\x12#.apikeys.ExchangeKeyForTokenRequest\x1a$.apikeys.ExchangeKeyForTokenResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_apikeys_proto_rawDescOnce sync.Once - file_apikeys_proto_rawDescData []byte -) - -func file_apikeys_proto_rawDescGZIP() []byte { - file_apikeys_proto_rawDescOnce.Do(func() { - file_apikeys_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_apikeys_proto_rawDesc), len(file_apikeys_proto_rawDesc))) - }) - return file_apikeys_proto_rawDescData -} - -var file_apikeys_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_apikeys_proto_msgTypes = make([]protoimpl.MessageInfo, 17) -var file_apikeys_proto_goTypes = []any{ - (KeyStatus)(0), // 0: apikeys.KeyStatus - (*APIKey)(nil), // 1: apikeys.APIKey - (*APIKeyMetadata)(nil), // 2: apikeys.APIKeyMetadata - (*APIKeyProperties)(nil), // 3: apikeys.APIKeyProperties - (*CreateAPIKeyRequest)(nil), // 4: apikeys.CreateAPIKeyRequest - (*CreateAPIKeyResponse)(nil), // 5: apikeys.CreateAPIKeyResponse - (*RefreshAPIKeyRequest)(nil), // 6: apikeys.RefreshAPIKeyRequest - (*RefreshAPIKeyResponse)(nil), // 7: apikeys.RefreshAPIKeyResponse - (*GetAPIKeyRequest)(nil), // 8: apikeys.GetAPIKeyRequest - (*GetAPIKeyResponse)(nil), // 9: apikeys.GetAPIKeyResponse - (*UpdateAPIKeyRequest)(nil), // 10: apikeys.UpdateAPIKeyRequest - (*UpdateAPIKeyResponse)(nil), // 11: apikeys.UpdateAPIKeyResponse - (*ListAPIKeysRequest)(nil), // 12: apikeys.ListAPIKeysRequest - (*ListAPIKeysResponse)(nil), // 13: apikeys.ListAPIKeysResponse - (*DeleteAPIKeyRequest)(nil), // 14: apikeys.DeleteAPIKeyRequest - (*DeleteAPIKeyResponse)(nil), // 15: apikeys.DeleteAPIKeyResponse - (*ExchangeKeyForTokenRequest)(nil), // 16: apikeys.ExchangeKeyForTokenRequest - (*ExchangeKeyForTokenResponse)(nil), // 17: apikeys.ExchangeKeyForTokenResponse - (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp -} -var file_apikeys_proto_depIdxs = []int32{ - 2, // 0: apikeys.APIKey.metadata:type_name -> apikeys.APIKeyMetadata - 3, // 1: apikeys.APIKey.properties:type_name -> apikeys.APIKeyProperties - 18, // 2: apikeys.APIKeyMetadata.created:type_name -> google.protobuf.Timestamp - 18, // 3: apikeys.APIKeyMetadata.lastUsed:type_name -> google.protobuf.Timestamp - 0, // 4: apikeys.APIKeyMetadata.status:type_name -> apikeys.KeyStatus - 1, // 5: apikeys.CreateAPIKeyResponse.key:type_name -> apikeys.APIKey - 5, // 6: apikeys.RefreshAPIKeyResponse.response:type_name -> apikeys.CreateAPIKeyResponse - 1, // 7: apikeys.GetAPIKeyResponse.key:type_name -> apikeys.APIKey - 3, // 8: apikeys.UpdateAPIKeyRequest.properties:type_name -> apikeys.APIKeyProperties - 1, // 9: apikeys.UpdateAPIKeyResponse.key:type_name -> apikeys.APIKey - 1, // 10: apikeys.ListAPIKeysResponse.keys:type_name -> apikeys.APIKey - 4, // 11: apikeys.ApiKeyService.CreateAPIKey:input_type -> apikeys.CreateAPIKeyRequest - 6, // 12: apikeys.ApiKeyService.RefreshAPIKey:input_type -> apikeys.RefreshAPIKeyRequest - 8, // 13: apikeys.ApiKeyService.GetAPIKey:input_type -> apikeys.GetAPIKeyRequest - 10, // 14: apikeys.ApiKeyService.UpdateAPIKey:input_type -> apikeys.UpdateAPIKeyRequest - 12, // 15: apikeys.ApiKeyService.ListAPIKeys:input_type -> apikeys.ListAPIKeysRequest - 14, // 16: apikeys.ApiKeyService.DeleteAPIKey:input_type -> apikeys.DeleteAPIKeyRequest - 16, // 17: apikeys.ApiKeyService.ExchangeKeyForToken:input_type -> apikeys.ExchangeKeyForTokenRequest - 5, // 18: apikeys.ApiKeyService.CreateAPIKey:output_type -> apikeys.CreateAPIKeyResponse - 7, // 19: apikeys.ApiKeyService.RefreshAPIKey:output_type -> apikeys.RefreshAPIKeyResponse - 9, // 20: apikeys.ApiKeyService.GetAPIKey:output_type -> apikeys.GetAPIKeyResponse - 11, // 21: apikeys.ApiKeyService.UpdateAPIKey:output_type -> apikeys.UpdateAPIKeyResponse - 13, // 22: apikeys.ApiKeyService.ListAPIKeys:output_type -> apikeys.ListAPIKeysResponse - 15, // 23: apikeys.ApiKeyService.DeleteAPIKey:output_type -> apikeys.DeleteAPIKeyResponse - 17, // 24: apikeys.ApiKeyService.ExchangeKeyForToken:output_type -> apikeys.ExchangeKeyForTokenResponse - 18, // [18:25] is the sub-list for method output_type - 11, // [11:18] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name -} - -func init() { file_apikeys_proto_init() } -func file_apikeys_proto_init() { - if File_apikeys_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_apikeys_proto_rawDesc), len(file_apikeys_proto_rawDesc)), - NumEnums: 1, - NumMessages: 17, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_apikeys_proto_goTypes, - DependencyIndexes: file_apikeys_proto_depIdxs, - EnumInfos: file_apikeys_proto_enumTypes, - MessageInfos: file_apikeys_proto_msgTypes, - }.Build() - File_apikeys_proto = out.File - file_apikeys_proto_goTypes = nil - file_apikeys_proto_depIdxs = nil -} diff --git a/sdp-go/area51.pb.go b/sdp-go/area51.pb.go deleted file mode 100644 index 6faa494d..00000000 --- a/sdp-go/area51.pb.go +++ /dev/null @@ -1,336 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: area51.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ChangeArchive struct { - state protoimpl.MessageState `protogen:"open.v1"` - Change *Change `protobuf:"bytes,1,opt,name=Change,proto3" json:"Change,omitempty"` - ChangingItemsBookmark *Bookmark `protobuf:"bytes,2,opt,name=changingItemsBookmark,proto3,oneof" json:"changingItemsBookmark,omitempty"` - BlastRadiusSnapshot *Snapshot `protobuf:"bytes,3,opt,name=blastRadiusSnapshot,proto3,oneof" json:"blastRadiusSnapshot,omitempty"` - SystemBeforeSnapshot *Snapshot `protobuf:"bytes,4,opt,name=systemBeforeSnapshot,proto3,oneof" json:"systemBeforeSnapshot,omitempty"` - SystemAfterSnapshot *Snapshot `protobuf:"bytes,5,opt,name=systemAfterSnapshot,proto3,oneof" json:"systemAfterSnapshot,omitempty"` - ChangeRiskMetadata *ChangeRiskMetadata `protobuf:"bytes,6,opt,name=changeRiskMetadata,proto3" json:"changeRiskMetadata,omitempty"` - PlannedChanges []*MappedItemDiff `protobuf:"bytes,7,rep,name=plannedChanges,proto3" json:"plannedChanges,omitempty"` - TimelineV2 []*ChangeTimelineEntryV2 `protobuf:"bytes,8,rep,name=timelineV2,proto3" json:"timelineV2,omitempty"` - Signals []*Signal `protobuf:"bytes,9,rep,name=signals,proto3" json:"signals,omitempty"` - Hypotheses []*HypothesesDetails `protobuf:"bytes,10,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeArchive) Reset() { - *x = ChangeArchive{} - mi := &file_area51_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeArchive) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeArchive) ProtoMessage() {} - -func (x *ChangeArchive) ProtoReflect() protoreflect.Message { - mi := &file_area51_proto_msgTypes[0] - 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 ChangeArchive.ProtoReflect.Descriptor instead. -func (*ChangeArchive) Descriptor() ([]byte, []int) { - return file_area51_proto_rawDescGZIP(), []int{0} -} - -func (x *ChangeArchive) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -func (x *ChangeArchive) GetChangingItemsBookmark() *Bookmark { - if x != nil { - return x.ChangingItemsBookmark - } - return nil -} - -func (x *ChangeArchive) GetBlastRadiusSnapshot() *Snapshot { - if x != nil { - return x.BlastRadiusSnapshot - } - return nil -} - -func (x *ChangeArchive) GetSystemBeforeSnapshot() *Snapshot { - if x != nil { - return x.SystemBeforeSnapshot - } - return nil -} - -func (x *ChangeArchive) GetSystemAfterSnapshot() *Snapshot { - if x != nil { - return x.SystemAfterSnapshot - } - return nil -} - -func (x *ChangeArchive) GetChangeRiskMetadata() *ChangeRiskMetadata { - if x != nil { - return x.ChangeRiskMetadata - } - return nil -} - -func (x *ChangeArchive) GetPlannedChanges() []*MappedItemDiff { - if x != nil { - return x.PlannedChanges - } - return nil -} - -func (x *ChangeArchive) GetTimelineV2() []*ChangeTimelineEntryV2 { - if x != nil { - return x.TimelineV2 - } - return nil -} - -func (x *ChangeArchive) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -func (x *ChangeArchive) GetHypotheses() []*HypothesesDetails { - if x != nil { - return x.Hypotheses - } - return nil -} - -type GetChangeArchiveRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeArchiveRequest) Reset() { - *x = GetChangeArchiveRequest{} - mi := &file_area51_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeArchiveRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeArchiveRequest) ProtoMessage() {} - -func (x *GetChangeArchiveRequest) ProtoReflect() protoreflect.Message { - mi := &file_area51_proto_msgTypes[1] - 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 GetChangeArchiveRequest.ProtoReflect.Descriptor instead. -func (*GetChangeArchiveRequest) Descriptor() ([]byte, []int) { - return file_area51_proto_rawDescGZIP(), []int{1} -} - -func (x *GetChangeArchiveRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type GetChangeArchiveResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeArchive *ChangeArchive `protobuf:"bytes,1,opt,name=changeArchive,proto3" json:"changeArchive,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeArchiveResponse) Reset() { - *x = GetChangeArchiveResponse{} - mi := &file_area51_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeArchiveResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeArchiveResponse) ProtoMessage() {} - -func (x *GetChangeArchiveResponse) ProtoReflect() protoreflect.Message { - mi := &file_area51_proto_msgTypes[2] - 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 GetChangeArchiveResponse.ProtoReflect.Descriptor instead. -func (*GetChangeArchiveResponse) Descriptor() ([]byte, []int) { - return file_area51_proto_rawDescGZIP(), []int{2} -} - -func (x *GetChangeArchiveResponse) GetChangeArchive() *ChangeArchive { - if x != nil { - return x.ChangeArchive - } - return nil -} - -var File_area51_proto protoreflect.FileDescriptor - -const file_area51_proto_rawDesc = "" + - "\n" + - "\farea51.proto\x12\x06area51\x1a\x0fbookmarks.proto\x1a\rchanges.proto\x1a\fsignal.proto\x1a\x0fsnapshots.proto\x1a\n" + - "util.proto\"\x85\x06\n" + - "\rChangeArchive\x12'\n" + - "\x06Change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06Change\x12N\n" + - "\x15changingItemsBookmark\x18\x02 \x01(\v2\x13.bookmarks.BookmarkH\x00R\x15changingItemsBookmark\x88\x01\x01\x12J\n" + - "\x13blastRadiusSnapshot\x18\x03 \x01(\v2\x13.snapshots.SnapshotH\x01R\x13blastRadiusSnapshot\x88\x01\x01\x12L\n" + - "\x14systemBeforeSnapshot\x18\x04 \x01(\v2\x13.snapshots.SnapshotH\x02R\x14systemBeforeSnapshot\x88\x01\x01\x12J\n" + - "\x13systemAfterSnapshot\x18\x05 \x01(\v2\x13.snapshots.SnapshotH\x03R\x13systemAfterSnapshot\x88\x01\x01\x12K\n" + - "\x12changeRiskMetadata\x18\x06 \x01(\v2\x1b.changes.ChangeRiskMetadataR\x12changeRiskMetadata\x12?\n" + - "\x0eplannedChanges\x18\a \x03(\v2\x17.changes.MappedItemDiffR\x0eplannedChanges\x12>\n" + - "\n" + - "timelineV2\x18\b \x03(\v2\x1e.changes.ChangeTimelineEntryV2R\n" + - "timelineV2\x12(\n" + - "\asignals\x18\t \x03(\v2\x0e.signal.SignalR\asignals\x12:\n" + - "\n" + - "hypotheses\x18\n" + - " \x03(\v2\x1a.changes.HypothesesDetailsR\n" + - "hypothesesB\x18\n" + - "\x16_changingItemsBookmarkB\x16\n" + - "\x14_blastRadiusSnapshotB\x17\n" + - "\x15_systemBeforeSnapshotB\x16\n" + - "\x14_systemAfterSnapshot\"-\n" + - "\x17GetChangeArchiveRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"W\n" + - "\x18GetChangeArchiveResponse\x12;\n" + - "\rchangeArchive\x18\x01 \x01(\v2\x15.area51.ChangeArchiveR\rchangeArchive2f\n" + - "\rArea51Service\x12U\n" + - "\x10GetChangeArchive\x12\x1f.area51.GetChangeArchiveRequest\x1a .area51.GetChangeArchiveResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_area51_proto_rawDescOnce sync.Once - file_area51_proto_rawDescData []byte -) - -func file_area51_proto_rawDescGZIP() []byte { - file_area51_proto_rawDescOnce.Do(func() { - file_area51_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_area51_proto_rawDesc), len(file_area51_proto_rawDesc))) - }) - return file_area51_proto_rawDescData -} - -var file_area51_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_area51_proto_goTypes = []any{ - (*ChangeArchive)(nil), // 0: area51.ChangeArchive - (*GetChangeArchiveRequest)(nil), // 1: area51.GetChangeArchiveRequest - (*GetChangeArchiveResponse)(nil), // 2: area51.GetChangeArchiveResponse - (*Change)(nil), // 3: changes.Change - (*Bookmark)(nil), // 4: bookmarks.Bookmark - (*Snapshot)(nil), // 5: snapshots.Snapshot - (*ChangeRiskMetadata)(nil), // 6: changes.ChangeRiskMetadata - (*MappedItemDiff)(nil), // 7: changes.MappedItemDiff - (*ChangeTimelineEntryV2)(nil), // 8: changes.ChangeTimelineEntryV2 - (*Signal)(nil), // 9: signal.Signal - (*HypothesesDetails)(nil), // 10: changes.HypothesesDetails -} -var file_area51_proto_depIdxs = []int32{ - 3, // 0: area51.ChangeArchive.Change:type_name -> changes.Change - 4, // 1: area51.ChangeArchive.changingItemsBookmark:type_name -> bookmarks.Bookmark - 5, // 2: area51.ChangeArchive.blastRadiusSnapshot:type_name -> snapshots.Snapshot - 5, // 3: area51.ChangeArchive.systemBeforeSnapshot:type_name -> snapshots.Snapshot - 5, // 4: area51.ChangeArchive.systemAfterSnapshot:type_name -> snapshots.Snapshot - 6, // 5: area51.ChangeArchive.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata - 7, // 6: area51.ChangeArchive.plannedChanges:type_name -> changes.MappedItemDiff - 8, // 7: area51.ChangeArchive.timelineV2:type_name -> changes.ChangeTimelineEntryV2 - 9, // 8: area51.ChangeArchive.signals:type_name -> signal.Signal - 10, // 9: area51.ChangeArchive.hypotheses:type_name -> changes.HypothesesDetails - 0, // 10: area51.GetChangeArchiveResponse.changeArchive:type_name -> area51.ChangeArchive - 1, // 11: area51.Area51Service.GetChangeArchive:input_type -> area51.GetChangeArchiveRequest - 2, // 12: area51.Area51Service.GetChangeArchive:output_type -> area51.GetChangeArchiveResponse - 12, // [12:13] is the sub-list for method output_type - 11, // [11:12] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name -} - -func init() { file_area51_proto_init() } -func file_area51_proto_init() { - if File_area51_proto != nil { - return - } - file_bookmarks_proto_init() - file_changes_proto_init() - file_signal_proto_init() - file_snapshots_proto_init() - file_util_proto_init() - file_area51_proto_msgTypes[0].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_area51_proto_rawDesc), len(file_area51_proto_rawDesc)), - NumEnums: 0, - NumMessages: 3, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_area51_proto_goTypes, - DependencyIndexes: file_area51_proto_depIdxs, - MessageInfos: file_area51_proto_msgTypes, - }.Build() - File_area51_proto = out.File - file_area51_proto_goTypes = nil - file_area51_proto_depIdxs = nil -} diff --git a/sdp-go/auth0support.pb.go b/sdp-go/auth0support.pb.go deleted file mode 100644 index 0d19e78e..00000000 --- a/sdp-go/auth0support.pb.go +++ /dev/null @@ -1,258 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: auth0support.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - _ "google.golang.org/protobuf/types/known/structpb" - _ "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Auth0CreateUserRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The Auth0 User ID - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - // The user's email address - Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` - // The user's full name. This will be split and stored as first_name and - // last_name internally. It is provided for convenience since some social - // providers do not provide first_name and last_name fields. If `first_name` - // and `last_name` are provided, this field will be ignored. - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - // Whether the user's email address has been verified - EmailVerified bool `protobuf:"varint,4,opt,name=email_verified,json=emailVerified,proto3" json:"email_verified,omitempty"` - // The user's first name - FirstName string `protobuf:"bytes,5,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` - // The user's last name - LastName string `protobuf:"bytes,6,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` - // The user's connection id - ConnectionId string `protobuf:"bytes,7,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` - // The user's profile picture URL - PictureUrl string `protobuf:"bytes,8,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Auth0CreateUserRequest) Reset() { - *x = Auth0CreateUserRequest{} - mi := &file_auth0support_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Auth0CreateUserRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Auth0CreateUserRequest) ProtoMessage() {} - -func (x *Auth0CreateUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_auth0support_proto_msgTypes[0] - 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 Auth0CreateUserRequest.ProtoReflect.Descriptor instead. -func (*Auth0CreateUserRequest) Descriptor() ([]byte, []int) { - return file_auth0support_proto_rawDescGZIP(), []int{0} -} - -func (x *Auth0CreateUserRequest) GetUserId() string { - if x != nil { - return x.UserId - } - return "" -} - -func (x *Auth0CreateUserRequest) GetEmail() string { - if x != nil { - return x.Email - } - return "" -} - -func (x *Auth0CreateUserRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Auth0CreateUserRequest) GetEmailVerified() bool { - if x != nil { - return x.EmailVerified - } - return false -} - -func (x *Auth0CreateUserRequest) GetFirstName() string { - if x != nil { - return x.FirstName - } - return "" -} - -func (x *Auth0CreateUserRequest) GetLastName() string { - if x != nil { - return x.LastName - } - return "" -} - -func (x *Auth0CreateUserRequest) GetConnectionId() string { - if x != nil { - return x.ConnectionId - } - return "" -} - -func (x *Auth0CreateUserRequest) GetPictureUrl() string { - if x != nil { - return x.PictureUrl - } - return "" -} - -type Auth0CreateUserResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Auth0CreateUserResponse) Reset() { - *x = Auth0CreateUserResponse{} - mi := &file_auth0support_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Auth0CreateUserResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Auth0CreateUserResponse) ProtoMessage() {} - -func (x *Auth0CreateUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_auth0support_proto_msgTypes[1] - 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 Auth0CreateUserResponse.ProtoReflect.Descriptor instead. -func (*Auth0CreateUserResponse) Descriptor() ([]byte, []int) { - return file_auth0support_proto_rawDescGZIP(), []int{1} -} - -func (x *Auth0CreateUserResponse) GetOrgId() string { - if x != nil { - return x.OrgId - } - return "" -} - -var File_auth0support_proto protoreflect.FileDescriptor - -const file_auth0support_proto_rawDesc = "" + - "\n" + - "\x12auth0support.proto\x12\fauth0support\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\raccount.proto\"\x84\x02\n" + - "\x16Auth0CreateUserRequest\x12\x17\n" + - "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x14\n" + - "\x05email\x18\x02 \x01(\tR\x05email\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\x12%\n" + - "\x0eemail_verified\x18\x04 \x01(\bR\remailVerified\x12\x1d\n" + - "\n" + - "first_name\x18\x05 \x01(\tR\tfirstName\x12\x1b\n" + - "\tlast_name\x18\x06 \x01(\tR\blastName\x12#\n" + - "\rconnection_id\x18\a \x01(\tR\fconnectionId\x12\x1f\n" + - "\vpicture_url\x18\b \x01(\tR\n" + - "pictureUrl\"0\n" + - "\x17Auth0CreateUserResponse\x12\x15\n" + - "\x06org_id\x18\x01 \x01(\tR\x05orgId2\xc7\x01\n" + - "\fAuth0Support\x12Y\n" + - "\n" + - "CreateUser\x12$.auth0support.Auth0CreateUserRequest\x1a%.auth0support.Auth0CreateUserResponse\x12\\\n" + - "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_auth0support_proto_rawDescOnce sync.Once - file_auth0support_proto_rawDescData []byte -) - -func file_auth0support_proto_rawDescGZIP() []byte { - file_auth0support_proto_rawDescOnce.Do(func() { - file_auth0support_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth0support_proto_rawDesc), len(file_auth0support_proto_rawDesc))) - }) - return file_auth0support_proto_rawDescData -} - -var file_auth0support_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_auth0support_proto_goTypes = []any{ - (*Auth0CreateUserRequest)(nil), // 0: auth0support.Auth0CreateUserRequest - (*Auth0CreateUserResponse)(nil), // 1: auth0support.Auth0CreateUserResponse - (*AdminKeepaliveSourcesRequest)(nil), // 2: account.AdminKeepaliveSourcesRequest - (*KeepaliveSourcesResponse)(nil), // 3: account.KeepaliveSourcesResponse -} -var file_auth0support_proto_depIdxs = []int32{ - 0, // 0: auth0support.Auth0Support.CreateUser:input_type -> auth0support.Auth0CreateUserRequest - 2, // 1: auth0support.Auth0Support.KeepaliveSources:input_type -> account.AdminKeepaliveSourcesRequest - 1, // 2: auth0support.Auth0Support.CreateUser:output_type -> auth0support.Auth0CreateUserResponse - 3, // 3: auth0support.Auth0Support.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_auth0support_proto_init() } -func file_auth0support_proto_init() { - if File_auth0support_proto != nil { - return - } - file_account_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth0support_proto_rawDesc), len(file_auth0support_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_auth0support_proto_goTypes, - DependencyIndexes: file_auth0support_proto_depIdxs, - MessageInfos: file_auth0support_proto_msgTypes, - }.Build() - File_auth0support_proto = out.File - file_auth0support_proto_goTypes = nil - file_auth0support_proto_depIdxs = nil -} diff --git a/sdp-go/bookmarks.go b/sdp-go/bookmarks.go deleted file mode 100644 index 578fa845..00000000 --- a/sdp-go/bookmarks.go +++ /dev/null @@ -1,23 +0,0 @@ -package sdp - -func (b *Bookmark) ToMap() map[string]any { - return map[string]any{ - "metadata": b.GetMetadata().ToMap(), - "properties": b.GetProperties().ToMap(), - } -} - -func (bm *BookmarkMetadata) ToMap() map[string]any { - return map[string]any{ - "UUID": stringFromUuidBytes(bm.GetUUID()), - "created": bm.GetCreated().AsTime(), - } -} - -func (bp *BookmarkProperties) ToMap() map[string]any { - return map[string]any{ - "name": bp.GetName(), - "description": bp.GetDescription(), - "queries": bp.GetQueries(), - } -} diff --git a/sdp-go/bookmarks.pb.go b/sdp-go/bookmarks.pb.go deleted file mode 100644 index d47f043d..00000000 --- a/sdp-go/bookmarks.pb.go +++ /dev/null @@ -1,886 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: bookmarks.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// a complete Bookmark with user-supplied and machine-supplied values -type Bookmark struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *BookmarkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *BookmarkProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Bookmark) Reset() { - *x = Bookmark{} - mi := &file_bookmarks_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Bookmark) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Bookmark) ProtoMessage() {} - -func (x *Bookmark) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[0] - 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 Bookmark.ProtoReflect.Descriptor instead. -func (*Bookmark) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{0} -} - -func (x *Bookmark) GetMetadata() *BookmarkMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Bookmark) GetProperties() *BookmarkProperties { - if x != nil { - return x.Properties - } - return nil -} - -// The user-editable parts of a Bookmark -type BookmarkProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // user supplied name of this bookmark - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // user supplied description of this bookmark - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - // queries that make up the bookmark - Queries []*Query `protobuf:"bytes,3,rep,name=queries,proto3" json:"queries,omitempty"` - // Whether this bookmark is a system bookmark. System bookmarks are hidden - // from list results and can therefore only be accessed by their UUID. - // Bookmarks created by users are not system bookmarks. - IsSystem bool `protobuf:"varint,5,opt,name=isSystem,proto3" json:"isSystem,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BookmarkProperties) Reset() { - *x = BookmarkProperties{} - mi := &file_bookmarks_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BookmarkProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BookmarkProperties) ProtoMessage() {} - -func (x *BookmarkProperties) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[1] - 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 BookmarkProperties.ProtoReflect.Descriptor instead. -func (*BookmarkProperties) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{1} -} - -func (x *BookmarkProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *BookmarkProperties) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *BookmarkProperties) GetQueries() []*Query { - if x != nil { - return x.Queries - } - return nil -} - -func (x *BookmarkProperties) GetIsSystem() bool { - if x != nil { - return x.IsSystem - } - return false -} - -// Descriptor for a bookmark -type BookmarkMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id to identify this bookmark - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // timestamp when this bookmark was created - Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BookmarkMetadata) Reset() { - *x = BookmarkMetadata{} - mi := &file_bookmarks_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BookmarkMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BookmarkMetadata) ProtoMessage() {} - -func (x *BookmarkMetadata) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[2] - 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 BookmarkMetadata.ProtoReflect.Descriptor instead. -func (*BookmarkMetadata) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{2} -} - -func (x *BookmarkMetadata) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *BookmarkMetadata) GetCreated() *timestamppb.Timestamp { - if x != nil { - return x.Created - } - return nil -} - -// list all bookmarks -type ListBookmarksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListBookmarksRequest) Reset() { - *x = ListBookmarksRequest{} - mi := &file_bookmarks_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListBookmarksRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListBookmarksRequest) ProtoMessage() {} - -func (x *ListBookmarksRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[3] - 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 ListBookmarksRequest.ProtoReflect.Descriptor instead. -func (*ListBookmarksRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{3} -} - -type ListBookmarkResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Bookmarks []*Bookmark `protobuf:"bytes,3,rep,name=bookmarks,proto3" json:"bookmarks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListBookmarkResponse) Reset() { - *x = ListBookmarkResponse{} - mi := &file_bookmarks_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListBookmarkResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListBookmarkResponse) ProtoMessage() {} - -func (x *ListBookmarkResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[4] - 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 ListBookmarkResponse.ProtoReflect.Descriptor instead. -func (*ListBookmarkResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{4} -} - -func (x *ListBookmarkResponse) GetBookmarks() []*Bookmark { - if x != nil { - return x.Bookmarks - } - return nil -} - -// creates a new bookmark -type CreateBookmarkRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *BookmarkProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateBookmarkRequest) Reset() { - *x = CreateBookmarkRequest{} - mi := &file_bookmarks_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateBookmarkRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateBookmarkRequest) ProtoMessage() {} - -func (x *CreateBookmarkRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[5] - 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 CreateBookmarkRequest.ProtoReflect.Descriptor instead. -func (*CreateBookmarkRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{5} -} - -func (x *CreateBookmarkRequest) GetProperties() *BookmarkProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateBookmarkResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Bookmark *Bookmark `protobuf:"bytes,1,opt,name=bookmark,proto3" json:"bookmark,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateBookmarkResponse) Reset() { - *x = CreateBookmarkResponse{} - mi := &file_bookmarks_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateBookmarkResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateBookmarkResponse) ProtoMessage() {} - -func (x *CreateBookmarkResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[6] - 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 CreateBookmarkResponse.ProtoReflect.Descriptor instead. -func (*CreateBookmarkResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{6} -} - -func (x *CreateBookmarkResponse) GetBookmark() *Bookmark { - if x != nil { - return x.Bookmark - } - return nil -} - -// gets a specific bookmark -type GetBookmarkRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetBookmarkRequest) Reset() { - *x = GetBookmarkRequest{} - mi := &file_bookmarks_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetBookmarkRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetBookmarkRequest) ProtoMessage() {} - -func (x *GetBookmarkRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[7] - 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 GetBookmarkRequest.ProtoReflect.Descriptor instead. -func (*GetBookmarkRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{7} -} - -func (x *GetBookmarkRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type GetBookmarkResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Bookmark *Bookmark `protobuf:"bytes,1,opt,name=bookmark,proto3" json:"bookmark,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetBookmarkResponse) Reset() { - *x = GetBookmarkResponse{} - mi := &file_bookmarks_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetBookmarkResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetBookmarkResponse) ProtoMessage() {} - -func (x *GetBookmarkResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[8] - 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 GetBookmarkResponse.ProtoReflect.Descriptor instead. -func (*GetBookmarkResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{8} -} - -func (x *GetBookmarkResponse) GetBookmark() *Bookmark { - if x != nil { - return x.Bookmark - } - return nil -} - -// updates an existing bookmark -type UpdateBookmarkRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id to identify this bookmark - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // new attributes for this bookmark - Properties *BookmarkProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateBookmarkRequest) Reset() { - *x = UpdateBookmarkRequest{} - mi := &file_bookmarks_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateBookmarkRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateBookmarkRequest) ProtoMessage() {} - -func (x *UpdateBookmarkRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[9] - 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 UpdateBookmarkRequest.ProtoReflect.Descriptor instead. -func (*UpdateBookmarkRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{9} -} - -func (x *UpdateBookmarkRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *UpdateBookmarkRequest) GetProperties() *BookmarkProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateBookmarkResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Bookmark *Bookmark `protobuf:"bytes,3,opt,name=bookmark,proto3" json:"bookmark,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateBookmarkResponse) Reset() { - *x = UpdateBookmarkResponse{} - mi := &file_bookmarks_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateBookmarkResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateBookmarkResponse) ProtoMessage() {} - -func (x *UpdateBookmarkResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[10] - 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 UpdateBookmarkResponse.ProtoReflect.Descriptor instead. -func (*UpdateBookmarkResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{10} -} - -func (x *UpdateBookmarkResponse) GetBookmark() *Bookmark { - if x != nil { - return x.Bookmark - } - return nil -} - -// Delete the bookmark with the specified ID. -type DeleteBookmarkRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id of the bookmark to delete - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteBookmarkRequest) Reset() { - *x = DeleteBookmarkRequest{} - mi := &file_bookmarks_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteBookmarkRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteBookmarkRequest) ProtoMessage() {} - -func (x *DeleteBookmarkRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[11] - 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 DeleteBookmarkRequest.ProtoReflect.Descriptor instead. -func (*DeleteBookmarkRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{11} -} - -func (x *DeleteBookmarkRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type DeleteBookmarkResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteBookmarkResponse) Reset() { - *x = DeleteBookmarkResponse{} - mi := &file_bookmarks_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteBookmarkResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteBookmarkResponse) ProtoMessage() {} - -func (x *DeleteBookmarkResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[12] - 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 DeleteBookmarkResponse.ProtoReflect.Descriptor instead. -func (*DeleteBookmarkResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{12} -} - -type GetAffectedBookmarksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the snapshot to consider - SnapshotUUID []byte `protobuf:"bytes,1,opt,name=snapshotUUID,proto3" json:"snapshotUUID,omitempty"` - // the bookmarks to filter - BookmarkUUIDs [][]byte `protobuf:"bytes,2,rep,name=bookmarkUUIDs,proto3" json:"bookmarkUUIDs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAffectedBookmarksRequest) Reset() { - *x = GetAffectedBookmarksRequest{} - mi := &file_bookmarks_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAffectedBookmarksRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAffectedBookmarksRequest) ProtoMessage() {} - -func (x *GetAffectedBookmarksRequest) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetAffectedBookmarksRequest.ProtoReflect.Descriptor instead. -func (*GetAffectedBookmarksRequest) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{13} -} - -func (x *GetAffectedBookmarksRequest) GetSnapshotUUID() []byte { - if x != nil { - return x.SnapshotUUID - } - return nil -} - -func (x *GetAffectedBookmarksRequest) GetBookmarkUUIDs() [][]byte { - if x != nil { - return x.BookmarkUUIDs - } - return nil -} - -type GetAffectedBookmarksResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the bookmarks that intersected with the snapshot - BookmarkUUIDs [][]byte `protobuf:"bytes,1,rep,name=bookmarkUUIDs,proto3" json:"bookmarkUUIDs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAffectedBookmarksResponse) Reset() { - *x = GetAffectedBookmarksResponse{} - mi := &file_bookmarks_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAffectedBookmarksResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAffectedBookmarksResponse) ProtoMessage() {} - -func (x *GetAffectedBookmarksResponse) ProtoReflect() protoreflect.Message { - mi := &file_bookmarks_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetAffectedBookmarksResponse.ProtoReflect.Descriptor instead. -func (*GetAffectedBookmarksResponse) Descriptor() ([]byte, []int) { - return file_bookmarks_proto_rawDescGZIP(), []int{14} -} - -func (x *GetAffectedBookmarksResponse) GetBookmarkUUIDs() [][]byte { - if x != nil { - return x.BookmarkUUIDs - } - return nil -} - -var File_bookmarks_proto protoreflect.FileDescriptor - -const file_bookmarks_proto_rawDesc = "" + - "\n" + - "\x0fbookmarks.proto\x12\tbookmarks\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vitems.proto\"\x82\x01\n" + - "\bBookmark\x127\n" + - "\bmetadata\x18\x01 \x01(\v2\x1b.bookmarks.BookmarkMetadataR\bmetadata\x12=\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + - "properties\"\x8e\x01\n" + - "\x12BookmarkProperties\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12 \n" + - "\aqueries\x18\x03 \x03(\v2\x06.QueryR\aqueries\x12\x1a\n" + - "\bisSystem\x18\x05 \x01(\bR\bisSystemJ\x04\b\x04\x10\x05\"\\\n" + - "\x10BookmarkMetadata\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + - "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\"\x16\n" + - "\x14ListBookmarksRequest\"I\n" + - "\x14ListBookmarkResponse\x121\n" + - "\tbookmarks\x18\x03 \x03(\v2\x13.bookmarks.BookmarkR\tbookmarks\"V\n" + - "\x15CreateBookmarkRequest\x12=\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + - "properties\"I\n" + - "\x16CreateBookmarkResponse\x12/\n" + - "\bbookmark\x18\x01 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"(\n" + - "\x12GetBookmarkRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"F\n" + - "\x13GetBookmarkResponse\x12/\n" + - "\bbookmark\x18\x01 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"j\n" + - "\x15UpdateBookmarkRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12=\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + - "properties\"I\n" + - "\x16UpdateBookmarkResponse\x12/\n" + - "\bbookmark\x18\x03 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"+\n" + - "\x15DeleteBookmarkRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x18\n" + - "\x16DeleteBookmarkResponse\"g\n" + - "\x1bGetAffectedBookmarksRequest\x12\"\n" + - "\fsnapshotUUID\x18\x01 \x01(\fR\fsnapshotUUID\x12$\n" + - "\rbookmarkUUIDs\x18\x02 \x03(\fR\rbookmarkUUIDs\"D\n" + - "\x1cGetAffectedBookmarksResponse\x12$\n" + - "\rbookmarkUUIDs\x18\x01 \x03(\fR\rbookmarkUUIDs2\xa1\x04\n" + - "\x10BookmarksService\x12Q\n" + - "\rListBookmarks\x12\x1f.bookmarks.ListBookmarksRequest\x1a\x1f.bookmarks.ListBookmarkResponse\x12U\n" + - "\x0eCreateBookmark\x12 .bookmarks.CreateBookmarkRequest\x1a!.bookmarks.CreateBookmarkResponse\x12L\n" + - "\vGetBookmark\x12\x1d.bookmarks.GetBookmarkRequest\x1a\x1e.bookmarks.GetBookmarkResponse\x12U\n" + - "\x0eUpdateBookmark\x12 .bookmarks.UpdateBookmarkRequest\x1a!.bookmarks.UpdateBookmarkResponse\x12U\n" + - "\x0eDeleteBookmark\x12 .bookmarks.DeleteBookmarkRequest\x1a!.bookmarks.DeleteBookmarkResponse\x12g\n" + - "\x14GetAffectedBookmarks\x12&.bookmarks.GetAffectedBookmarksRequest\x1a'.bookmarks.GetAffectedBookmarksResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_bookmarks_proto_rawDescOnce sync.Once - file_bookmarks_proto_rawDescData []byte -) - -func file_bookmarks_proto_rawDescGZIP() []byte { - file_bookmarks_proto_rawDescOnce.Do(func() { - file_bookmarks_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bookmarks_proto_rawDesc), len(file_bookmarks_proto_rawDesc))) - }) - return file_bookmarks_proto_rawDescData -} - -var file_bookmarks_proto_msgTypes = make([]protoimpl.MessageInfo, 15) -var file_bookmarks_proto_goTypes = []any{ - (*Bookmark)(nil), // 0: bookmarks.Bookmark - (*BookmarkProperties)(nil), // 1: bookmarks.BookmarkProperties - (*BookmarkMetadata)(nil), // 2: bookmarks.BookmarkMetadata - (*ListBookmarksRequest)(nil), // 3: bookmarks.ListBookmarksRequest - (*ListBookmarkResponse)(nil), // 4: bookmarks.ListBookmarkResponse - (*CreateBookmarkRequest)(nil), // 5: bookmarks.CreateBookmarkRequest - (*CreateBookmarkResponse)(nil), // 6: bookmarks.CreateBookmarkResponse - (*GetBookmarkRequest)(nil), // 7: bookmarks.GetBookmarkRequest - (*GetBookmarkResponse)(nil), // 8: bookmarks.GetBookmarkResponse - (*UpdateBookmarkRequest)(nil), // 9: bookmarks.UpdateBookmarkRequest - (*UpdateBookmarkResponse)(nil), // 10: bookmarks.UpdateBookmarkResponse - (*DeleteBookmarkRequest)(nil), // 11: bookmarks.DeleteBookmarkRequest - (*DeleteBookmarkResponse)(nil), // 12: bookmarks.DeleteBookmarkResponse - (*GetAffectedBookmarksRequest)(nil), // 13: bookmarks.GetAffectedBookmarksRequest - (*GetAffectedBookmarksResponse)(nil), // 14: bookmarks.GetAffectedBookmarksResponse - (*Query)(nil), // 15: Query - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp -} -var file_bookmarks_proto_depIdxs = []int32{ - 2, // 0: bookmarks.Bookmark.metadata:type_name -> bookmarks.BookmarkMetadata - 1, // 1: bookmarks.Bookmark.properties:type_name -> bookmarks.BookmarkProperties - 15, // 2: bookmarks.BookmarkProperties.queries:type_name -> Query - 16, // 3: bookmarks.BookmarkMetadata.created:type_name -> google.protobuf.Timestamp - 0, // 4: bookmarks.ListBookmarkResponse.bookmarks:type_name -> bookmarks.Bookmark - 1, // 5: bookmarks.CreateBookmarkRequest.properties:type_name -> bookmarks.BookmarkProperties - 0, // 6: bookmarks.CreateBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark - 0, // 7: bookmarks.GetBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark - 1, // 8: bookmarks.UpdateBookmarkRequest.properties:type_name -> bookmarks.BookmarkProperties - 0, // 9: bookmarks.UpdateBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark - 3, // 10: bookmarks.BookmarksService.ListBookmarks:input_type -> bookmarks.ListBookmarksRequest - 5, // 11: bookmarks.BookmarksService.CreateBookmark:input_type -> bookmarks.CreateBookmarkRequest - 7, // 12: bookmarks.BookmarksService.GetBookmark:input_type -> bookmarks.GetBookmarkRequest - 9, // 13: bookmarks.BookmarksService.UpdateBookmark:input_type -> bookmarks.UpdateBookmarkRequest - 11, // 14: bookmarks.BookmarksService.DeleteBookmark:input_type -> bookmarks.DeleteBookmarkRequest - 13, // 15: bookmarks.BookmarksService.GetAffectedBookmarks:input_type -> bookmarks.GetAffectedBookmarksRequest - 4, // 16: bookmarks.BookmarksService.ListBookmarks:output_type -> bookmarks.ListBookmarkResponse - 6, // 17: bookmarks.BookmarksService.CreateBookmark:output_type -> bookmarks.CreateBookmarkResponse - 8, // 18: bookmarks.BookmarksService.GetBookmark:output_type -> bookmarks.GetBookmarkResponse - 10, // 19: bookmarks.BookmarksService.UpdateBookmark:output_type -> bookmarks.UpdateBookmarkResponse - 12, // 20: bookmarks.BookmarksService.DeleteBookmark:output_type -> bookmarks.DeleteBookmarkResponse - 14, // 21: bookmarks.BookmarksService.GetAffectedBookmarks:output_type -> bookmarks.GetAffectedBookmarksResponse - 16, // [16:22] is the sub-list for method output_type - 10, // [10:16] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name -} - -func init() { file_bookmarks_proto_init() } -func file_bookmarks_proto_init() { - if File_bookmarks_proto != nil { - return - } - file_items_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_bookmarks_proto_rawDesc), len(file_bookmarks_proto_rawDesc)), - NumEnums: 0, - NumMessages: 15, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_bookmarks_proto_goTypes, - DependencyIndexes: file_bookmarks_proto_depIdxs, - MessageInfos: file_bookmarks_proto_msgTypes, - }.Build() - File_bookmarks_proto = out.File - file_bookmarks_proto_goTypes = nil - file_bookmarks_proto_depIdxs = nil -} diff --git a/sdp-go/cached_entry.pb.go b/sdp-go/cached_entry.pb.go deleted file mode 100644 index b33e6638..00000000 --- a/sdp-go/cached_entry.pb.go +++ /dev/null @@ -1,188 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: cached_entry.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// CachedEntry represents a cached result in the BoltDB cache -type CachedEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The cached item (nil/empty for errors) - Item *Item `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // The cached error (nil/empty for items) - Error *QueryError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` - // Expiry timestamp in Unix nanoseconds - ExpiryUnixNano int64 `protobuf:"varint,3,opt,name=expiry_unix_nano,json=expiryUnixNano,proto3" json:"expiry_unix_nano,omitempty"` - // Index values for efficient lookup - UniqueAttributeValue string `protobuf:"bytes,4,opt,name=unique_attribute_value,json=uniqueAttributeValue,proto3" json:"unique_attribute_value,omitempty"` - Method QueryMethod `protobuf:"varint,5,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` - Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"` - SstHash string `protobuf:"bytes,7,opt,name=sst_hash,json=sstHash,proto3" json:"sst_hash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CachedEntry) Reset() { - *x = CachedEntry{} - mi := &file_cached_entry_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CachedEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CachedEntry) ProtoMessage() {} - -func (x *CachedEntry) ProtoReflect() protoreflect.Message { - mi := &file_cached_entry_proto_msgTypes[0] - 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 CachedEntry.ProtoReflect.Descriptor instead. -func (*CachedEntry) Descriptor() ([]byte, []int) { - return file_cached_entry_proto_rawDescGZIP(), []int{0} -} - -func (x *CachedEntry) GetItem() *Item { - if x != nil { - return x.Item - } - return nil -} - -func (x *CachedEntry) GetError() *QueryError { - if x != nil { - return x.Error - } - return nil -} - -func (x *CachedEntry) GetExpiryUnixNano() int64 { - if x != nil { - return x.ExpiryUnixNano - } - return 0 -} - -func (x *CachedEntry) GetUniqueAttributeValue() string { - if x != nil { - return x.UniqueAttributeValue - } - return "" -} - -func (x *CachedEntry) GetMethod() QueryMethod { - if x != nil { - return x.Method - } - return QueryMethod_GET -} - -func (x *CachedEntry) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *CachedEntry) GetSstHash() string { - if x != nil { - return x.SstHash - } - return "" -} - -var File_cached_entry_proto protoreflect.FileDescriptor - -const file_cached_entry_proto_rawDesc = "" + - "\n" + - "\x12cached_entry.proto\x1a\vitems.proto\"\x82\x02\n" + - "\vCachedEntry\x12\x19\n" + - "\x04item\x18\x01 \x01(\v2\x05.ItemR\x04item\x12!\n" + - "\x05error\x18\x02 \x01(\v2\v.QueryErrorR\x05error\x12(\n" + - "\x10expiry_unix_nano\x18\x03 \x01(\x03R\x0eexpiryUnixNano\x124\n" + - "\x16unique_attribute_value\x18\x04 \x01(\tR\x14uniqueAttributeValue\x12$\n" + - "\x06method\x18\x05 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + - "\x05query\x18\x06 \x01(\tR\x05query\x12\x19\n" + - "\bsst_hash\x18\a \x01(\tR\asstHashB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_cached_entry_proto_rawDescOnce sync.Once - file_cached_entry_proto_rawDescData []byte -) - -func file_cached_entry_proto_rawDescGZIP() []byte { - file_cached_entry_proto_rawDescOnce.Do(func() { - file_cached_entry_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cached_entry_proto_rawDesc), len(file_cached_entry_proto_rawDesc))) - }) - return file_cached_entry_proto_rawDescData -} - -var file_cached_entry_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_cached_entry_proto_goTypes = []any{ - (*CachedEntry)(nil), // 0: CachedEntry - (*Item)(nil), // 1: Item - (*QueryError)(nil), // 2: QueryError - (QueryMethod)(0), // 3: QueryMethod -} -var file_cached_entry_proto_depIdxs = []int32{ - 1, // 0: CachedEntry.item:type_name -> Item - 2, // 1: CachedEntry.error:type_name -> QueryError - 3, // 2: CachedEntry.method:type_name -> QueryMethod - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name -} - -func init() { file_cached_entry_proto_init() } -func file_cached_entry_proto_init() { - if File_cached_entry_proto != nil { - return - } - file_items_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_cached_entry_proto_rawDesc), len(file_cached_entry_proto_rawDesc)), - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_cached_entry_proto_goTypes, - DependencyIndexes: file_cached_entry_proto_depIdxs, - MessageInfos: file_cached_entry_proto_msgTypes, - }.Build() - File_cached_entry_proto = out.File - file_cached_entry_proto_goTypes = nil - file_cached_entry_proto_depIdxs = nil -} diff --git a/sdp-go/changes.go b/sdp-go/changes.go deleted file mode 100644 index 63881d87..00000000 --- a/sdp-go/changes.go +++ /dev/null @@ -1,455 +0,0 @@ -package sdp - -import ( - "errors" - "fmt" - - "github.com/google/uuid" - "gopkg.in/yaml.v3" -) - -func (a *ChangeMetadata) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *ChangeMetadata) GetNullUUID() uuid.NullUUID { - u := a.GetUUIDParsed() - if u == nil { - return uuid.NullUUID{Valid: false} - } - return uuid.NullUUID{UUID: *u, Valid: true} -} - -func (a *ChangeProperties) GetChangingItemsBookmarkUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetChangingItemsBookmarkUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *ChangeProperties) GetSystemBeforeSnapshotUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetSystemBeforeSnapshotUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *ChangeProperties) GetSystemAfterSnapshotUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetSystemAfterSnapshotUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *GetChangeRequest) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *UpdateChangeRequest) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (a *DeleteChangeRequest) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(a.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *GetDiffRequest) GetChangeUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetChangeUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *ListChangingItemsSummaryRequest) GetChangeUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetChangeUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *StartChangeRequest) GetChangeUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetChangeUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *EndChangeRequest) GetChangeUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetChangeUUID()) - if err != nil { - return nil - } - return &u -} - -func (c *Change) ToMap() map[string]any { - return map[string]any{ - "metadata": c.GetMetadata().ToMap(), - "properties": c.GetProperties().ToMap(), - } -} - -func stringFromUuidBytes(b []byte) string { - u, err := uuid.FromBytes(b) - if err != nil { - return "" - } - return u.String() -} - -func (r *Reference) ToMap() map[string]any { - return map[string]any{ - "type": r.GetType(), - "uniqueAttributeValue": r.GetUniqueAttributeValue(), - "scope": r.GetScope(), - } -} - -func (r *Risk) ToMap() map[string]any { - relatedItems := make([]map[string]any, len(r.GetRelatedItems())) - for i, ri := range r.GetRelatedItems() { - relatedItems[i] = ri.ToMap() - } - - return map[string]any{ - "uuid": stringFromUuidBytes(r.GetUUID()), - "title": r.GetTitle(), - "severity": r.GetSeverity().String(), - "description": r.GetDescription(), - "relatedItems": relatedItems, - } -} - -func (r *GetChangeRisksResponse) ToMap() map[string]any { - rmd := r.GetChangeRiskMetadata() - risks := make([]map[string]any, len(rmd.GetRisks())) - for i, ri := range rmd.GetRisks() { - risks[i] = ri.ToMap() - } - - return map[string]any{ - "risks": risks, - "numHighRisk": rmd.GetNumHighRisk(), - "numMediumRisk": rmd.GetNumMediumRisk(), - "numLowRisk": rmd.GetNumLowRisk(), - "changeAnalysisStatus": rmd.GetChangeAnalysisStatus().ToMap(), - } -} - -func (cm *ChangeMetadata) ToMap() map[string]any { - return map[string]any{ - "UUID": stringFromUuidBytes(cm.GetUUID()), - "createdAt": cm.GetCreatedAt().AsTime(), - "updatedAt": cm.GetUpdatedAt().AsTime(), - "status": cm.GetStatus().String(), - "creatorName": cm.GetCreatorName(), - "numAffectedItems": cm.GetNumAffectedItems(), - "numAffectedEdges": cm.GetNumAffectedEdges(), - "numUnchangedItems": cm.GetNumUnchangedItems(), - "numCreatedItems": cm.GetNumCreatedItems(), - "numUpdatedItems": cm.GetNumUpdatedItems(), - "numDeletedItems": cm.GetNumDeletedItems(), - "UnknownHealthChange": cm.GetUnknownHealthChange(), - "OkHealthChange": cm.GetOkHealthChange(), - "WarningHealthChange": cm.GetWarningHealthChange(), - "ErrorHealthChange": cm.GetErrorHealthChange(), - "PendingHealthChange": cm.GetPendingHealthChange(), - } -} - -func (i *Item) ToMap() map[string]any { - return map[string]any{ - "type": i.GetType(), - "uniqueAttributeValue": i.UniqueAttributeValue(), - "scope": i.GetScope(), - "attributes": i.GetAttributes().GetAttrStruct().GetFields(), - } -} - -func (id *ItemDiff) ToMap() map[string]any { - result := map[string]any{ - "status": id.GetStatus().String(), - } - if id.GetItem() != nil { - result["item"] = id.GetItem().ToMap() - } - if id.GetBefore() != nil { - result["before"] = id.GetBefore().ToMap() - } - if id.GetAfter() != nil { - result["after"] = id.GetAfter().ToMap() - } - return result -} - -func (id *ItemDiff) GloballyUniqueName() string { - if id.GetItem() != nil { - return id.GetItem().GloballyUniqueName() - } else if id.GetBefore() != nil { - return id.GetBefore().GloballyUniqueName() - } else if id.GetAfter() != nil { - return id.GetAfter().GloballyUniqueName() - } else { - return "empty item diff" - } -} - -func (cp *ChangeProperties) ToMap() map[string]any { - plannedChanges := make([]map[string]any, len(cp.GetPlannedChanges())) - for i, id := range cp.GetPlannedChanges() { - plannedChanges[i] = id.ToMap() - } - - return map[string]any{ - "title": cp.GetTitle(), - "description": cp.GetDescription(), - "ticketLink": cp.GetTicketLink(), - "owner": cp.GetOwner(), - "ccEmails": cp.GetCcEmails(), - "changingItemsBookmarkUUID": stringFromUuidBytes(cp.GetChangingItemsBookmarkUUID()), - "blastRadiusSnapshotUUID": stringFromUuidBytes(cp.GetBlastRadiusSnapshotUUID()), - "systemBeforeSnapshotUUID": stringFromUuidBytes(cp.GetSystemBeforeSnapshotUUID()), - "systemAfterSnapshotUUID": stringFromUuidBytes(cp.GetSystemAfterSnapshotUUID()), - "plannedChanges": cp.GetPlannedChanges(), - "rawPlan": cp.GetRawPlan(), - "codeChanges": cp.GetCodeChanges(), - "repo": cp.GetRepo(), - "tags": cp.GetEnrichedTags(), - } -} - -func (rcs *ChangeAnalysisStatus) ToMap() map[string]any { - if rcs == nil { - return map[string]any{} - } - - return map[string]any{ - "status": rcs.GetStatus().String(), - } -} - -func (s StartChangeResponse_State) ToMessage() string { - switch s { - case StartChangeResponse_STATE_UNSPECIFIED: - return "unknown" - case StartChangeResponse_STATE_TAKING_SNAPSHOT: - return "Snapshot is being taken" - case StartChangeResponse_STATE_SAVING_SNAPSHOT: - return "Snapshot is being saved" - case StartChangeResponse_STATE_DONE: - return "Everything is complete" - default: - return "unknown" - } -} - -func (s EndChangeResponse_State) ToMessage() string { - switch s { - case EndChangeResponse_STATE_UNSPECIFIED: - return "unknown" - case EndChangeResponse_STATE_TAKING_SNAPSHOT: - return "Snapshot is being taken" - case EndChangeResponse_STATE_SAVING_SNAPSHOT: - return "Snapshot is being saved" - case EndChangeResponse_STATE_DONE: - return "Everything is complete" - default: - return "unknown" - } -} - -// RoutineChangesYAML represents the YAML structure for routine changes configuration. -// It defines parameters for detecting routine changes in infrastructure: -// - Sensitivity: Threshold for determining what constitutes a routine change (0 or higher) -// - DurationInDays: Time window in days to analyze for routine patterns (must be >= 1) -// - EventsPerDay: Expected number of change events per day for routine detection (must be >= 1) -type RoutineChangesYAML struct { - Sensitivity float32 `yaml:"sensitivity"` - DurationInDays float32 `yaml:"duration_in_days"` - EventsPerDay float32 `yaml:"events_per_day"` -} - -// GithubOrganisationYAML represents the YAML structure for GitHub organization profile configuration. -// It contains organization-specific settings such as the primary branch name used for -// change detection and analysis. -type GithubOrganisationYAML struct { - PrimaryBranchName string `yaml:"primary_branch_name"` -} - -// SignalConfigYAML represents the root YAML structure for signal configuration files. -// It can contain either or both of: -// - RoutineChangesConfig: Configuration for routine change detection -// - GithubOrganisationProfile: GitHub organization-specific settings -// At least one section must be provided in the YAML file. -type SignalConfigYAML struct { - RoutineChangesConfig *RoutineChangesYAML `yaml:"routine_changes_config,omitempty"` - GithubOrganisationProfile *GithubOrganisationYAML `yaml:"github_organisation_profile,omitempty"` -} - -// SignalConfigFile represents the internal, parsed signal configuration structure. -// This is the converted form of SignalConfigYAML, where YAML-specific types are -// transformed into their corresponding protocol buffer types for use in the application. -type SignalConfigFile struct { - RoutineChangesConfig *RoutineChangesConfig - GithubOrganisationProfile *GithubOrganisationProfile -} - -// YamlStringToSignalConfig parses a YAML string containing signal configuration and converts it -// into a SignalConfigFile. It validates that at least one configuration section is provided -// and performs validation on the routine changes configuration if present. -// -// The function handles conversion from YAML-friendly types (e.g., float32 for durations) -// to the internal protocol buffer types (e.g., RoutineChangesConfig with unit specifications). -// -// Returns an error if: -// - The YAML is invalid or cannot be unmarshaled -// - No configuration sections are provided -// - Routine changes configuration validation fails -func YamlStringToSignalConfig(yamlString string) (*SignalConfigFile, error) { - var signalConfigYAML SignalConfigYAML - err := yaml.Unmarshal([]byte(yamlString), &signalConfigYAML) - if err != nil { - return nil, fmt.Errorf("error unmarshalling yaml to signal config: %w", err) - } - - // check that at least one section is provided - if signalConfigYAML.RoutineChangesConfig == nil && signalConfigYAML.GithubOrganisationProfile == nil { - return nil, fmt.Errorf("signal config file must contain at least one of: routine_changes_config or github_organisation_profile") - } - - // validate the routine changes config - if signalConfigYAML.RoutineChangesConfig != nil { - if err := validateRoutineChangesConfig(signalConfigYAML.RoutineChangesConfig); err != nil { - return nil, err - } - } - - var routineCfg *RoutineChangesConfig - if signalConfigYAML.RoutineChangesConfig != nil { - routineCfg = &RoutineChangesConfig{ - Sensitivity: signalConfigYAML.RoutineChangesConfig.Sensitivity, - EventsPer: signalConfigYAML.RoutineChangesConfig.EventsPerDay, - EventsPerUnit: RoutineChangesConfig_DAYS, - Duration: signalConfigYAML.RoutineChangesConfig.DurationInDays, - DurationUnit: RoutineChangesConfig_DAYS, - } - } - - var githubProfile *GithubOrganisationProfile - if signalConfigYAML.GithubOrganisationProfile != nil { - githubProfile = &GithubOrganisationProfile{ - PrimaryBranchName: signalConfigYAML.GithubOrganisationProfile.PrimaryBranchName, - } - } - - signalConfigFile := &SignalConfigFile{ - RoutineChangesConfig: routineCfg, - GithubOrganisationProfile: githubProfile, - } - return signalConfigFile, nil -} - -// validateRoutineChangesConfig validates the routine changes configuration values. -// It ensures that: -// - EventsPerDay is at least 1 -// - DurationInDays is at least 1 -// - Sensitivity is 0 or higher -// -// Returns an error with a descriptive message if any validation fails. -func validateRoutineChangesConfig(routineChangesConfigYAML *RoutineChangesYAML) error { - if routineChangesConfigYAML.EventsPerDay < 1 { - return fmt.Errorf("events_per_day must be greater than 1, got %v", routineChangesConfigYAML.EventsPerDay) - } - if routineChangesConfigYAML.DurationInDays < 1 { - return fmt.Errorf("duration_in_days must be greater than 1, got %v", routineChangesConfigYAML.DurationInDays) - } - if routineChangesConfigYAML.Sensitivity < 0 { - return fmt.Errorf("sensitivity must be 0 or higher, got %v", routineChangesConfigYAML.Sensitivity) - } - return nil -} - -// TimelineEntryContentDescription returns a human-readable description of the -// entry's content based on its type. -func TimelineEntryContentDescription(entry *ChangeTimelineEntryV2) string { - switch c := entry.GetContent().(type) { - case *ChangeTimelineEntryV2_MappedItems: - return fmt.Sprintf("%d mapped items", len(c.MappedItems.GetMappedItems())) - case *ChangeTimelineEntryV2_CalculatedBlastRadius: - return fmt.Sprintf("%d items, %d edges", c.CalculatedBlastRadius.GetNumItems(), c.CalculatedBlastRadius.GetNumEdges()) - case *ChangeTimelineEntryV2_CalculatedRisks: - return fmt.Sprintf("%d risks", len(c.CalculatedRisks.GetRisks())) - case *ChangeTimelineEntryV2_CalculatedLabels: - return fmt.Sprintf("%d labels", len(c.CalculatedLabels.GetLabels())) - case *ChangeTimelineEntryV2_ChangeValidation: - return fmt.Sprintf("%d validation categories", len(c.ChangeValidation.GetValidationChecklist())) - case *ChangeTimelineEntryV2_FormHypotheses: - return fmt.Sprintf("%d hypotheses", c.FormHypotheses.GetNumHypotheses()) - case *ChangeTimelineEntryV2_InvestigateHypotheses: - return fmt.Sprintf("%d proven, %d disproven, %d investigating", - c.InvestigateHypotheses.GetNumProven(), - c.InvestigateHypotheses.GetNumDisproven(), - c.InvestigateHypotheses.GetNumInvestigating()) - case *ChangeTimelineEntryV2_RecordObservations: - return fmt.Sprintf("%d observations", c.RecordObservations.GetNumObservations()) - case *ChangeTimelineEntryV2_Error: - return c.Error - case *ChangeTimelineEntryV2_StatusMessage: - return c.StatusMessage - case *ChangeTimelineEntryV2_Empty, nil: - return "" - default: - return "" - } -} - -// TimelineFindInProgressEntry returns the current running entry in the list of entries -// The function handles the following cases: -// - If the input slice is nil or empty, it returns an error. -// - The first entry that has a status of IN_PROGRESS, PENDING, or ERROR, it returns the entry's name, content description, status, and a nil error. -// - If an entry has an unknown status, it returns an error. -// - If the timeline is complete it returns an empty string, empty content description, DONE status, and a nil error. -func TimelineFindInProgressEntry(entries []*ChangeTimelineEntryV2) (string, string, ChangeTimelineEntryStatus, error) { - if entries == nil { - return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, errors.New("entries is nil") - } - if len(entries) == 0 { - return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, errors.New("entries is empty") - } - - for _, entry := range entries { - switch entry.GetStatus() { - case ChangeTimelineEntryStatus_IN_PROGRESS, ChangeTimelineEntryStatus_PENDING, ChangeTimelineEntryStatus_ERROR: - // if the entry is in progress or about to start, or has an error(to be retried) - return entry.GetName(), TimelineEntryContentDescription(entry), entry.GetStatus(), nil - case ChangeTimelineEntryStatus_UNSPECIFIED, ChangeTimelineEntryStatus_DONE: - // do nothing - default: - return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, fmt.Errorf("unknown status: %s", entry.GetStatus().String()) - } - } - - return "", "", ChangeTimelineEntryStatus_DONE, nil -} diff --git a/sdp-go/changes.pb.go b/sdp-go/changes.pb.go deleted file mode 100644 index 41245de2..00000000 --- a/sdp-go/changes.pb.go +++ /dev/null @@ -1,7252 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: changes.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = 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 - MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION MappedItemTimelineStatus = 4 -) - -// 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", - 4: "MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION", - } - 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, - "MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION": 4, - } -) - -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} -} - -// Explicit mapping status from CLI - allows CLI to communicate state instead of API inferring -type MappedItemMappingStatus int32 - -const ( - MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED MappedItemMappingStatus = 0 - MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_SUCCESS MappedItemMappingStatus = 1 - MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED MappedItemMappingStatus = 2 - MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION MappedItemMappingStatus = 3 - MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR MappedItemMappingStatus = 4 -) - -// Enum value maps for MappedItemMappingStatus. -var ( - MappedItemMappingStatus_name = map[int32]string{ - 0: "MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED", - 1: "MAPPED_ITEM_MAPPING_STATUS_SUCCESS", - 2: "MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED", - 3: "MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION", - 4: "MAPPED_ITEM_MAPPING_STATUS_ERROR", - } - MappedItemMappingStatus_value = map[string]int32{ - "MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED": 0, - "MAPPED_ITEM_MAPPING_STATUS_SUCCESS": 1, - "MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED": 2, - "MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION": 3, - "MAPPED_ITEM_MAPPING_STATUS_ERROR": 4, - } -) - -func (x MappedItemMappingStatus) Enum() *MappedItemMappingStatus { - p := new(MappedItemMappingStatus) - *p = x - return p -} - -func (x MappedItemMappingStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (MappedItemMappingStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[1].Descriptor() -} - -func (MappedItemMappingStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[1] -} - -func (x MappedItemMappingStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use MappedItemMappingStatus.Descriptor instead. -func (MappedItemMappingStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{1} -} - -type HypothesisStatus int32 - -const ( - HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED HypothesisStatus = 0 - // The hypothesis is being formed, the detail may change as more observations - // are recorded - HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_FORMING HypothesisStatus = 1 - // The hypotheses is being investigated, the detail will be available once the - // investigation is complete - HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING HypothesisStatus = 2 - // They hypothesis has been proven to be a risk - HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_PROVEN HypothesisStatus = 3 - // The hypothesis has been disproven, no risk has been found - HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN HypothesisStatus = 4 -) - -// Enum value maps for HypothesisStatus. -var ( - HypothesisStatus_name = map[int32]string{ - 0: "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED", - 1: "INVESTIGATED_HYPOTHESIS_STATUS_FORMING", - 2: "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING", - 3: "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN", - 4: "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN", - } - HypothesisStatus_value = map[string]int32{ - "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED": 0, - "INVESTIGATED_HYPOTHESIS_STATUS_FORMING": 1, - "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING": 2, - "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN": 3, - "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN": 4, - } -) - -func (x HypothesisStatus) Enum() *HypothesisStatus { - p := new(HypothesisStatus) - *p = x - return p -} - -func (x HypothesisStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (HypothesisStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[2].Descriptor() -} - -func (HypothesisStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[2] -} - -func (x HypothesisStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use HypothesisStatus.Descriptor instead. -func (HypothesisStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{2} -} - -type ChangeTimelineEntryStatus int32 - -const ( - // This should never be used, it is the default value - ChangeTimelineEntryStatus_UNSPECIFIED ChangeTimelineEntryStatus = 0 - // This step has not yet started - ChangeTimelineEntryStatus_PENDING ChangeTimelineEntryStatus = 1 - // This step is currently happening - ChangeTimelineEntryStatus_IN_PROGRESS ChangeTimelineEntryStatus = 2 - // The step is completed - ChangeTimelineEntryStatus_DONE ChangeTimelineEntryStatus = 3 - // The step has an error and cannot be completed - ChangeTimelineEntryStatus_ERROR ChangeTimelineEntryStatus = 4 -) - -// Enum value maps for ChangeTimelineEntryStatus. -var ( - ChangeTimelineEntryStatus_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "PENDING", - 2: "IN_PROGRESS", - 3: "DONE", - 4: "ERROR", - } - ChangeTimelineEntryStatus_value = map[string]int32{ - "UNSPECIFIED": 0, - "PENDING": 1, - "IN_PROGRESS": 2, - "DONE": 3, - "ERROR": 4, - } -) - -func (x ChangeTimelineEntryStatus) Enum() *ChangeTimelineEntryStatus { - p := new(ChangeTimelineEntryStatus) - *p = x - return p -} - -func (x ChangeTimelineEntryStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChangeTimelineEntryStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[3].Descriptor() -} - -func (ChangeTimelineEntryStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[3] -} - -func (x ChangeTimelineEntryStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChangeTimelineEntryStatus.Descriptor instead. -func (ChangeTimelineEntryStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{3} -} - -type ItemDiffStatus int32 - -const ( - ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED ItemDiffStatus = 0 - ItemDiffStatus_ITEM_DIFF_STATUS_UNCHANGED ItemDiffStatus = 1 - ItemDiffStatus_ITEM_DIFF_STATUS_CREATED ItemDiffStatus = 2 - ItemDiffStatus_ITEM_DIFF_STATUS_UPDATED ItemDiffStatus = 3 - ItemDiffStatus_ITEM_DIFF_STATUS_DELETED ItemDiffStatus = 4 - ItemDiffStatus_ITEM_DIFF_STATUS_REPLACED ItemDiffStatus = 5 -) - -// Enum value maps for ItemDiffStatus. -var ( - ItemDiffStatus_name = map[int32]string{ - 0: "ITEM_DIFF_STATUS_UNSPECIFIED", - 1: "ITEM_DIFF_STATUS_UNCHANGED", - 2: "ITEM_DIFF_STATUS_CREATED", - 3: "ITEM_DIFF_STATUS_UPDATED", - 4: "ITEM_DIFF_STATUS_DELETED", - 5: "ITEM_DIFF_STATUS_REPLACED", - } - ItemDiffStatus_value = map[string]int32{ - "ITEM_DIFF_STATUS_UNSPECIFIED": 0, - "ITEM_DIFF_STATUS_UNCHANGED": 1, - "ITEM_DIFF_STATUS_CREATED": 2, - "ITEM_DIFF_STATUS_UPDATED": 3, - "ITEM_DIFF_STATUS_DELETED": 4, - "ITEM_DIFF_STATUS_REPLACED": 5, - } -) - -func (x ItemDiffStatus) Enum() *ItemDiffStatus { - p := new(ItemDiffStatus) - *p = x - return p -} - -func (x ItemDiffStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ItemDiffStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[4].Descriptor() -} - -func (ItemDiffStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[4] -} - -func (x ItemDiffStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ItemDiffStatus.Descriptor instead. -func (ItemDiffStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{4} -} - -type ChangeOutputFormat int32 - -const ( - ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED ChangeOutputFormat = 0 - ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_JSON ChangeOutputFormat = 1 - ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_MARKDOWN ChangeOutputFormat = 2 -) - -// Enum value maps for ChangeOutputFormat. -var ( - ChangeOutputFormat_name = map[int32]string{ - 0: "CHANGE_OUTPUT_FORMAT_UNSPECIFIED", - 1: "CHANGE_OUTPUT_FORMAT_JSON", - 2: "CHANGE_OUTPUT_FORMAT_MARKDOWN", - } - ChangeOutputFormat_value = map[string]int32{ - "CHANGE_OUTPUT_FORMAT_UNSPECIFIED": 0, - "CHANGE_OUTPUT_FORMAT_JSON": 1, - "CHANGE_OUTPUT_FORMAT_MARKDOWN": 2, - } -) - -func (x ChangeOutputFormat) Enum() *ChangeOutputFormat { - p := new(ChangeOutputFormat) - *p = x - return p -} - -func (x ChangeOutputFormat) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChangeOutputFormat) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[5].Descriptor() -} - -func (ChangeOutputFormat) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[5] -} - -func (x ChangeOutputFormat) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChangeOutputFormat.Descriptor instead. -func (ChangeOutputFormat) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{5} -} - -type LabelType int32 - -const ( - LabelType_LABEL_TYPE_UNSPECIFIED LabelType = 0 - LabelType_LABEL_TYPE_AUTO LabelType = 1 - LabelType_LABEL_TYPE_USER LabelType = 2 -) - -// Enum value maps for LabelType. -var ( - LabelType_name = map[int32]string{ - 0: "LABEL_TYPE_UNSPECIFIED", - 1: "LABEL_TYPE_AUTO", - 2: "LABEL_TYPE_USER", - } - LabelType_value = map[string]int32{ - "LABEL_TYPE_UNSPECIFIED": 0, - "LABEL_TYPE_AUTO": 1, - "LABEL_TYPE_USER": 2, - } -) - -func (x LabelType) Enum() *LabelType { - p := new(LabelType) - *p = x - return p -} - -func (x LabelType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (LabelType) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[6].Descriptor() -} - -func (LabelType) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[6] -} - -func (x LabelType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use LabelType.Descriptor instead. -func (LabelType) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{6} -} - -type ChangeStatus int32 - -const ( - // Reserved for truly unspecified states. Should not be used for newly created changes. - ChangeStatus_CHANGE_STATUS_UNSPECIFIED ChangeStatus = 0 - // The change has been created and is ready for change analysis to be started. - // Or change analysis is in progress. - // Or change analysis is complete and the change is ready to be started. - ChangeStatus_CHANGE_STATUS_DEFINING ChangeStatus = 1 - // The change is in progress or deployment is in progress. The change can be ended using the `EndChange` - // RPC. - ChangeStatus_CHANGE_STATUS_HAPPENING ChangeStatus = 2 - // DEPRECATED: This status is no longer used and should not be used in new code. - // It will be removed in a future version. Use other appropriate status values instead. - // Deprecated as part of https://linear.app/overmind/issue/ENG-1520/change-status-processing-is-no-longer-used-in-a-meaningful-way - // - // Deprecated: Marked as deprecated in changes.proto. - ChangeStatus_CHANGE_STATUS_PROCESSING ChangeStatus = 3 - // The change has been ended and the results have been processed. - ChangeStatus_CHANGE_STATUS_DONE ChangeStatus = 4 -) - -// Enum value maps for ChangeStatus. -var ( - ChangeStatus_name = map[int32]string{ - 0: "CHANGE_STATUS_UNSPECIFIED", - 1: "CHANGE_STATUS_DEFINING", - 2: "CHANGE_STATUS_HAPPENING", - 3: "CHANGE_STATUS_PROCESSING", - 4: "CHANGE_STATUS_DONE", - } - ChangeStatus_value = map[string]int32{ - "CHANGE_STATUS_UNSPECIFIED": 0, - "CHANGE_STATUS_DEFINING": 1, - "CHANGE_STATUS_HAPPENING": 2, - "CHANGE_STATUS_PROCESSING": 3, - "CHANGE_STATUS_DONE": 4, - } -) - -func (x ChangeStatus) Enum() *ChangeStatus { - p := new(ChangeStatus) - *p = x - return p -} - -func (x ChangeStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChangeStatus) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[7].Descriptor() -} - -func (ChangeStatus) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[7] -} - -func (x ChangeStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChangeStatus.Descriptor instead. -func (ChangeStatus) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{7} -} - -type StartChangeResponse_State int32 - -const ( - // No state has been specified - StartChangeResponse_STATE_UNSPECIFIED StartChangeResponse_State = 0 - // Snapshot is being taken - StartChangeResponse_STATE_TAKING_SNAPSHOT StartChangeResponse_State = 1 - // Snapshot is being saved - StartChangeResponse_STATE_SAVING_SNAPSHOT StartChangeResponse_State = 2 - // Everything is complete - StartChangeResponse_STATE_DONE StartChangeResponse_State = 3 -) - -// Enum value maps for StartChangeResponse_State. -var ( - StartChangeResponse_State_name = map[int32]string{ - 0: "STATE_UNSPECIFIED", - 1: "STATE_TAKING_SNAPSHOT", - 2: "STATE_SAVING_SNAPSHOT", - 3: "STATE_DONE", - } - StartChangeResponse_State_value = map[string]int32{ - "STATE_UNSPECIFIED": 0, - "STATE_TAKING_SNAPSHOT": 1, - "STATE_SAVING_SNAPSHOT": 2, - "STATE_DONE": 3, - } -) - -func (x StartChangeResponse_State) Enum() *StartChangeResponse_State { - p := new(StartChangeResponse_State) - *p = x - return p -} - -func (x StartChangeResponse_State) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (StartChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[8].Descriptor() -} - -func (StartChangeResponse_State) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[8] -} - -func (x StartChangeResponse_State) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use StartChangeResponse_State.Descriptor instead. -func (StartChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84, 0} -} - -type EndChangeResponse_State int32 - -const ( - // No state has been specified - EndChangeResponse_STATE_UNSPECIFIED EndChangeResponse_State = 0 - // Snapshot is being taken - EndChangeResponse_STATE_TAKING_SNAPSHOT EndChangeResponse_State = 1 - // Snapshot is being saved - EndChangeResponse_STATE_SAVING_SNAPSHOT EndChangeResponse_State = 2 - // Everything is complete - EndChangeResponse_STATE_DONE EndChangeResponse_State = 3 -) - -// Enum value maps for EndChangeResponse_State. -var ( - EndChangeResponse_State_name = map[int32]string{ - 0: "STATE_UNSPECIFIED", - 1: "STATE_TAKING_SNAPSHOT", - 2: "STATE_SAVING_SNAPSHOT", - 3: "STATE_DONE", - } - EndChangeResponse_State_value = map[string]int32{ - "STATE_UNSPECIFIED": 0, - "STATE_TAKING_SNAPSHOT": 1, - "STATE_SAVING_SNAPSHOT": 2, - "STATE_DONE": 3, - } -) - -func (x EndChangeResponse_State) Enum() *EndChangeResponse_State { - p := new(EndChangeResponse_State) - *p = x - return p -} - -func (x EndChangeResponse_State) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (EndChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[9].Descriptor() -} - -func (EndChangeResponse_State) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[9] -} - -func (x EndChangeResponse_State) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use EndChangeResponse_State.Descriptor instead. -func (EndChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86, 0} -} - -type Risk_Severity int32 - -const ( - Risk_SEVERITY_UNSPECIFIED Risk_Severity = 0 - Risk_SEVERITY_LOW Risk_Severity = 1 - Risk_SEVERITY_MEDIUM Risk_Severity = 2 - Risk_SEVERITY_HIGH Risk_Severity = 3 -) - -// Enum value maps for Risk_Severity. -var ( - Risk_Severity_name = map[int32]string{ - 0: "SEVERITY_UNSPECIFIED", - 1: "SEVERITY_LOW", - 2: "SEVERITY_MEDIUM", - 3: "SEVERITY_HIGH", - } - Risk_Severity_value = map[string]int32{ - "SEVERITY_UNSPECIFIED": 0, - "SEVERITY_LOW": 1, - "SEVERITY_MEDIUM": 2, - "SEVERITY_HIGH": 3, - } -) - -func (x Risk_Severity) Enum() *Risk_Severity { - p := new(Risk_Severity) - *p = x - return p -} - -func (x Risk_Severity) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Risk_Severity) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[10].Descriptor() -} - -func (Risk_Severity) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[10] -} - -func (x Risk_Severity) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Risk_Severity.Descriptor instead. -func (Risk_Severity) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89, 0} -} - -type ChangeAnalysisStatus_Status int32 - -const ( - ChangeAnalysisStatus_STATUS_UNSPECIFIED ChangeAnalysisStatus_Status = 0 - ChangeAnalysisStatus_STATUS_INPROGRESS ChangeAnalysisStatus_Status = 1 - ChangeAnalysisStatus_STATUS_SKIPPED ChangeAnalysisStatus_Status = 2 - ChangeAnalysisStatus_STATUS_DONE ChangeAnalysisStatus_Status = 3 - ChangeAnalysisStatus_STATUS_ERROR ChangeAnalysisStatus_Status = 4 -) - -// Enum value maps for ChangeAnalysisStatus_Status. -var ( - ChangeAnalysisStatus_Status_name = map[int32]string{ - 0: "STATUS_UNSPECIFIED", - 1: "STATUS_INPROGRESS", - 2: "STATUS_SKIPPED", - 3: "STATUS_DONE", - 4: "STATUS_ERROR", - } - ChangeAnalysisStatus_Status_value = map[string]int32{ - "STATUS_UNSPECIFIED": 0, - "STATUS_INPROGRESS": 1, - "STATUS_SKIPPED": 2, - "STATUS_DONE": 3, - "STATUS_ERROR": 4, - } -) - -func (x ChangeAnalysisStatus_Status) Enum() *ChangeAnalysisStatus_Status { - p := new(ChangeAnalysisStatus_Status) - *p = x - return p -} - -func (x ChangeAnalysisStatus_Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ChangeAnalysisStatus_Status) Descriptor() protoreflect.EnumDescriptor { - return file_changes_proto_enumTypes[11].Descriptor() -} - -func (ChangeAnalysisStatus_Status) Type() protoreflect.EnumType { - return &file_changes_proto_enumTypes[11] -} - -func (x ChangeAnalysisStatus_Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ChangeAnalysisStatus_Status.Descriptor instead. -func (ChangeAnalysisStatus_Status) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90, 0} -} - -type LabelRule struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *LabelRuleMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *LabelRuleProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LabelRule) Reset() { - *x = LabelRule{} - mi := &file_changes_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LabelRule) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LabelRule) ProtoMessage() {} - -func (x *LabelRule) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[0] - 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 LabelRule.ProtoReflect.Descriptor instead. -func (*LabelRule) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{0} -} - -func (x *LabelRule) GetMetadata() *LabelRuleMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *LabelRule) GetProperties() *LabelRuleProperties { - if x != nil { - return x.Properties - } - return nil -} - -type LabelRuleMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The unique identifier for this rule, it is required - LabelRuleUUID []byte `protobuf:"bytes,1,opt,name=LabelRuleUUID,proto3" json:"LabelRuleUUID,omitempty"` - // The time that this rule was created, set to the current time when the rule is created - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - // The time the rule was last updated, set to the current time when the rule is created - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LabelRuleMetadata) Reset() { - *x = LabelRuleMetadata{} - mi := &file_changes_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LabelRuleMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LabelRuleMetadata) ProtoMessage() {} - -func (x *LabelRuleMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[1] - 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 LabelRuleMetadata.ProtoReflect.Descriptor instead. -func (*LabelRuleMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{1} -} - -func (x *LabelRuleMetadata) GetLabelRuleUUID() []byte { - if x != nil { - return x.LabelRuleUUID - } - return nil -} - -func (x *LabelRuleMetadata) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *LabelRuleMetadata) GetUpdatedAt() *timestamppb.Timestamp { - if x != nil { - return x.UpdatedAt - } - return nil -} - -type LabelRuleProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the rule, friendly for users, it is required - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // The colour of the label, it is required - Colour string `protobuf:"bytes,2,opt,name=colour,proto3" json:"colour,omitempty"` - // The instructions for the rule, this is the logic that will be used to determine if the label should be applied to a change, it is required - Instructions string `protobuf:"bytes,3,opt,name=instructions,proto3" json:"instructions,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LabelRuleProperties) Reset() { - *x = LabelRuleProperties{} - mi := &file_changes_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LabelRuleProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LabelRuleProperties) ProtoMessage() {} - -func (x *LabelRuleProperties) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[2] - 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 LabelRuleProperties.ProtoReflect.Descriptor instead. -func (*LabelRuleProperties) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{2} -} - -func (x *LabelRuleProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *LabelRuleProperties) GetColour() string { - if x != nil { - return x.Colour - } - return "" -} - -func (x *LabelRuleProperties) GetInstructions() string { - if x != nil { - return x.Instructions - } - return "" -} - -type ListLabelRulesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListLabelRulesRequest) Reset() { - *x = ListLabelRulesRequest{} - mi := &file_changes_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListLabelRulesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListLabelRulesRequest) ProtoMessage() {} - -func (x *ListLabelRulesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[3] - 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 ListLabelRulesRequest.ProtoReflect.Descriptor instead. -func (*ListLabelRulesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{3} -} - -type ListLabelRulesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rules []*LabelRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListLabelRulesResponse) Reset() { - *x = ListLabelRulesResponse{} - mi := &file_changes_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListLabelRulesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListLabelRulesResponse) ProtoMessage() {} - -func (x *ListLabelRulesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[4] - 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 ListLabelRulesResponse.ProtoReflect.Descriptor instead. -func (*ListLabelRulesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{4} -} - -func (x *ListLabelRulesResponse) GetRules() []*LabelRule { - if x != nil { - return x.Rules - } - return nil -} - -type CreateLabelRuleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *LabelRuleProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateLabelRuleRequest) Reset() { - *x = CreateLabelRuleRequest{} - mi := &file_changes_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateLabelRuleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateLabelRuleRequest) ProtoMessage() {} - -func (x *CreateLabelRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[5] - 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 CreateLabelRuleRequest.ProtoReflect.Descriptor instead. -func (*CreateLabelRuleRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{5} -} - -func (x *CreateLabelRuleRequest) GetProperties() *LabelRuleProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateLabelRuleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateLabelRuleResponse) Reset() { - *x = CreateLabelRuleResponse{} - mi := &file_changes_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateLabelRuleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateLabelRuleResponse) ProtoMessage() {} - -func (x *CreateLabelRuleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[6] - 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 CreateLabelRuleResponse.ProtoReflect.Descriptor instead. -func (*CreateLabelRuleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{6} -} - -func (x *CreateLabelRuleResponse) GetRule() *LabelRule { - if x != nil { - return x.Rule - } - return nil -} - -type GetLabelRuleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetLabelRuleRequest) Reset() { - *x = GetLabelRuleRequest{} - mi := &file_changes_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetLabelRuleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetLabelRuleRequest) ProtoMessage() {} - -func (x *GetLabelRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[7] - 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 GetLabelRuleRequest.ProtoReflect.Descriptor instead. -func (*GetLabelRuleRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{7} -} - -func (x *GetLabelRuleRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type GetLabelRuleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetLabelRuleResponse) Reset() { - *x = GetLabelRuleResponse{} - mi := &file_changes_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetLabelRuleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetLabelRuleResponse) ProtoMessage() {} - -func (x *GetLabelRuleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[8] - 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 GetLabelRuleResponse.ProtoReflect.Descriptor instead. -func (*GetLabelRuleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{8} -} - -func (x *GetLabelRuleResponse) GetRule() *LabelRule { - if x != nil { - return x.Rule - } - return nil -} - -type UpdateLabelRuleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - Properties *LabelRuleProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateLabelRuleRequest) Reset() { - *x = UpdateLabelRuleRequest{} - mi := &file_changes_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateLabelRuleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateLabelRuleRequest) ProtoMessage() {} - -func (x *UpdateLabelRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[9] - 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 UpdateLabelRuleRequest.ProtoReflect.Descriptor instead. -func (*UpdateLabelRuleRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{9} -} - -func (x *UpdateLabelRuleRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *UpdateLabelRuleRequest) GetProperties() *LabelRuleProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateLabelRuleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateLabelRuleResponse) Reset() { - *x = UpdateLabelRuleResponse{} - mi := &file_changes_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateLabelRuleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateLabelRuleResponse) ProtoMessage() {} - -func (x *UpdateLabelRuleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[10] - 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 UpdateLabelRuleResponse.ProtoReflect.Descriptor instead. -func (*UpdateLabelRuleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{10} -} - -func (x *UpdateLabelRuleResponse) GetRule() *LabelRule { - if x != nil { - return x.Rule - } - return nil -} - -type DeleteLabelRuleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteLabelRuleRequest) Reset() { - *x = DeleteLabelRuleRequest{} - mi := &file_changes_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteLabelRuleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteLabelRuleRequest) ProtoMessage() {} - -func (x *DeleteLabelRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[11] - 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 DeleteLabelRuleRequest.ProtoReflect.Descriptor instead. -func (*DeleteLabelRuleRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{11} -} - -func (x *DeleteLabelRuleRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type DeleteLabelRuleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteLabelRuleResponse) Reset() { - *x = DeleteLabelRuleResponse{} - mi := &file_changes_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteLabelRuleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteLabelRuleResponse) ProtoMessage() {} - -func (x *DeleteLabelRuleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[12] - 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 DeleteLabelRuleResponse.ProtoReflect.Descriptor instead. -func (*DeleteLabelRuleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{12} -} - -type TestLabelRuleRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *LabelRuleProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - ChangeUUID [][]byte `protobuf:"bytes,2,rep,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TestLabelRuleRequest) Reset() { - *x = TestLabelRuleRequest{} - mi := &file_changes_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TestLabelRuleRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TestLabelRuleRequest) ProtoMessage() {} - -func (x *TestLabelRuleRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TestLabelRuleRequest.ProtoReflect.Descriptor instead. -func (*TestLabelRuleRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{13} -} - -func (x *TestLabelRuleRequest) GetProperties() *LabelRuleProperties { - if x != nil { - return x.Properties - } - return nil -} - -func (x *TestLabelRuleRequest) GetChangeUUID() [][]byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type TestLabelRuleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - Applied bool `protobuf:"varint,2,opt,name=applied,proto3" json:"applied,omitempty"` - Label *Label `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TestLabelRuleResponse) Reset() { - *x = TestLabelRuleResponse{} - mi := &file_changes_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TestLabelRuleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TestLabelRuleResponse) ProtoMessage() {} - -func (x *TestLabelRuleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TestLabelRuleResponse.ProtoReflect.Descriptor instead. -func (*TestLabelRuleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{14} -} - -func (x *TestLabelRuleResponse) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -func (x *TestLabelRuleResponse) GetApplied() bool { - if x != nil { - return x.Applied - } - return false -} - -func (x *TestLabelRuleResponse) GetLabel() *Label { - if x != nil { - return x.Label - } - return nil -} - -type ReapplyLabelRuleInTimeRangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - StartAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=startAt,proto3" json:"startAt,omitempty"` - EndAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=endAt,proto3" json:"endAt,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ReapplyLabelRuleInTimeRangeRequest) Reset() { - *x = ReapplyLabelRuleInTimeRangeRequest{} - mi := &file_changes_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ReapplyLabelRuleInTimeRangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ReapplyLabelRuleInTimeRangeRequest) ProtoMessage() {} - -func (x *ReapplyLabelRuleInTimeRangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[15] - 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 ReapplyLabelRuleInTimeRangeRequest.ProtoReflect.Descriptor instead. -func (*ReapplyLabelRuleInTimeRangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{15} -} - -func (x *ReapplyLabelRuleInTimeRangeRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *ReapplyLabelRuleInTimeRangeRequest) GetStartAt() *timestamppb.Timestamp { - if x != nil { - return x.StartAt - } - return nil -} - -func (x *ReapplyLabelRuleInTimeRangeRequest) GetEndAt() *timestamppb.Timestamp { - if x != nil { - return x.EndAt - } - return nil -} - -type ReapplyLabelRuleInTimeRangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID [][]byte `protobuf:"bytes,1,rep,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ReapplyLabelRuleInTimeRangeResponse) Reset() { - *x = ReapplyLabelRuleInTimeRangeResponse{} - mi := &file_changes_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ReapplyLabelRuleInTimeRangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ReapplyLabelRuleInTimeRangeResponse) ProtoMessage() {} - -func (x *ReapplyLabelRuleInTimeRangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[16] - 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 ReapplyLabelRuleInTimeRangeResponse.ProtoReflect.Descriptor instead. -func (*ReapplyLabelRuleInTimeRangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{16} -} - -func (x *ReapplyLabelRuleInTimeRangeResponse) GetChangeUUID() [][]byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetHypothesesDetailsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHypothesesDetailsRequest) Reset() { - *x = GetHypothesesDetailsRequest{} - mi := &file_changes_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHypothesesDetailsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHypothesesDetailsRequest) ProtoMessage() {} - -func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetHypothesesDetailsRequest.ProtoReflect.Descriptor instead. -func (*GetHypothesesDetailsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{17} -} - -func (x *GetHypothesesDetailsRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetHypothesesDetailsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Hypotheses []*HypothesesDetails `protobuf:"bytes,1,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHypothesesDetailsResponse) Reset() { - *x = GetHypothesesDetailsResponse{} - mi := &file_changes_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHypothesesDetailsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHypothesesDetailsResponse) ProtoMessage() {} - -func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetHypothesesDetailsResponse.ProtoReflect.Descriptor instead. -func (*GetHypothesesDetailsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{18} -} - -func (x *GetHypothesesDetailsResponse) GetHypotheses() []*HypothesesDetails { - if x != nil { - return x.Hypotheses - } - return nil -} - -type HypothesesDetails struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The title of the hypothesis - Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` - // The number of observations that were combined to form the hypothesis - NumObservations uint32 `protobuf:"varint,2,opt,name=numObservations,proto3" json:"numObservations,omitempty"` - // The detail of the hypothesis - Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` - // The status of the hypothesis - Status HypothesisStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` - // The results of the investigation of the hypothesis - InvestigationResults string `protobuf:"bytes,5,opt,name=investigationResults,proto3" json:"investigationResults,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HypothesesDetails) Reset() { - *x = HypothesesDetails{} - mi := &file_changes_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HypothesesDetails) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HypothesesDetails) ProtoMessage() {} - -func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { - mi := &file_changes_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 HypothesesDetails.ProtoReflect.Descriptor instead. -func (*HypothesesDetails) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{19} -} - -func (x *HypothesesDetails) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *HypothesesDetails) GetNumObservations() uint32 { - if x != nil { - return x.NumObservations - } - return 0 -} - -func (x *HypothesesDetails) GetDetail() string { - if x != nil { - return x.Detail - } - return "" -} - -func (x *HypothesesDetails) GetStatus() HypothesisStatus { - if x != nil { - return x.Status - } - return HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED -} - -func (x *HypothesesDetails) GetInvestigationResults() string { - if x != nil { - return x.InvestigationResults - } - return "" -} - -type GetChangeTimelineV2Request struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeTimelineV2Request) Reset() { - *x = GetChangeTimelineV2Request{} - mi := &file_changes_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeTimelineV2Request) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeTimelineV2Request) ProtoMessage() {} - -func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[20] - 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 GetChangeTimelineV2Request.ProtoReflect.Descriptor instead. -func (*GetChangeTimelineV2Request) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{20} -} - -func (x *GetChangeTimelineV2Request) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetChangeTimelineV2Response struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The entries of this timeline, in chronological order (oldest first) - Entries []*ChangeTimelineEntryV2 `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeTimelineV2Response) Reset() { - *x = GetChangeTimelineV2Response{} - mi := &file_changes_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeTimelineV2Response) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeTimelineV2Response) ProtoMessage() {} - -func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[21] - 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 GetChangeTimelineV2Response.ProtoReflect.Descriptor instead. -func (*GetChangeTimelineV2Response) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{21} -} - -func (x *GetChangeTimelineV2Response) GetEntries() []*ChangeTimelineEntryV2 { - if x != nil { - return x.Entries - } - return nil -} - -// Contains all the information about a step in the Change Analysis workflow -// to show the user the historical, current and future of this Change. -// to show the user the historical, current and future. -type ChangeTimelineEntryV2 struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of this step, this will be shown to the user - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // The little icon that will be shown for the timeline entry to indicate - // it's status - Status ChangeTimelineEntryStatus `protobuf:"varint,2,opt,name=status,proto3,enum=changes.ChangeTimelineEntryStatus" json:"status,omitempty"` - // The time that this step started, this will be used to calculate the - // duration this step. If `startedAt` is set, but `endedAt` is not, then the - // step is currently in progress. If `startedAt` is not set, the step is still - // pending. - StartedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=startedAt,proto3,oneof" json:"startedAt,omitempty"` - // When this step ended, this allows us to calculate how long it took, and - // allows users to see when certain things happened when looking back. If - // `endedAt` is set but `startedAt` is not, or `endedAt` is before `startedAt` - // then the step will be considered done, but we will be unable to calculate - // the duration. If `startedAt` and `endedAt` are the same timestamp, or only - // `endedAt` is populated, this entry does not have a duration, but should be - // interpreted as a point-in-time event. - EndedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=endedAt,proto3,oneof" json:"endedAt,omitempty"` - // Who performed this step, this will be shown to the user - Actor *string `protobuf:"bytes,5,opt,name=actor,proto3,oneof" json:"actor,omitempty"` - // The actual content of this step. This will be displayed to the user - // within the timeline and rendered differently depending on the type - // - // Types that are valid to be assigned to Content: - // - // *ChangeTimelineEntryV2_MappedItems - // *ChangeTimelineEntryV2_CalculatedBlastRadius - // *ChangeTimelineEntryV2_CalculatedRisks - // *ChangeTimelineEntryV2_Error - // *ChangeTimelineEntryV2_StatusMessage - // *ChangeTimelineEntryV2_Empty - // *ChangeTimelineEntryV2_ChangeValidation - // *ChangeTimelineEntryV2_CalculatedLabels - // *ChangeTimelineEntryV2_FormHypotheses - // *ChangeTimelineEntryV2_InvestigateHypotheses - // *ChangeTimelineEntryV2_RecordObservations - Content isChangeTimelineEntryV2_Content `protobuf_oneof:"content"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeTimelineEntryV2) Reset() { - *x = ChangeTimelineEntryV2{} - mi := &file_changes_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeTimelineEntryV2) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeTimelineEntryV2) ProtoMessage() {} - -func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[22] - 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 ChangeTimelineEntryV2.ProtoReflect.Descriptor instead. -func (*ChangeTimelineEntryV2) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{22} -} - -func (x *ChangeTimelineEntryV2) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ChangeTimelineEntryV2) GetStatus() ChangeTimelineEntryStatus { - if x != nil { - return x.Status - } - return ChangeTimelineEntryStatus_UNSPECIFIED -} - -func (x *ChangeTimelineEntryV2) GetStartedAt() *timestamppb.Timestamp { - if x != nil { - return x.StartedAt - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetEndedAt() *timestamppb.Timestamp { - if x != nil { - return x.EndedAt - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetActor() string { - if x != nil && x.Actor != nil { - return *x.Actor - } - return "" -} - -func (x *ChangeTimelineEntryV2) GetContent() isChangeTimelineEntryV2_Content { - if x != nil { - return x.Content - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetMappedItems() *MappedItemsTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_MappedItems); ok { - return x.MappedItems - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetCalculatedBlastRadius() *CalculatedBlastRadiusTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedBlastRadius); ok { - return x.CalculatedBlastRadius - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetCalculatedRisks() *CalculatedRisksTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedRisks); ok { - return x.CalculatedRisks - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetError() string { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_Error); ok { - return x.Error - } - } - return "" -} - -func (x *ChangeTimelineEntryV2) GetStatusMessage() string { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_StatusMessage); ok { - return x.StatusMessage - } - } - return "" -} - -func (x *ChangeTimelineEntryV2) GetEmpty() *EmptyContent { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_Empty); ok { - return x.Empty - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetChangeValidation() *ChangeValidationTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_ChangeValidation); ok { - return x.ChangeValidation - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetCalculatedLabels() *CalculatedLabelsTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedLabels); ok { - return x.CalculatedLabels - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetFormHypotheses() *FormHypothesesTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_FormHypotheses); ok { - return x.FormHypotheses - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetInvestigateHypotheses() *InvestigateHypothesesTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_InvestigateHypotheses); ok { - return x.InvestigateHypotheses - } - } - return nil -} - -func (x *ChangeTimelineEntryV2) GetRecordObservations() *RecordObservationsTimelineEntry { - if x != nil { - if x, ok := x.Content.(*ChangeTimelineEntryV2_RecordObservations); ok { - return x.RecordObservations - } - } - return nil -} - -type isChangeTimelineEntryV2_Content interface { - isChangeTimelineEntryV2_Content() -} - -type ChangeTimelineEntryV2_MappedItems struct { - // Shows the mapping results so that user can see why items didn't map - // successfully - MappedItems *MappedItemsTimelineEntry `protobuf:"bytes,7,opt,name=mappedItems,proto3,oneof"` -} - -type ChangeTimelineEntryV2_CalculatedBlastRadius struct { - // The number of items in the blast radius - CalculatedBlastRadius *CalculatedBlastRadiusTimelineEntry `protobuf:"bytes,8,opt,name=calculatedBlastRadius,proto3,oneof"` -} - -type ChangeTimelineEntryV2_CalculatedRisks struct { - // The list of risks - CalculatedRisks *CalculatedRisksTimelineEntry `protobuf:"bytes,9,opt,name=calculatedRisks,proto3,oneof"` -} - -type ChangeTimelineEntryV2_Error struct { - // An error that will be shown to the user - Error string `protobuf:"bytes,11,opt,name=error,proto3,oneof"` -} - -type ChangeTimelineEntryV2_StatusMessage struct { - // A generic message that will be rendered as a paragraph - StatusMessage string `protobuf:"bytes,12,opt,name=statusMessage,proto3,oneof"` -} - -type ChangeTimelineEntryV2_Empty struct { - // A message that will be shown to the user, but will not have any content - // associated with it. Examples of this include "Change Created", "Change - // Started" etc. - Empty *EmptyContent `protobuf:"bytes,13,opt,name=empty,proto3,oneof"` -} - -type ChangeTimelineEntryV2_ChangeValidation struct { - // A list of validation steps that should be performed on the change - ChangeValidation *ChangeValidationTimelineEntry `protobuf:"bytes,14,opt,name=changeValidation,proto3,oneof"` -} - -type ChangeTimelineEntryV2_CalculatedLabels struct { - // The list of labels that have been calculated for this change, or that were assigned by a user - CalculatedLabels *CalculatedLabelsTimelineEntry `protobuf:"bytes,15,opt,name=calculatedLabels,proto3,oneof"` -} - -type ChangeTimelineEntryV2_FormHypotheses struct { - // The list of hypotheses that have been formed - FormHypotheses *FormHypothesesTimelineEntry `protobuf:"bytes,16,opt,name=formHypotheses,proto3,oneof"` -} - -type ChangeTimelineEntryV2_InvestigateHypotheses struct { - // The list of hypotheses that have been investigated - InvestigateHypotheses *InvestigateHypothesesTimelineEntry `protobuf:"bytes,17,opt,name=investigateHypotheses,proto3,oneof"` -} - -type ChangeTimelineEntryV2_RecordObservations struct { - // The number of observations that were found as part of calculating the blast - // radius - RecordObservations *RecordObservationsTimelineEntry `protobuf:"bytes,18,opt,name=recordObservations,proto3,oneof"` -} - -func (*ChangeTimelineEntryV2_MappedItems) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_CalculatedBlastRadius) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_CalculatedRisks) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_Error) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_StatusMessage) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_Empty) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_ChangeValidation) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_CalculatedLabels) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_FormHypotheses) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_InvestigateHypotheses) isChangeTimelineEntryV2_Content() {} - -func (*ChangeTimelineEntryV2_RecordObservations) isChangeTimelineEntryV2_Content() {} - -// This is a message that can be used to signal that a step in the timeline -// should be empty. This is useful for when we want to show a step in the -// timeline, but there is no content to show -type EmptyContent struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EmptyContent) Reset() { - *x = EmptyContent{} - mi := &file_changes_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EmptyContent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EmptyContent) ProtoMessage() {} - -func (x *EmptyContent) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[23] - 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 EmptyContent.ProtoReflect.Descriptor instead. -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"` - // 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. - // - // 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[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MappedItemsTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MappedItemsTimelineEntry) ProtoMessage() {} - -func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[25] - 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 MappedItemsTimelineEntry.ProtoReflect.Descriptor instead. -func (*MappedItemsTimelineEntry) Descriptor() ([]byte, []int) { - 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 - } - 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"` - NumEdges uint32 `protobuf:"varint,2,opt,name=numEdges,proto3" json:"numEdges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CalculatedBlastRadiusTimelineEntry) Reset() { - *x = CalculatedBlastRadiusTimelineEntry{} - mi := &file_changes_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CalculatedBlastRadiusTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CalculatedBlastRadiusTimelineEntry) ProtoMessage() {} - -func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[26] - 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 CalculatedBlastRadiusTimelineEntry.ProtoReflect.Descriptor instead. -func (*CalculatedBlastRadiusTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{26} -} - -func (x *CalculatedBlastRadiusTimelineEntry) GetNumItems() uint32 { - if x != nil { - return x.NumItems - } - return 0 -} - -func (x *CalculatedBlastRadiusTimelineEntry) GetNumEdges() uint32 { - if x != nil { - return x.NumEdges - } - return 0 -} - -type RecordObservationsTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The number of observations that were found as part of calculating the blast - // radius - NumObservations uint32 `protobuf:"varint,1,opt,name=numObservations,proto3" json:"numObservations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RecordObservationsTimelineEntry) Reset() { - *x = RecordObservationsTimelineEntry{} - mi := &file_changes_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RecordObservationsTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RecordObservationsTimelineEntry) ProtoMessage() {} - -func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[27] - 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 RecordObservationsTimelineEntry.ProtoReflect.Descriptor instead. -func (*RecordObservationsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{27} -} - -func (x *RecordObservationsTimelineEntry) GetNumObservations() uint32 { - if x != nil { - return x.NumObservations - } - return 0 -} - -// Timeline entry: Forming hypotheses by grouping observations -type FormHypothesesTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Total number of hypotheses formed - NumHypotheses uint32 `protobuf:"varint,1,opt,name=numHypotheses,proto3" json:"numHypotheses,omitempty"` - // The current state of the hypotheses under investigation - Hypotheses []*HypothesisSummary `protobuf:"bytes,2,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FormHypothesesTimelineEntry) Reset() { - *x = FormHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FormHypothesesTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FormHypothesesTimelineEntry) ProtoMessage() {} - -func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[28] - 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 FormHypothesesTimelineEntry.ProtoReflect.Descriptor instead. -func (*FormHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{28} -} - -func (x *FormHypothesesTimelineEntry) GetNumHypotheses() uint32 { - if x != nil { - return x.NumHypotheses - } - return 0 -} - -func (x *FormHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummary { - if x != nil { - return x.Hypotheses - } - return nil -} - -type InvestigateHypothesesTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Number of hypotheses that became real risks - NumProven uint32 `protobuf:"varint,1,opt,name=numProven,proto3" json:"numProven,omitempty"` - // Number of hypotheses that were disproven (verified safe) - NumDisproven uint32 `protobuf:"varint,2,opt,name=numDisproven,proto3" json:"numDisproven,omitempty"` - // Number of hypotheses that are still being investigated - NumInvestigating uint32 `protobuf:"varint,3,opt,name=numInvestigating,proto3" json:"numInvestigating,omitempty"` - // The current state of the hypotheses under investigation - Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *InvestigateHypothesesTimelineEntry) Reset() { - *x = InvestigateHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *InvestigateHypothesesTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InvestigateHypothesesTimelineEntry) ProtoMessage() {} - -func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[29] - 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 InvestigateHypothesesTimelineEntry.ProtoReflect.Descriptor instead. -func (*InvestigateHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{29} -} - -func (x *InvestigateHypothesesTimelineEntry) GetNumProven() uint32 { - if x != nil { - return x.NumProven - } - return 0 -} - -func (x *InvestigateHypothesesTimelineEntry) GetNumDisproven() uint32 { - if x != nil { - return x.NumDisproven - } - return 0 -} - -func (x *InvestigateHypothesesTimelineEntry) GetNumInvestigating() uint32 { - if x != nil { - return x.NumInvestigating - } - return 0 -} - -func (x *InvestigateHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummary { - if x != nil { - return x.Hypotheses - } - return nil -} - -type HypothesisSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The status of the investigation - Status HypothesisStatus `protobuf:"varint,1,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` - // The title of the hypothesis - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - // The current detail to show the user, While the hypothesis is being - // investigated, this could be the description of the hypothesis itself. And - // then once the investigation is finished, it could be the conclusion of the - // investigation. So it could update. This will be limited to two or three - // lines and truncated after that. - Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HypothesisSummary) Reset() { - *x = HypothesisSummary{} - mi := &file_changes_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HypothesisSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HypothesisSummary) ProtoMessage() {} - -func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[30] - 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 HypothesisSummary.ProtoReflect.Descriptor instead. -func (*HypothesisSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{30} -} - -func (x *HypothesisSummary) GetStatus() HypothesisStatus { - if x != nil { - return x.Status - } - return HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED -} - -func (x *HypothesisSummary) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *HypothesisSummary) GetDetail() string { - if x != nil { - return x.Detail - } - return "" -} - -type CalculatedRisksTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Risks []*Risk `protobuf:"bytes,1,rep,name=risks,proto3" json:"risks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CalculatedRisksTimelineEntry) Reset() { - *x = CalculatedRisksTimelineEntry{} - mi := &file_changes_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CalculatedRisksTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CalculatedRisksTimelineEntry) ProtoMessage() {} - -func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[31] - 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 CalculatedRisksTimelineEntry.ProtoReflect.Descriptor instead. -func (*CalculatedRisksTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{31} -} - -func (x *CalculatedRisksTimelineEntry) GetRisks() []*Risk { - if x != nil { - return x.Risks - } - return nil -} - -// The list of labels that have been calculated for this change, or that were assigned by a user -type CalculatedLabelsTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Labels []*Label `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CalculatedLabelsTimelineEntry) Reset() { - *x = CalculatedLabelsTimelineEntry{} - mi := &file_changes_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CalculatedLabelsTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CalculatedLabelsTimelineEntry) ProtoMessage() {} - -func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[32] - 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 CalculatedLabelsTimelineEntry.ProtoReflect.Descriptor instead. -func (*CalculatedLabelsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{32} -} - -func (x *CalculatedLabelsTimelineEntry) GetLabels() []*Label { - if x != nil { - return x.Labels - } - return nil -} - -// The list of validation steps that are to be performed on the change -type ChangeValidationTimelineEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - // "A concise overview of the proposed changes and their potential impact (2-3 sentences) - BriefAnalysis string `protobuf:"bytes,1,opt,name=briefAnalysis,proto3" json:"briefAnalysis,omitempty"` - // For the first stage of the change validation implementation we are only returning Validation Checklist Category - ValidationChecklist []*ChangeValidationCategory `protobuf:"bytes,2,rep,name=validationChecklist,proto3" json:"validationChecklist,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeValidationTimelineEntry) Reset() { - *x = ChangeValidationTimelineEntry{} - mi := &file_changes_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeValidationTimelineEntry) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeValidationTimelineEntry) ProtoMessage() {} - -func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[33] - 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 ChangeValidationTimelineEntry.ProtoReflect.Descriptor instead. -func (*ChangeValidationTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{33} -} - -func (x *ChangeValidationTimelineEntry) GetBriefAnalysis() string { - if x != nil { - return x.BriefAnalysis - } - return "" -} - -func (x *ChangeValidationTimelineEntry) GetValidationChecklist() []*ChangeValidationCategory { - if x != nil { - return x.ValidationChecklist - } - return nil -} - -// Description with specific commands/API calls to execute (1-2 sentences).Add commentMore actions -// - Include exact AWS CLI commands with parameters and resource IDs -// - Focus on must-have verification steps only -type ChangeValidationCategory struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The category title (e.g., 'Security Validation', 'Configuration Verification') - Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` - // Description with specific AWS CLI commands/API calls to execute (1-2 sentences) - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeValidationCategory) Reset() { - *x = ChangeValidationCategory{} - mi := &file_changes_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeValidationCategory) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeValidationCategory) ProtoMessage() {} - -func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[34] - 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 ChangeValidationCategory.ProtoReflect.Descriptor instead. -func (*ChangeValidationCategory) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{34} -} - -func (x *ChangeValidationCategory) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *ChangeValidationCategory) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -type GetDiffRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetDiffRequest) Reset() { - *x = GetDiffRequest{} - mi := &file_changes_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetDiffRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetDiffRequest) ProtoMessage() {} - -func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[35] - 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 GetDiffRequest.ProtoReflect.Descriptor instead. -func (*GetDiffRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{35} -} - -func (x *GetDiffRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetDiffResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Items that were planned to be changed, and were changed - ExpectedItems []*ItemDiff `protobuf:"bytes,1,rep,name=expectedItems,proto3" json:"expectedItems,omitempty"` - // Items that were changed, but were not planned to be changed - UnexpectedItems []*ItemDiff `protobuf:"bytes,3,rep,name=unexpectedItems,proto3" json:"unexpectedItems,omitempty"` - Edges []*Edge `protobuf:"bytes,2,rep,name=edges,proto3" json:"edges,omitempty"` - // Items that were planned to be changed, but were not changed - MissingItems []*ItemDiff `protobuf:"bytes,4,rep,name=missingItems,proto3" json:"missingItems,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetDiffResponse) Reset() { - *x = GetDiffResponse{} - mi := &file_changes_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetDiffResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetDiffResponse) ProtoMessage() {} - -func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[36] - 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 GetDiffResponse.ProtoReflect.Descriptor instead. -func (*GetDiffResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{36} -} - -func (x *GetDiffResponse) GetExpectedItems() []*ItemDiff { - if x != nil { - return x.ExpectedItems - } - return nil -} - -func (x *GetDiffResponse) GetUnexpectedItems() []*ItemDiff { - if x != nil { - return x.UnexpectedItems - } - return nil -} - -func (x *GetDiffResponse) GetEdges() []*Edge { - if x != nil { - return x.Edges - } - return nil -} - -func (x *GetDiffResponse) GetMissingItems() []*ItemDiff { - if x != nil { - return x.MissingItems - } - return nil -} - -type ListChangingItemsSummaryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangingItemsSummaryRequest) Reset() { - *x = ListChangingItemsSummaryRequest{} - mi := &file_changes_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangingItemsSummaryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangingItemsSummaryRequest) ProtoMessage() {} - -func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[37] - 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 ListChangingItemsSummaryRequest.ProtoReflect.Descriptor instead. -func (*ListChangingItemsSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{37} -} - -func (x *ListChangingItemsSummaryRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type ListChangingItemsSummaryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items []*ItemDiffSummary `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangingItemsSummaryResponse) Reset() { - *x = ListChangingItemsSummaryResponse{} - mi := &file_changes_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangingItemsSummaryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangingItemsSummaryResponse) ProtoMessage() {} - -func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[38] - 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 ListChangingItemsSummaryResponse.ProtoReflect.Descriptor instead. -func (*ListChangingItemsSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{38} -} - -func (x *ListChangingItemsSummaryResponse) GetItems() []*ItemDiffSummary { - if x != nil { - return x.Items - } - return nil -} - -type MappedItemDiff struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The item that is changing and any known changes to it - Item *ItemDiff `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // a mapping query that can be used to find the item. this can be empty if the - // submitter does not know how to map this item. - MappingQuery *Query `protobuf:"bytes,2,opt,name=mappingQuery,proto3,oneof" json:"mappingQuery,omitempty"` - // The error that was returned as part of the mapping process. This will be - // empty if the mapping was successful. - MappingError *QueryError `protobuf:"bytes,3,opt,name=mappingError,proto3,oneof" json:"mappingError,omitempty"` - // Explicit status from CLI - when set, API uses this instead of inferring. - // This allows CLI to distinguish between "unsupported resource type", - // "pending creation (doesn't exist yet)", and "actual mapping error". - MappingStatus *MappedItemMappingStatus `protobuf:"varint,4,opt,name=mapping_status,json=mappingStatus,proto3,enum=changes.MappedItemMappingStatus,oneof" json:"mapping_status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *MappedItemDiff) Reset() { - *x = MappedItemDiff{} - mi := &file_changes_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *MappedItemDiff) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MappedItemDiff) ProtoMessage() {} - -func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[39] - 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 MappedItemDiff.ProtoReflect.Descriptor instead. -func (*MappedItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{39} -} - -func (x *MappedItemDiff) GetItem() *ItemDiff { - if x != nil { - return x.Item - } - return nil -} - -func (x *MappedItemDiff) GetMappingQuery() *Query { - if x != nil { - return x.MappingQuery - } - return nil -} - -func (x *MappedItemDiff) GetMappingError() *QueryError { - if x != nil { - return x.MappingError - } - return nil -} - -func (x *MappedItemDiff) GetMappingStatus() MappedItemMappingStatus { - if x != nil && x.MappingStatus != nil { - return *x.MappingStatus - } - return MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED -} - -// StartChangeAnalysisRequest is used to start the change analysis process. This -// will calculate various things blast radius, risks, auto-tagging etc. This -// it contains overrides for the auto-tagging rules and the blast radius config -type StartChangeAnalysisRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The change to update - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - // The changing items - ChangingItems []*MappedItemDiff `protobuf:"bytes,2,rep,name=changingItems,proto3" json:"changingItems,omitempty"` - // Overrides the stored blast radius config for this change - BlastRadiusConfigOverride *BlastRadiusConfig `protobuf:"bytes,3,opt,name=blastRadiusConfigOverride,proto3,oneof" json:"blastRadiusConfigOverride,omitempty"` - // The routine config that should be used for this change. If this is empty - // the routine config that has been configured in the UI will be used - RoutineChangesConfigOverride *RoutineChangesConfig `protobuf:"bytes,5,opt,name=routineChangesConfigOverride,proto3,oneof" json:"routineChangesConfigOverride,omitempty"` - // github organisation profile to use for this change - GithubOrganisationProfileOverride *GithubOrganisationProfile `protobuf:"bytes,6,opt,name=githubOrganisationProfileOverride,proto3,oneof" json:"githubOrganisationProfileOverride,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StartChangeAnalysisRequest) Reset() { - *x = StartChangeAnalysisRequest{} - mi := &file_changes_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StartChangeAnalysisRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartChangeAnalysisRequest) ProtoMessage() {} - -func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[40] - 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 StartChangeAnalysisRequest.ProtoReflect.Descriptor instead. -func (*StartChangeAnalysisRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{40} -} - -func (x *StartChangeAnalysisRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -func (x *StartChangeAnalysisRequest) GetChangingItems() []*MappedItemDiff { - if x != nil { - return x.ChangingItems - } - return nil -} - -func (x *StartChangeAnalysisRequest) GetBlastRadiusConfigOverride() *BlastRadiusConfig { - if x != nil { - return x.BlastRadiusConfigOverride - } - return nil -} - -func (x *StartChangeAnalysisRequest) GetRoutineChangesConfigOverride() *RoutineChangesConfig { - if x != nil { - return x.RoutineChangesConfigOverride - } - return nil -} - -func (x *StartChangeAnalysisRequest) GetGithubOrganisationProfileOverride() *GithubOrganisationProfile { - if x != nil { - return x.GithubOrganisationProfileOverride - } - return nil -} - -// StartChangeAnalysisResponse is used to signal that the change analysis has been successfully started -// we use HTTP response codes to signal errors -type StartChangeAnalysisResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StartChangeAnalysisResponse) Reset() { - *x = StartChangeAnalysisResponse{} - mi := &file_changes_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StartChangeAnalysisResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartChangeAnalysisResponse) ProtoMessage() {} - -func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[41] - 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 StartChangeAnalysisResponse.ProtoReflect.Descriptor instead. -func (*StartChangeAnalysisResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{41} -} - -type ListHomeChangesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Pagination *PaginationRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"` - Filters *ChangeFiltersRequest `protobuf:"bytes,2,opt,name=filters,proto3,oneof" json:"filters,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListHomeChangesRequest) Reset() { - *x = ListHomeChangesRequest{} - mi := &file_changes_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListHomeChangesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListHomeChangesRequest) ProtoMessage() {} - -func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[42] - 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 ListHomeChangesRequest.ProtoReflect.Descriptor instead. -func (*ListHomeChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{42} -} - -func (x *ListHomeChangesRequest) GetPagination() *PaginationRequest { - if x != nil { - return x.Pagination - } - return nil -} - -func (x *ListHomeChangesRequest) GetFilters() *ChangeFiltersRequest { - if x != nil { - return x.Filters - } - return nil -} - -// ChangeFiltersRequest is used for filtering on the changes page. -// Repeated entries of the same type are used to represent OR conditions. eg if repo is ["a", "b"] then the filter is (repo == "a" OR repo == "b") -// The filters are ANDed together. eg if repo is ["a", "b"] and author is ["c"] then the filter is (repo == "a" OR repo == "b") AND author == "c" -type ChangeFiltersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Repos []string `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` - Risks []Risk_Severity `protobuf:"varint,3,rep,packed,name=risks,proto3,enum=changes.Risk_Severity" json:"risks,omitempty"` - Authors []string `protobuf:"bytes,4,rep,name=authors,proto3" json:"authors,omitempty"` - Statuses []ChangeStatus `protobuf:"varint,5,rep,packed,name=statuses,proto3,enum=changes.ChangeStatus" json:"statuses,omitempty"` - SortOrder *SortOrder `protobuf:"varint,6,opt,name=sortOrder,proto3,enum=SortOrder,oneof" json:"sortOrder,omitempty"` // the default is SortOrder.DATE_DESCENDING (newest first) - Labels []string `protobuf:"bytes,7,rep,name=labels,proto3" json:"labels,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeFiltersRequest) Reset() { - *x = ChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeFiltersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeFiltersRequest) ProtoMessage() {} - -func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[43] - 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 ChangeFiltersRequest.ProtoReflect.Descriptor instead. -func (*ChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{43} -} - -func (x *ChangeFiltersRequest) GetRepos() []string { - if x != nil { - return x.Repos - } - return nil -} - -func (x *ChangeFiltersRequest) GetRisks() []Risk_Severity { - if x != nil { - return x.Risks - } - return nil -} - -func (x *ChangeFiltersRequest) GetAuthors() []string { - if x != nil { - return x.Authors - } - return nil -} - -func (x *ChangeFiltersRequest) GetStatuses() []ChangeStatus { - if x != nil { - return x.Statuses - } - return nil -} - -func (x *ChangeFiltersRequest) GetSortOrder() SortOrder { - if x != nil && x.SortOrder != nil { - return *x.SortOrder - } - return SortOrder_ALPHABETICAL_ASCENDING -} - -func (x *ChangeFiltersRequest) GetLabels() []string { - if x != nil { - return x.Labels - } - return nil -} - -type ListHomeChangesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Changes []*ChangeSummary `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` - Pagination *PaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListHomeChangesResponse) Reset() { - *x = ListHomeChangesResponse{} - mi := &file_changes_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListHomeChangesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListHomeChangesResponse) ProtoMessage() {} - -func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[44] - 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 ListHomeChangesResponse.ProtoReflect.Descriptor instead. -func (*ListHomeChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{44} -} - -func (x *ListHomeChangesResponse) GetChanges() []*ChangeSummary { - if x != nil { - return x.Changes - } - return nil -} - -func (x *ListHomeChangesResponse) GetPagination() *PaginationResponse { - if x != nil { - return x.Pagination - } - return nil -} - -type PopulateChangeFiltersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PopulateChangeFiltersRequest) Reset() { - *x = PopulateChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PopulateChangeFiltersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PopulateChangeFiltersRequest) ProtoMessage() {} - -func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[45] - 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 PopulateChangeFiltersRequest.ProtoReflect.Descriptor instead. -func (*PopulateChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{45} -} - -type PopulateChangeFiltersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Repos []string `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` - Authors []string `protobuf:"bytes,2,rep,name=authors,proto3" json:"authors,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PopulateChangeFiltersResponse) Reset() { - *x = PopulateChangeFiltersResponse{} - mi := &file_changes_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PopulateChangeFiltersResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PopulateChangeFiltersResponse) ProtoMessage() {} - -func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[46] - 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 PopulateChangeFiltersResponse.ProtoReflect.Descriptor instead. -func (*PopulateChangeFiltersResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{46} -} - -func (x *PopulateChangeFiltersResponse) GetRepos() []string { - if x != nil { - return x.Repos - } - return nil -} - -func (x *PopulateChangeFiltersResponse) GetAuthors() []string { - if x != nil { - return x.Authors - } - return nil -} - -type ItemDiffSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A reference to the item that this diff is related to - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // The status of the item - Status ItemDiffStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` - // The health of the item currently (as opposed to before the change) - HealthAfter Health `protobuf:"varint,5,opt,name=healthAfter,proto3,enum=Health" json:"healthAfter,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ItemDiffSummary) Reset() { - *x = ItemDiffSummary{} - mi := &file_changes_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ItemDiffSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ItemDiffSummary) ProtoMessage() {} - -func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[47] - 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 ItemDiffSummary.ProtoReflect.Descriptor instead. -func (*ItemDiffSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{47} -} - -func (x *ItemDiffSummary) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *ItemDiffSummary) GetStatus() ItemDiffStatus { - if x != nil { - return x.Status - } - return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED -} - -func (x *ItemDiffSummary) GetHealthAfter() Health { - if x != nil { - return x.HealthAfter - } - return Health_HEALTH_UNKNOWN -} - -type ItemDiff struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A reference to the item that this diff is related to, if this exists in the - // real infrastructure. If this is blank it represents a change that we were - // unable to find a matching item for - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3,oneof" json:"item,omitempty"` - // The status of the item - Status ItemDiffStatus `protobuf:"varint,2,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` - Before *Item `protobuf:"bytes,3,opt,name=before,proto3" json:"before,omitempty"` - After *Item `protobuf:"bytes,4,opt,name=after,proto3" json:"after,omitempty"` - // A summary of how often the GUN's have had similar changes for individual attributes along with planned and unplanned changes - ModificationSummary string `protobuf:"bytes,5,opt,name=modificationSummary,proto3" json:"modificationSummary,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ItemDiff) Reset() { - *x = ItemDiff{} - mi := &file_changes_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ItemDiff) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ItemDiff) ProtoMessage() {} - -func (x *ItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[48] - 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 ItemDiff.ProtoReflect.Descriptor instead. -func (*ItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{48} -} - -func (x *ItemDiff) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *ItemDiff) GetStatus() ItemDiffStatus { - if x != nil { - return x.Status - } - return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED -} - -func (x *ItemDiff) GetBefore() *Item { - if x != nil { - return x.Before - } - return nil -} - -func (x *ItemDiff) GetAfter() *Item { - if x != nil { - return x.After - } - return nil -} - -func (x *ItemDiff) GetModificationSummary() string { - if x != nil { - return x.ModificationSummary - } - return "" -} - -type EnrichedTags struct { - state protoimpl.MessageState `protogen:"open.v1"` - TagValue map[string]*TagValue `protobuf:"bytes,18,rep,name=tagValue,proto3" json:"tagValue,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EnrichedTags) Reset() { - *x = EnrichedTags{} - mi := &file_changes_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EnrichedTags) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EnrichedTags) ProtoMessage() {} - -func (x *EnrichedTags) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[49] - 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 EnrichedTags.ProtoReflect.Descriptor instead. -func (*EnrichedTags) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{49} -} - -func (x *EnrichedTags) GetTagValue() map[string]*TagValue { - if x != nil { - return x.TagValue - } - return nil -} - -type TagValue struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The value of the tag, this can be user-defined or auto-generated - // - // Types that are valid to be assigned to Value: - // - // *TagValue_UserTagValue - // *TagValue_AutoTagValue - Value isTagValue_Value `protobuf_oneof:"value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TagValue) Reset() { - *x = TagValue{} - mi := &file_changes_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TagValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TagValue) ProtoMessage() {} - -func (x *TagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[50] - 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 TagValue.ProtoReflect.Descriptor instead. -func (*TagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{50} -} - -func (x *TagValue) GetValue() isTagValue_Value { - if x != nil { - return x.Value - } - return nil -} - -func (x *TagValue) GetUserTagValue() *UserTagValue { - if x != nil { - if x, ok := x.Value.(*TagValue_UserTagValue); ok { - return x.UserTagValue - } - } - return nil -} - -func (x *TagValue) GetAutoTagValue() *AutoTagValue { - if x != nil { - if x, ok := x.Value.(*TagValue_AutoTagValue); ok { - return x.AutoTagValue - } - } - return nil -} - -type isTagValue_Value interface { - isTagValue_Value() -} - -type TagValue_UserTagValue struct { - UserTagValue *UserTagValue `protobuf:"bytes,1,opt,name=userTagValue,proto3,oneof"` -} - -type TagValue_AutoTagValue struct { - AutoTagValue *AutoTagValue `protobuf:"bytes,2,opt,name=autoTagValue,proto3,oneof"` -} - -func (*TagValue_UserTagValue) isTagValue_Value() {} - -func (*TagValue_AutoTagValue) isTagValue_Value() {} - -type UserTagValue struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The value of the tag that was set by the user. - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UserTagValue) Reset() { - *x = UserTagValue{} - mi := &file_changes_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UserTagValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UserTagValue) ProtoMessage() {} - -func (x *UserTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[51] - 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 UserTagValue.ProtoReflect.Descriptor instead. -func (*UserTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{51} -} - -func (x *UserTagValue) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type AutoTagValue struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The value of the tag - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` - // Reasoning for this decision - Reasoning string `protobuf:"bytes,2,opt,name=reasoning,proto3" json:"reasoning,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AutoTagValue) Reset() { - *x = AutoTagValue{} - mi := &file_changes_proto_msgTypes[52] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AutoTagValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AutoTagValue) ProtoMessage() {} - -func (x *AutoTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[52] - 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 AutoTagValue.ProtoReflect.Descriptor instead. -func (*AutoTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{52} -} - -func (x *AutoTagValue) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -func (x *AutoTagValue) GetReasoning() string { - if x != nil { - return x.Reasoning - } - return "" -} - -// a label that can be applied to a change -// note that it keeps the colour / name based on the rule that was used at the time of creation -// if the rule is updated, the colour / name will not be updated, unless -// 1. the change is re-run, in which case the label will be updated to the new colour / name. -// 2. the user will have to manually re-run the rule to get the new colour / name. this may also remove the label from the change if the rule is no longer applied. -type Label struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The type of the label, it is required - Type LabelType `protobuf:"varint,1,opt,name=type,proto3,enum=changes.LabelType" json:"type,omitempty"` - // name of the label, it is required - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // colour of the label - // discussed with the UI team and we will use a hex code for the colour, it is required - Colour string `protobuf:"bytes,3,opt,name=colour,proto3" json:"colour,omitempty"` - // the label rule that was used to generate this label, this is only populated for auto-generated labels - LabelRuleUUID []byte `protobuf:"bytes,4,opt,name=labelRuleUUID,proto3" json:"labelRuleUUID,omitempty"` - // reasoning for this label, this is only populated for auto-generated labels - AutoLabelReasoning string `protobuf:"bytes,5,opt,name=autoLabelReasoning,proto3" json:"autoLabelReasoning,omitempty"` - // skipped if the label rule was not applied to the change, this is only populated for auto-generated labels - Skipped bool `protobuf:"varint,6,opt,name=skipped,proto3" json:"skipped,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Label) Reset() { - *x = Label{} - mi := &file_changes_proto_msgTypes[53] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Label) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Label) ProtoMessage() {} - -func (x *Label) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[53] - 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 Label.ProtoReflect.Descriptor instead. -func (*Label) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{53} -} - -func (x *Label) GetType() LabelType { - if x != nil { - return x.Type - } - return LabelType_LABEL_TYPE_UNSPECIFIED -} - -func (x *Label) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Label) GetColour() string { - if x != nil { - return x.Colour - } - return "" -} - -func (x *Label) GetLabelRuleUUID() []byte { - if x != nil { - return x.LabelRuleUUID - } - return nil -} - -func (x *Label) GetAutoLabelReasoning() string { - if x != nil { - return x.AutoLabelReasoning - } - return "" -} - -func (x *Label) GetSkipped() bool { - if x != nil { - return x.Skipped - } - return false -} - -// A smaller summary of a change -type ChangeSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id to identify this change - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Short title for this change. - // Example: "database upgrade" - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - // The current status of this change. This is changed by the lifecycle - // functions such as `StartChange` and `EndChange`. - Status ChangeStatus `protobuf:"varint,3,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` - // Link to the ticket for this change. - // Example: "http://jira.contoso-engineering.com/browse/CM-1337" - TicketLink string `protobuf:"bytes,4,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` - // timestamp when this change was created - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - // The name of the user that created the change - CreatorName string `protobuf:"bytes,6,opt,name=creatorName,proto3" json:"creatorName,omitempty"` - // The email of the user that created the change - CreatorEmail string `protobuf:"bytes,15,opt,name=creatorEmail,proto3" json:"creatorEmail,omitempty"` - // The number of items in the blast radius of this change - NumAffectedItems int32 `protobuf:"varint,9,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` - // The number of edges in the blast radius of this change - NumAffectedEdges int32 `protobuf:"varint,10,opt,name=numAffectedEdges,proto3" json:"numAffectedEdges,omitempty"` - // The number of low risks in this change - NumLowRisk int32 `protobuf:"varint,11,opt,name=numLowRisk,proto3" json:"numLowRisk,omitempty"` - // The number of medium risks in this change - NumMediumRisk int32 `protobuf:"varint,12,opt,name=numMediumRisk,proto3" json:"numMediumRisk,omitempty"` - // The number of high risks in this change - NumHighRisk int32 `protobuf:"varint,13,opt,name=numHighRisk,proto3" json:"numHighRisk,omitempty"` - // Quick description of the change. - // Example: "upgrade of the database to get access to the new contoso management processor" - Description string `protobuf:"bytes,14,opt,name=description,proto3" json:"description,omitempty"` - // Repo information; can be an empty string. CLI attempts auto-population, but - // users can override. Not necessarily a URL. The UI will be responsible for - // any formatting/shortening/sprucing up should it be required. - Repo string `protobuf:"bytes,16,opt,name=repo,proto3" json:"repo,omitempty"` - // Deprecated: Use enrichedTags instead - // - // Deprecated: Marked as deprecated in changes.proto. - Tags map[string]string `protobuf:"bytes,17,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Tags associated with this change - EnrichedTags *EnrichedTags `protobuf:"bytes,18,opt,name=enrichedTags,proto3" json:"enrichedTags,omitempty"` - // labels - Labels []*Label `protobuf:"bytes,19,rep,name=labels,proto3" json:"labels,omitempty"` - // Github change information - // contains information about the author - GithubChangeInfo *GithubChangeInfo `protobuf:"bytes,20,opt,name=githubChangeInfo,proto3" json:"githubChangeInfo,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeSummary) Reset() { - *x = ChangeSummary{} - mi := &file_changes_proto_msgTypes[54] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeSummary) ProtoMessage() {} - -func (x *ChangeSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[54] - 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 ChangeSummary.ProtoReflect.Descriptor instead. -func (*ChangeSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{54} -} - -func (x *ChangeSummary) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *ChangeSummary) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *ChangeSummary) GetStatus() ChangeStatus { - if x != nil { - return x.Status - } - return ChangeStatus_CHANGE_STATUS_UNSPECIFIED -} - -func (x *ChangeSummary) GetTicketLink() string { - if x != nil { - return x.TicketLink - } - return "" -} - -func (x *ChangeSummary) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *ChangeSummary) GetCreatorName() string { - if x != nil { - return x.CreatorName - } - return "" -} - -func (x *ChangeSummary) GetCreatorEmail() string { - if x != nil { - return x.CreatorEmail - } - return "" -} - -func (x *ChangeSummary) GetNumAffectedItems() int32 { - if x != nil { - return x.NumAffectedItems - } - return 0 -} - -func (x *ChangeSummary) GetNumAffectedEdges() int32 { - if x != nil { - return x.NumAffectedEdges - } - return 0 -} - -func (x *ChangeSummary) GetNumLowRisk() int32 { - if x != nil { - return x.NumLowRisk - } - return 0 -} - -func (x *ChangeSummary) GetNumMediumRisk() int32 { - if x != nil { - return x.NumMediumRisk - } - return 0 -} - -func (x *ChangeSummary) GetNumHighRisk() int32 { - if x != nil { - return x.NumHighRisk - } - return 0 -} - -func (x *ChangeSummary) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *ChangeSummary) GetRepo() string { - if x != nil { - return x.Repo - } - return "" -} - -// Deprecated: Marked as deprecated in changes.proto. -func (x *ChangeSummary) GetTags() map[string]string { - if x != nil { - return x.Tags - } - return nil -} - -func (x *ChangeSummary) GetEnrichedTags() *EnrichedTags { - if x != nil { - return x.EnrichedTags - } - return nil -} - -func (x *ChangeSummary) GetLabels() []*Label { - if x != nil { - return x.Labels - } - return nil -} - -func (x *ChangeSummary) GetGithubChangeInfo() *GithubChangeInfo { - if x != nil { - return x.GithubChangeInfo - } - return nil -} - -// a complete Change with machine-supplied and user-supplied values -type Change struct { - state protoimpl.MessageState `protogen:"open.v1"` - // machine-generated metadata of this change - Metadata *ChangeMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // user-supplied properties of this change - Properties *ChangeProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Change) Reset() { - *x = Change{} - mi := &file_changes_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Change) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Change) ProtoMessage() {} - -func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[55] - 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 Change.ProtoReflect.Descriptor instead. -func (*Change) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{55} -} - -func (x *Change) GetMetadata() *ChangeMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Change) GetProperties() *ChangeProperties { - if x != nil { - return x.Properties - } - return nil -} - -// machine-generated metadata of this change -type ChangeMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id to identify this change - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // timestamp when this change was created - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - // timestamp when this change was last updated - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"` - // The current status of this change. This is changed by the lifecycle - // functions such as `StartChange` and `EndChange`. - Status ChangeStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` - // The name of the user that created the change - CreatorName string `protobuf:"bytes,5,opt,name=creatorName,proto3" json:"creatorName,omitempty"` - // The email of the user that created the change - CreatorEmail string `protobuf:"bytes,19,opt,name=creatorEmail,proto3" json:"creatorEmail,omitempty"` - // The number of items in the blast radius if this change - NumAffectedItems int32 `protobuf:"varint,7,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` - // The number of edges in the blast radius if this change - NumAffectedEdges int32 `protobuf:"varint,17,opt,name=numAffectedEdges,proto3" json:"numAffectedEdges,omitempty"` - // The number of items within the blast radius that were not affected by this - // change - NumUnchangedItems int32 `protobuf:"varint,8,opt,name=numUnchangedItems,proto3" json:"numUnchangedItems,omitempty"` - // The number of items that were created as part of this change - NumCreatedItems int32 `protobuf:"varint,9,opt,name=numCreatedItems,proto3" json:"numCreatedItems,omitempty"` - // The number of items that were updated as part of this change - NumUpdatedItems int32 `protobuf:"varint,10,opt,name=numUpdatedItems,proto3" json:"numUpdatedItems,omitempty"` - // The number of items that were replaced as part of this change - NumReplacedItems int32 `protobuf:"varint,18,opt,name=numReplacedItems,proto3" json:"numReplacedItems,omitempty"` - // The number of items that were deleted as part of this change - NumDeletedItems int32 `protobuf:"varint,11,opt,name=numDeletedItems,proto3" json:"numDeletedItems,omitempty"` - UnknownHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,12,opt,name=UnknownHealthChange,proto3" json:"UnknownHealthChange,omitempty"` - OkHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,13,opt,name=OkHealthChange,proto3" json:"OkHealthChange,omitempty"` - WarningHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,14,opt,name=WarningHealthChange,proto3" json:"WarningHealthChange,omitempty"` - ErrorHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,15,opt,name=ErrorHealthChange,proto3" json:"ErrorHealthChange,omitempty"` - PendingHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,16,opt,name=PendingHealthChange,proto3" json:"PendingHealthChange,omitempty"` - // Github change information - // contains information about the author from github - GithubChangeInfo *GithubChangeInfo `protobuf:"bytes,20,opt,name=githubChangeInfo,proto3" json:"githubChangeInfo,omitempty"` - // The total number of observations recorded for this change during blast radius analysis. - // This is null/undefined for legacy changes where observations were not tracked. - // This count increments immediately as observations are added, providing fast feedback. - TotalObservations *uint32 `protobuf:"varint,21,opt,name=total_observations,json=totalObservations,proto3,oneof" json:"total_observations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeMetadata) Reset() { - *x = ChangeMetadata{} - mi := &file_changes_proto_msgTypes[56] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeMetadata) ProtoMessage() {} - -func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[56] - 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 ChangeMetadata.ProtoReflect.Descriptor instead. -func (*ChangeMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56} -} - -func (x *ChangeMetadata) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *ChangeMetadata) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *ChangeMetadata) GetUpdatedAt() *timestamppb.Timestamp { - if x != nil { - return x.UpdatedAt - } - return nil -} - -func (x *ChangeMetadata) GetStatus() ChangeStatus { - if x != nil { - return x.Status - } - return ChangeStatus_CHANGE_STATUS_UNSPECIFIED -} - -func (x *ChangeMetadata) GetCreatorName() string { - if x != nil { - return x.CreatorName - } - return "" -} - -func (x *ChangeMetadata) GetCreatorEmail() string { - if x != nil { - return x.CreatorEmail - } - return "" -} - -func (x *ChangeMetadata) GetNumAffectedItems() int32 { - if x != nil { - return x.NumAffectedItems - } - return 0 -} - -func (x *ChangeMetadata) GetNumAffectedEdges() int32 { - if x != nil { - return x.NumAffectedEdges - } - return 0 -} - -func (x *ChangeMetadata) GetNumUnchangedItems() int32 { - if x != nil { - return x.NumUnchangedItems - } - return 0 -} - -func (x *ChangeMetadata) GetNumCreatedItems() int32 { - if x != nil { - return x.NumCreatedItems - } - return 0 -} - -func (x *ChangeMetadata) GetNumUpdatedItems() int32 { - if x != nil { - return x.NumUpdatedItems - } - return 0 -} - -func (x *ChangeMetadata) GetNumReplacedItems() int32 { - if x != nil { - return x.NumReplacedItems - } - return 0 -} - -func (x *ChangeMetadata) GetNumDeletedItems() int32 { - if x != nil { - return x.NumDeletedItems - } - return 0 -} - -func (x *ChangeMetadata) GetUnknownHealthChange() *ChangeMetadata_HealthChange { - if x != nil { - return x.UnknownHealthChange - } - return nil -} - -func (x *ChangeMetadata) GetOkHealthChange() *ChangeMetadata_HealthChange { - if x != nil { - return x.OkHealthChange - } - return nil -} - -func (x *ChangeMetadata) GetWarningHealthChange() *ChangeMetadata_HealthChange { - if x != nil { - return x.WarningHealthChange - } - return nil -} - -func (x *ChangeMetadata) GetErrorHealthChange() *ChangeMetadata_HealthChange { - if x != nil { - return x.ErrorHealthChange - } - return nil -} - -func (x *ChangeMetadata) GetPendingHealthChange() *ChangeMetadata_HealthChange { - if x != nil { - return x.PendingHealthChange - } - return nil -} - -func (x *ChangeMetadata) GetGithubChangeInfo() *GithubChangeInfo { - if x != nil { - return x.GithubChangeInfo - } - return nil -} - -func (x *ChangeMetadata) GetTotalObservations() uint32 { - if x != nil && x.TotalObservations != nil { - return *x.TotalObservations - } - return 0 -} - -// user-supplied properties of this change -type ChangeProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Short title for this change. - // Example: "database upgrade" - Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` - // Quick description of the change. - // Example: "upgrade of the database to get access to the new contoso management processor" - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - // Link to the ticket for this change. - // Example: "http://jira.contoso-engineering.com/browse/CM-1337" - TicketLink string `protobuf:"bytes,4,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` - // The owner of this change. - // Example: Susan - Owner string `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` - // A comma-separated list of emails to keep updated with the status of this change. - // Example: susan@contoso.com, jimmy@contoso.com - CcEmails string `protobuf:"bytes,6,opt,name=ccEmails,proto3" json:"ccEmails,omitempty"` - // UUID of a bookmark for the item queries of the items *directly* affected by - // this change. This might be parsed from a terraform plan, added from the API, - // parsed from a freeform ticket description etc. - ChangingItemsBookmarkUUID []byte `protobuf:"bytes,7,opt,name=changingItemsBookmarkUUID,proto3" json:"changingItemsBookmarkUUID,omitempty"` - // UUID of a snapshot for the item queries of the items *indirectly* affected - // by this change i.e. the blast radius. The initial selection will be determined - // automatically based off changingItemsBookmark, but can refined by the user. - BlastRadiusSnapshotUUID []byte `protobuf:"bytes,11,opt,name=blastRadiusSnapshotUUID,proto3" json:"blastRadiusSnapshotUUID,omitempty"` - // UUID of the whole-system snapshot created before the change has started. - SystemBeforeSnapshotUUID []byte `protobuf:"bytes,8,opt,name=systemBeforeSnapshotUUID,proto3" json:"systemBeforeSnapshotUUID,omitempty"` - // UUID of the whole-system snapshot created after the change has finished. - SystemAfterSnapshotUUID []byte `protobuf:"bytes,9,opt,name=systemAfterSnapshotUUID,proto3" json:"systemAfterSnapshotUUID,omitempty"` - // a list of item diffs that were planned to be changed as part of this change. For all items that we could map, the ItemDiff.Reference will be set to the actual item found. - PlannedChanges []*ItemDiff `protobuf:"bytes,12,rep,name=plannedChanges,proto3" json:"plannedChanges,omitempty"` - // The raw plan output for calculating the change's risks. - RawPlan string `protobuf:"bytes,13,opt,name=rawPlan,proto3" json:"rawPlan,omitempty"` - // The code changes of this change for calculating the change's risks. - CodeChanges string `protobuf:"bytes,14,opt,name=codeChanges,proto3" json:"codeChanges,omitempty"` - // Repo information; can be an empty string. CLI attempts auto-population, but users can override. Not necessarily a URL. The UI will be responsible for any formatting/shortening/sprucing up should it be required. - Repo string `protobuf:"bytes,15,opt,name=repo,proto3" json:"repo,omitempty"` - // Tags that were set bu the user when the tag was created - // - // Deprecated: Use enrichedTags instead - // - // Deprecated: Marked as deprecated in changes.proto. - Tags map[string]string `protobuf:"bytes,16,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Tags associated with this change - EnrichedTags *EnrichedTags `protobuf:"bytes,18,opt,name=enrichedTags,proto3" json:"enrichedTags,omitempty"` - // labels - // note we keep track of the label type in the label struct itself - Labels []*Label `protobuf:"bytes,21,rep,name=labels,proto3" json:"labels,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeProperties) Reset() { - *x = ChangeProperties{} - mi := &file_changes_proto_msgTypes[57] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeProperties) ProtoMessage() {} - -func (x *ChangeProperties) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[57] - 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 ChangeProperties.ProtoReflect.Descriptor instead. -func (*ChangeProperties) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{57} -} - -func (x *ChangeProperties) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *ChangeProperties) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *ChangeProperties) GetTicketLink() string { - if x != nil { - return x.TicketLink - } - return "" -} - -func (x *ChangeProperties) GetOwner() string { - if x != nil { - return x.Owner - } - return "" -} - -func (x *ChangeProperties) GetCcEmails() string { - if x != nil { - return x.CcEmails - } - return "" -} - -func (x *ChangeProperties) GetChangingItemsBookmarkUUID() []byte { - if x != nil { - return x.ChangingItemsBookmarkUUID - } - return nil -} - -func (x *ChangeProperties) GetBlastRadiusSnapshotUUID() []byte { - if x != nil { - return x.BlastRadiusSnapshotUUID - } - return nil -} - -func (x *ChangeProperties) GetSystemBeforeSnapshotUUID() []byte { - if x != nil { - return x.SystemBeforeSnapshotUUID - } - return nil -} - -func (x *ChangeProperties) GetSystemAfterSnapshotUUID() []byte { - if x != nil { - return x.SystemAfterSnapshotUUID - } - return nil -} - -func (x *ChangeProperties) GetPlannedChanges() []*ItemDiff { - if x != nil { - return x.PlannedChanges - } - return nil -} - -func (x *ChangeProperties) GetRawPlan() string { - if x != nil { - return x.RawPlan - } - return "" -} - -func (x *ChangeProperties) GetCodeChanges() string { - if x != nil { - return x.CodeChanges - } - return "" -} - -func (x *ChangeProperties) GetRepo() string { - if x != nil { - return x.Repo - } - return "" -} - -// Deprecated: Marked as deprecated in changes.proto. -func (x *ChangeProperties) GetTags() map[string]string { - if x != nil { - return x.Tags - } - return nil -} - -func (x *ChangeProperties) GetEnrichedTags() *EnrichedTags { - if x != nil { - return x.EnrichedTags - } - return nil -} - -func (x *ChangeProperties) GetLabels() []*Label { - if x != nil { - return x.Labels - } - return nil -} - -// GithubChangeInfo contains information about a change that originated from GitHub -// contains mostly author information. -type GithubChangeInfo struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The GitHub username of the author of the change - AuthorUsername string `protobuf:"bytes,1,opt,name=authorUsername,proto3" json:"authorUsername,omitempty"` - // The author full name - AuthorFullName string `protobuf:"bytes,2,opt,name=authorFullName,proto3" json:"authorFullName,omitempty"` - // The link to the author's avatar - AuthorAvatarLink string `protobuf:"bytes,3,opt,name=authorAvatarLink,proto3" json:"authorAvatarLink,omitempty"` - // The email of the author - AuthorEmail string `protobuf:"bytes,4,opt,name=authorEmail,proto3" json:"authorEmail,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GithubChangeInfo) Reset() { - *x = GithubChangeInfo{} - mi := &file_changes_proto_msgTypes[58] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GithubChangeInfo) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GithubChangeInfo) ProtoMessage() {} - -func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[58] - 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 GithubChangeInfo.ProtoReflect.Descriptor instead. -func (*GithubChangeInfo) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{58} -} - -func (x *GithubChangeInfo) GetAuthorUsername() string { - if x != nil { - return x.AuthorUsername - } - return "" -} - -func (x *GithubChangeInfo) GetAuthorFullName() string { - if x != nil { - return x.AuthorFullName - } - return "" -} - -func (x *GithubChangeInfo) GetAuthorAvatarLink() string { - if x != nil { - return x.AuthorAvatarLink - } - return "" -} - -func (x *GithubChangeInfo) GetAuthorEmail() string { - if x != nil { - return x.AuthorEmail - } - return "" -} - -// list all changes -type ListChangesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesRequest) Reset() { - *x = ListChangesRequest{} - mi := &file_changes_proto_msgTypes[59] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesRequest) ProtoMessage() {} - -func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[59] - 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 ListChangesRequest.ProtoReflect.Descriptor instead. -func (*ListChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{59} -} - -type ListChangesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesResponse) Reset() { - *x = ListChangesResponse{} - mi := &file_changes_proto_msgTypes[60] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesResponse) ProtoMessage() {} - -func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[60] - 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 ListChangesResponse.ProtoReflect.Descriptor instead. -func (*ListChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{60} -} - -func (x *ListChangesResponse) GetChanges() []*Change { - if x != nil { - return x.Changes - } - return nil -} - -// list all changes in a specific status -type ListChangesByStatusRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status ChangeStatus `protobuf:"varint,1,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesByStatusRequest) Reset() { - *x = ListChangesByStatusRequest{} - mi := &file_changes_proto_msgTypes[61] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesByStatusRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesByStatusRequest) ProtoMessage() {} - -func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[61] - 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 ListChangesByStatusRequest.ProtoReflect.Descriptor instead. -func (*ListChangesByStatusRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{61} -} - -func (x *ListChangesByStatusRequest) GetStatus() ChangeStatus { - if x != nil { - return x.Status - } - return ChangeStatus_CHANGE_STATUS_UNSPECIFIED -} - -type ListChangesByStatusResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesByStatusResponse) Reset() { - *x = ListChangesByStatusResponse{} - mi := &file_changes_proto_msgTypes[62] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesByStatusResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesByStatusResponse) ProtoMessage() {} - -func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[62] - 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 ListChangesByStatusResponse.ProtoReflect.Descriptor instead. -func (*ListChangesByStatusResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{62} -} - -func (x *ListChangesByStatusResponse) GetChanges() []*Change { - if x != nil { - return x.Changes - } - return nil -} - -// create a new change -type CreateChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Properties *ChangeProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateChangeRequest) Reset() { - *x = CreateChangeRequest{} - mi := &file_changes_proto_msgTypes[63] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateChangeRequest) ProtoMessage() {} - -func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[63] - 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 CreateChangeRequest.ProtoReflect.Descriptor instead. -func (*CreateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{63} -} - -func (x *CreateChangeRequest) GetProperties() *ChangeProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateChangeResponse) Reset() { - *x = CreateChangeResponse{} - mi := &file_changes_proto_msgTypes[64] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateChangeResponse) ProtoMessage() {} - -func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[64] - 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 CreateChangeResponse.ProtoReflect.Descriptor instead. -func (*CreateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{64} -} - -func (x *CreateChangeResponse) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -// get the details of a specific change -type GetChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Return a slimmed down version of the change. This will exclude the - // following data: - // * `rawPlan`: The entire Terraform plan output - // * `codeChanges`: The code changes that created this change - Slim bool `protobuf:"varint,2,opt,name=slim,proto3" json:"slim,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeRequest) Reset() { - *x = GetChangeRequest{} - mi := &file_changes_proto_msgTypes[65] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeRequest) ProtoMessage() {} - -func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[65] - 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 GetChangeRequest.ProtoReflect.Descriptor instead. -func (*GetChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{65} -} - -func (x *GetChangeRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *GetChangeRequest) GetSlim() bool { - if x != nil { - return x.Slim - } - return false -} - -type GetChangeByTicketLinkRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - TicketLink string `protobuf:"bytes,1,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeByTicketLinkRequest) Reset() { - *x = GetChangeByTicketLinkRequest{} - mi := &file_changes_proto_msgTypes[66] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeByTicketLinkRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeByTicketLinkRequest) ProtoMessage() {} - -func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[66] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetChangeByTicketLinkRequest.ProtoReflect.Descriptor instead. -func (*GetChangeByTicketLinkRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{66} -} - -func (x *GetChangeByTicketLinkRequest) GetTicketLink() string { - if x != nil { - return x.TicketLink - } - return "" -} - -type GetChangeSummaryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Return a slimmed down version of the change. This will exclude the - // following data: - // * `rawPlan`: The entire Terraform plan output - // * `codeChanges`: The code changes that created this change - Slim bool `protobuf:"varint,2,opt,name=slim,proto3" json:"slim,omitempty"` - // currently json or markdown - ChangeOutputFormat ChangeOutputFormat `protobuf:"varint,3,opt,name=changeOutputFormat,proto3,enum=changes.ChangeOutputFormat" json:"changeOutputFormat,omitempty"` - // For filtering risks from display in the output - RiskSeverityFilter []Risk_Severity `protobuf:"varint,4,rep,packed,name=riskSeverityFilter,proto3,enum=changes.Risk_Severity" json:"riskSeverityFilter,omitempty"` - // this is the app url the user has been today - AppURL string `protobuf:"bytes,5,opt,name=appURL,proto3" json:"appURL,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeSummaryRequest) Reset() { - *x = GetChangeSummaryRequest{} - mi := &file_changes_proto_msgTypes[67] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeSummaryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeSummaryRequest) ProtoMessage() {} - -func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[67] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetChangeSummaryRequest.ProtoReflect.Descriptor instead. -func (*GetChangeSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{67} -} - -func (x *GetChangeSummaryRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *GetChangeSummaryRequest) GetSlim() bool { - if x != nil { - return x.Slim - } - return false -} - -func (x *GetChangeSummaryRequest) GetChangeOutputFormat() ChangeOutputFormat { - if x != nil { - return x.ChangeOutputFormat - } - return ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED -} - -func (x *GetChangeSummaryRequest) GetRiskSeverityFilter() []Risk_Severity { - if x != nil { - return x.RiskSeverityFilter - } - return nil -} - -func (x *GetChangeSummaryRequest) GetAppURL() string { - if x != nil { - return x.AppURL - } - return "" -} - -type GetChangeSummaryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Change string `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeSummaryResponse) Reset() { - *x = GetChangeSummaryResponse{} - mi := &file_changes_proto_msgTypes[68] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeSummaryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeSummaryResponse) ProtoMessage() {} - -func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[68] - 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 GetChangeSummaryResponse.ProtoReflect.Descriptor instead. -func (*GetChangeSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{68} -} - -func (x *GetChangeSummaryResponse) GetChange() string { - if x != nil { - return x.Change - } - return "" -} - -type GetChangeSignalsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // Output format for the signals data (json by default) - ChangeOutputFormat ChangeOutputFormat `protobuf:"varint,2,opt,name=changeOutputFormat,proto3,enum=changes.ChangeOutputFormat" json:"changeOutputFormat,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeSignalsRequest) Reset() { - *x = GetChangeSignalsRequest{} - mi := &file_changes_proto_msgTypes[69] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeSignalsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeSignalsRequest) ProtoMessage() {} - -func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[69] - 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 GetChangeSignalsRequest.ProtoReflect.Descriptor instead. -func (*GetChangeSignalsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{69} -} - -func (x *GetChangeSignalsRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *GetChangeSignalsRequest) GetChangeOutputFormat() ChangeOutputFormat { - if x != nil { - return x.ChangeOutputFormat - } - return ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED -} - -type GetChangeSignalsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signals string `protobuf:"bytes,1,opt,name=signals,proto3" json:"signals,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeSignalsResponse) Reset() { - *x = GetChangeSignalsResponse{} - mi := &file_changes_proto_msgTypes[70] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeSignalsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeSignalsResponse) ProtoMessage() {} - -func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[70] - 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 GetChangeSignalsResponse.ProtoReflect.Descriptor instead. -func (*GetChangeSignalsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{70} -} - -func (x *GetChangeSignalsResponse) GetSignals() string { - if x != nil { - return x.Signals - } - return "" -} - -type GetChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeResponse) Reset() { - *x = GetChangeResponse{} - mi := &file_changes_proto_msgTypes[71] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeResponse) ProtoMessage() {} - -func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[71] - 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 GetChangeResponse.ProtoReflect.Descriptor instead. -func (*GetChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{71} -} - -func (x *GetChangeResponse) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -// get the details of a specific change -type GetChangeRisksRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeRisksRequest) Reset() { - *x = GetChangeRisksRequest{} - mi := &file_changes_proto_msgTypes[72] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeRisksRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeRisksRequest) ProtoMessage() {} - -func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[72] - 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 GetChangeRisksRequest.ProtoReflect.Descriptor instead. -func (*GetChangeRisksRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{72} -} - -func (x *GetChangeRisksRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type ChangeRiskMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The status of the risk calculation - ChangeAnalysisStatus *ChangeAnalysisStatus `protobuf:"bytes,1,opt,name=changeAnalysisStatus,proto3" json:"changeAnalysisStatus,omitempty"` - // The risks that are related to this change - Risks []*Risk `protobuf:"bytes,5,rep,name=risks,proto3" json:"risks,omitempty"` - // The number of low risks in this change - NumLowRisk int32 `protobuf:"varint,6,opt,name=numLowRisk,proto3" json:"numLowRisk,omitempty"` - // The number of medium risks in this change - NumMediumRisk int32 `protobuf:"varint,7,opt,name=numMediumRisk,proto3" json:"numMediumRisk,omitempty"` - // The number of high risks in this change - NumHighRisk int32 `protobuf:"varint,8,opt,name=numHighRisk,proto3" json:"numHighRisk,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeRiskMetadata) Reset() { - *x = ChangeRiskMetadata{} - mi := &file_changes_proto_msgTypes[73] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeRiskMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeRiskMetadata) ProtoMessage() {} - -func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[73] - 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 ChangeRiskMetadata.ProtoReflect.Descriptor instead. -func (*ChangeRiskMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{73} -} - -func (x *ChangeRiskMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { - if x != nil { - return x.ChangeAnalysisStatus - } - return nil -} - -func (x *ChangeRiskMetadata) GetRisks() []*Risk { - if x != nil { - return x.Risks - } - return nil -} - -func (x *ChangeRiskMetadata) GetNumLowRisk() int32 { - if x != nil { - return x.NumLowRisk - } - return 0 -} - -func (x *ChangeRiskMetadata) GetNumMediumRisk() int32 { - if x != nil { - return x.NumMediumRisk - } - return 0 -} - -func (x *ChangeRiskMetadata) GetNumHighRisk() int32 { - if x != nil { - return x.NumHighRisk - } - return 0 -} - -type GetChangeRisksResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeRiskMetadata *ChangeRiskMetadata `protobuf:"bytes,1,opt,name=changeRiskMetadata,proto3" json:"changeRiskMetadata,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeRisksResponse) Reset() { - *x = GetChangeRisksResponse{} - mi := &file_changes_proto_msgTypes[74] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeRisksResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeRisksResponse) ProtoMessage() {} - -func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[74] - 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 GetChangeRisksResponse.ProtoReflect.Descriptor instead. -func (*GetChangeRisksResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{74} -} - -func (x *GetChangeRisksResponse) GetChangeRiskMetadata() *ChangeRiskMetadata { - if x != nil { - return x.ChangeRiskMetadata - } - return nil -} - -// update an existing change -type UpdateChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - Properties *ChangeProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateChangeRequest) Reset() { - *x = UpdateChangeRequest{} - mi := &file_changes_proto_msgTypes[75] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateChangeRequest) ProtoMessage() {} - -func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[75] - 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 UpdateChangeRequest.ProtoReflect.Descriptor instead. -func (*UpdateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{75} -} - -func (x *UpdateChangeRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *UpdateChangeRequest) GetProperties() *ChangeProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateChangeResponse) Reset() { - *x = UpdateChangeResponse{} - mi := &file_changes_proto_msgTypes[76] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateChangeResponse) ProtoMessage() {} - -func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[76] - 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 UpdateChangeResponse.ProtoReflect.Descriptor instead. -func (*UpdateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{76} -} - -func (x *UpdateChangeResponse) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -// delete a change -type DeleteChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteChangeRequest) Reset() { - *x = DeleteChangeRequest{} - mi := &file_changes_proto_msgTypes[77] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteChangeRequest) ProtoMessage() {} - -func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[77] - 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 DeleteChangeRequest.ProtoReflect.Descriptor instead. -func (*DeleteChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{77} -} - -func (x *DeleteChangeRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -// list changes for a snapshot UUID -type ListChangesBySnapshotUUIDRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesBySnapshotUUIDRequest) Reset() { - *x = ListChangesBySnapshotUUIDRequest{} - mi := &file_changes_proto_msgTypes[78] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesBySnapshotUUIDRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesBySnapshotUUIDRequest) ProtoMessage() {} - -func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[78] - 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 ListChangesBySnapshotUUIDRequest.ProtoReflect.Descriptor instead. -func (*ListChangesBySnapshotUUIDRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{78} -} - -func (x *ListChangesBySnapshotUUIDRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type ListChangesBySnapshotUUIDResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListChangesBySnapshotUUIDResponse) Reset() { - *x = ListChangesBySnapshotUUIDResponse{} - mi := &file_changes_proto_msgTypes[79] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListChangesBySnapshotUUIDResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListChangesBySnapshotUUIDResponse) ProtoMessage() {} - -func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[79] - 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 ListChangesBySnapshotUUIDResponse.ProtoReflect.Descriptor instead. -func (*ListChangesBySnapshotUUIDResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{79} -} - -func (x *ListChangesBySnapshotUUIDResponse) GetChanges() []*Change { - if x != nil { - return x.Changes - } - return nil -} - -type DeleteChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteChangeResponse) Reset() { - *x = DeleteChangeResponse{} - mi := &file_changes_proto_msgTypes[80] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteChangeResponse) ProtoMessage() {} - -func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[80] - 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 DeleteChangeResponse.ProtoReflect.Descriptor instead. -func (*DeleteChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{80} -} - -type RefreshStateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RefreshStateRequest) Reset() { - *x = RefreshStateRequest{} - mi := &file_changes_proto_msgTypes[81] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RefreshStateRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RefreshStateRequest) ProtoMessage() {} - -func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[81] - 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 RefreshStateRequest.ProtoReflect.Descriptor instead. -func (*RefreshStateRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{81} -} - -type RefreshStateResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RefreshStateResponse) Reset() { - *x = RefreshStateResponse{} - mi := &file_changes_proto_msgTypes[82] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RefreshStateResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RefreshStateResponse) ProtoMessage() {} - -func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[82] - 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 RefreshStateResponse.ProtoReflect.Descriptor instead. -func (*RefreshStateResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{82} -} - -type StartChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StartChangeRequest) Reset() { - *x = StartChangeRequest{} - mi := &file_changes_proto_msgTypes[83] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StartChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartChangeRequest) ProtoMessage() {} - -func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[83] - 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 StartChangeRequest.ProtoReflect.Descriptor instead. -func (*StartChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{83} -} - -func (x *StartChangeRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type StartChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - State StartChangeResponse_State `protobuf:"varint,1,opt,name=state,proto3,enum=changes.StartChangeResponse_State" json:"state,omitempty"` - NumItems uint32 `protobuf:"varint,2,opt,name=numItems,proto3" json:"numItems,omitempty"` - NumEdges uint32 `protobuf:"varint,3,opt,name=NumEdges,proto3" json:"NumEdges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StartChangeResponse) Reset() { - *x = StartChangeResponse{} - mi := &file_changes_proto_msgTypes[84] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StartChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartChangeResponse) ProtoMessage() {} - -func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[84] - 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 StartChangeResponse.ProtoReflect.Descriptor instead. -func (*StartChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84} -} - -func (x *StartChangeResponse) GetState() StartChangeResponse_State { - if x != nil { - return x.State - } - return StartChangeResponse_STATE_UNSPECIFIED -} - -func (x *StartChangeResponse) GetNumItems() uint32 { - if x != nil { - return x.NumItems - } - return 0 -} - -func (x *StartChangeResponse) GetNumEdges() uint32 { - if x != nil { - return x.NumEdges - } - return 0 -} - -type EndChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EndChangeRequest) Reset() { - *x = EndChangeRequest{} - mi := &file_changes_proto_msgTypes[85] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EndChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EndChangeRequest) ProtoMessage() {} - -func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[85] - 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 EndChangeRequest.ProtoReflect.Descriptor instead. -func (*EndChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{85} -} - -func (x *EndChangeRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type EndChangeResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - State EndChangeResponse_State `protobuf:"varint,1,opt,name=state,proto3,enum=changes.EndChangeResponse_State" json:"state,omitempty"` - NumItems uint32 `protobuf:"varint,2,opt,name=numItems,proto3" json:"numItems,omitempty"` - NumEdges uint32 `protobuf:"varint,3,opt,name=NumEdges,proto3" json:"NumEdges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EndChangeResponse) Reset() { - *x = EndChangeResponse{} - mi := &file_changes_proto_msgTypes[86] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EndChangeResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EndChangeResponse) ProtoMessage() {} - -func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[86] - 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 EndChangeResponse.ProtoReflect.Descriptor instead. -func (*EndChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86} -} - -func (x *EndChangeResponse) GetState() EndChangeResponse_State { - if x != nil { - return x.State - } - return EndChangeResponse_STATE_UNSPECIFIED -} - -func (x *EndChangeResponse) GetNumItems() uint32 { - if x != nil { - return x.NumItems - } - return 0 -} - -func (x *EndChangeResponse) GetNumEdges() uint32 { - if x != nil { - return x.NumEdges - } - return 0 -} - -type StartChangeSimpleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StartChangeSimpleResponse) Reset() { - *x = StartChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[87] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StartChangeSimpleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StartChangeSimpleResponse) ProtoMessage() {} - -func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[87] - 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 StartChangeSimpleResponse.ProtoReflect.Descriptor instead. -func (*StartChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{87} -} - -type EndChangeSimpleResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // True if the job was successfully enqueued (or queued to run after start-change) - Queued bool `protobuf:"varint,1,opt,name=queued,proto3" json:"queued,omitempty"` - // True if end-change was queued to run after start-change completes - QueuedAfterStart bool `protobuf:"varint,2,opt,name=queued_after_start,json=queuedAfterStart,proto3" json:"queued_after_start,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EndChangeSimpleResponse) Reset() { - *x = EndChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[88] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EndChangeSimpleResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EndChangeSimpleResponse) ProtoMessage() {} - -func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[88] - 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 EndChangeSimpleResponse.ProtoReflect.Descriptor instead. -func (*EndChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{88} -} - -func (x *EndChangeSimpleResponse) GetQueued() bool { - if x != nil { - return x.Queued - } - return false -} - -func (x *EndChangeSimpleResponse) GetQueuedAfterStart() bool { - if x != nil { - return x.QueuedAfterStart - } - return false -} - -type Risk struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,5,opt,name=UUID,proto3" json:"UUID,omitempty"` - Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` - Severity Risk_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=changes.Risk_Severity" json:"severity,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - RelatedItems []*Reference `protobuf:"bytes,4,rep,name=relatedItems,proto3" json:"relatedItems,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Risk) Reset() { - *x = Risk{} - mi := &file_changes_proto_msgTypes[89] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Risk) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Risk) ProtoMessage() {} - -func (x *Risk) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[89] - 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 Risk.ProtoReflect.Descriptor instead. -func (*Risk) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89} -} - -func (x *Risk) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *Risk) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *Risk) GetSeverity() Risk_Severity { - if x != nil { - return x.Severity - } - return Risk_SEVERITY_UNSPECIFIED -} - -func (x *Risk) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *Risk) GetRelatedItems() []*Reference { - if x != nil { - return x.RelatedItems - } - return nil -} - -type ChangeAnalysisStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status ChangeAnalysisStatus_Status `protobuf:"varint,1,opt,name=status,proto3,enum=changes.ChangeAnalysisStatus_Status" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeAnalysisStatus) Reset() { - *x = ChangeAnalysisStatus{} - mi := &file_changes_proto_msgTypes[90] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeAnalysisStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeAnalysisStatus) ProtoMessage() {} - -func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[90] - 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 ChangeAnalysisStatus.ProtoReflect.Descriptor instead. -func (*ChangeAnalysisStatus) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90} -} - -func (x *ChangeAnalysisStatus) GetStatus() ChangeAnalysisStatus_Status { - if x != nil { - return x.Status - } - return ChangeAnalysisStatus_STATUS_UNSPECIFIED -} - -// Generate fix suggestion for a risk -type GenerateRiskFixRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The UUID of the risk to generate a fix for - RiskUUID []byte `protobuf:"bytes,1,opt,name=riskUUID,proto3" json:"riskUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GenerateRiskFixRequest) Reset() { - *x = GenerateRiskFixRequest{} - mi := &file_changes_proto_msgTypes[91] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GenerateRiskFixRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GenerateRiskFixRequest) ProtoMessage() {} - -func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[91] - 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 GenerateRiskFixRequest.ProtoReflect.Descriptor instead. -func (*GenerateRiskFixRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{91} -} - -func (x *GenerateRiskFixRequest) GetRiskUUID() []byte { - if x != nil { - return x.RiskUUID - } - return nil -} - -type GenerateRiskFixResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The generated fix suggestion text - FixSuggestion string `protobuf:"bytes,1,opt,name=fixSuggestion,proto3" json:"fixSuggestion,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GenerateRiskFixResponse) Reset() { - *x = GenerateRiskFixResponse{} - mi := &file_changes_proto_msgTypes[92] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GenerateRiskFixResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GenerateRiskFixResponse) ProtoMessage() {} - -func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[92] - 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 GenerateRiskFixResponse.ProtoReflect.Descriptor instead. -func (*GenerateRiskFixResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{92} -} - -func (x *GenerateRiskFixResponse) GetFixSuggestion() string { - if x != nil { - return x.FixSuggestion - } - return "" -} - -// Represents the current state of a given health state, and the amount that -// it has changed. This doesn't just look at the change in total number of -// items, but also the number of items that have been added and removed, even -// if they were to add to the same number -type ChangeMetadata_HealthChange struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The number of items that were added to this health state as part of the - // change - Added int32 `protobuf:"varint,1,opt,name=added,proto3" json:"added,omitempty"` - // The number of items that were removed them this health state as part of - // the change - Removed int32 `protobuf:"varint,2,opt,name=removed,proto3" json:"removed,omitempty"` - // The final number of items that were in this health state - FinalTotal int32 `protobuf:"varint,3,opt,name=finalTotal,proto3" json:"finalTotal,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeMetadata_HealthChange) Reset() { - *x = ChangeMetadata_HealthChange{} - mi := &file_changes_proto_msgTypes[95] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeMetadata_HealthChange) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeMetadata_HealthChange) ProtoMessage() {} - -func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[95] - 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 ChangeMetadata_HealthChange.ProtoReflect.Descriptor instead. -func (*ChangeMetadata_HealthChange) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56, 0} -} - -func (x *ChangeMetadata_HealthChange) GetAdded() int32 { - if x != nil { - return x.Added - } - return 0 -} - -func (x *ChangeMetadata_HealthChange) GetRemoved() int32 { - if x != nil { - return x.Removed - } - return 0 -} - -func (x *ChangeMetadata_HealthChange) GetFinalTotal() int32 { - if x != nil { - return x.FinalTotal - } - return 0 -} - -var File_changes_proto protoreflect.FileDescriptor - -const file_changes_proto_rawDesc = "" + - "\n" + - "\rchanges.proto\x12\achanges\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\fconfig.proto\x1a\vitems.proto\x1a\n" + - "util.proto\"\x81\x01\n" + - "\tLabelRule\x126\n" + - "\bmetadata\x18\x01 \x01(\v2\x1a.changes.LabelRuleMetadataR\bmetadata\x12<\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + - "properties\"\xad\x01\n" + - "\x11LabelRuleMetadata\x12$\n" + - "\rLabelRuleUUID\x18\x01 \x01(\fR\rLabelRuleUUID\x128\n" + - "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x128\n" + - "\tupdatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"e\n" + - "\x13LabelRuleProperties\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + - "\x06colour\x18\x02 \x01(\tR\x06colour\x12\"\n" + - "\finstructions\x18\x03 \x01(\tR\finstructions\"\x17\n" + - "\x15ListLabelRulesRequest\"B\n" + - "\x16ListLabelRulesResponse\x12(\n" + - "\x05rules\x18\x01 \x03(\v2\x12.changes.LabelRuleR\x05rules\"V\n" + - "\x16CreateLabelRuleRequest\x12<\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + - "properties\"A\n" + - "\x17CreateLabelRuleResponse\x12&\n" + - "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\")\n" + - "\x13GetLabelRuleRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\">\n" + - "\x14GetLabelRuleResponse\x12&\n" + - "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\"j\n" + - "\x16UpdateLabelRuleRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12<\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + - "properties\"A\n" + - "\x17UpdateLabelRuleResponse\x12&\n" + - "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\",\n" + - "\x16DeleteLabelRuleRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x19\n" + - "\x17DeleteLabelRuleResponse\"t\n" + - "\x14TestLabelRuleRequest\x12<\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + - "properties\x12\x1e\n" + - "\n" + - "changeUUID\x18\x02 \x03(\fR\n" + - "changeUUID\"w\n" + - "\x15TestLabelRuleResponse\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\x12\x18\n" + - "\aapplied\x18\x02 \x01(\bR\aapplied\x12$\n" + - "\x05label\x18\x03 \x01(\v2\x0e.changes.LabelR\x05label\"\xa0\x01\n" + - "\"ReapplyLabelRuleInTimeRangeRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + - "\astartAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\astartAt\x120\n" + - "\x05endAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05endAt\"E\n" + - "#ReapplyLabelRuleInTimeRangeResponse\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x03(\fR\n" + - "changeUUID\"=\n" + - "\x1bGetHypothesesDetailsRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"Z\n" + - "\x1cGetHypothesesDetailsResponse\x12:\n" + - "\n" + - "hypotheses\x18\x01 \x03(\v2\x1a.changes.HypothesesDetailsR\n" + - "hypotheses\"\xd2\x01\n" + - "\x11HypothesesDetails\x12\x14\n" + - "\x05title\x18\x01 \x01(\tR\x05title\x12(\n" + - "\x0fnumObservations\x18\x02 \x01(\rR\x0fnumObservations\x12\x16\n" + - "\x06detail\x18\x03 \x01(\tR\x06detail\x121\n" + - "\x06status\x18\x04 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x122\n" + - "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\"<\n" + - "\x1aGetChangeTimelineV2Request\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"W\n" + - "\x1bGetChangeTimelineV2Response\x128\n" + - "\aentries\x18\x01 \x03(\v2\x1e.changes.ChangeTimelineEntryV2R\aentries\"\xd6\b\n" + - "\x15ChangeTimelineEntryV2\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12:\n" + - "\x06status\x18\x02 \x01(\x0e2\".changes.ChangeTimelineEntryStatusR\x06status\x12=\n" + - "\tstartedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\tstartedAt\x88\x01\x01\x129\n" + - "\aendedAt\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x02R\aendedAt\x88\x01\x01\x12\x19\n" + - "\x05actor\x18\x05 \x01(\tH\x03R\x05actor\x88\x01\x01\x12E\n" + - "\vmappedItems\x18\a \x01(\v2!.changes.MappedItemsTimelineEntryH\x00R\vmappedItems\x12c\n" + - "\x15calculatedBlastRadius\x18\b \x01(\v2+.changes.CalculatedBlastRadiusTimelineEntryH\x00R\x15calculatedBlastRadius\x12Q\n" + - "\x0fcalculatedRisks\x18\t \x01(\v2%.changes.CalculatedRisksTimelineEntryH\x00R\x0fcalculatedRisks\x12\x16\n" + - "\x05error\x18\v \x01(\tH\x00R\x05error\x12&\n" + - "\rstatusMessage\x18\f \x01(\tH\x00R\rstatusMessage\x12-\n" + - "\x05empty\x18\r \x01(\v2\x15.changes.EmptyContentH\x00R\x05empty\x12T\n" + - "\x10changeValidation\x18\x0e \x01(\v2&.changes.ChangeValidationTimelineEntryH\x00R\x10changeValidation\x12T\n" + - "\x10calculatedLabels\x18\x0f \x01(\v2&.changes.CalculatedLabelsTimelineEntryH\x00R\x10calculatedLabels\x12N\n" + - "\x0eformHypotheses\x18\x10 \x01(\v2$.changes.FormHypothesesTimelineEntryH\x00R\x0eformHypotheses\x12c\n" + - "\x15investigateHypotheses\x18\x11 \x01(\v2+.changes.InvestigateHypothesesTimelineEntryH\x00R\x15investigateHypotheses\x12Z\n" + - "\x12recordObservations\x18\x12 \x01(\v2(.changes.RecordObservationsTimelineEntryH\x00R\x12recordObservationsB\t\n" + - "\acontentB\f\n" + - "\n" + - "_startedAtB\n" + - "\n" + - "\b_endedAtB\b\n" + - "\x06_actor\"\x0e\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" + - "\x1fRecordObservationsTimelineEntry\x12(\n" + - "\x0fnumObservations\x18\x01 \x01(\rR\x0fnumObservations\"\x7f\n" + - "\x1bFormHypothesesTimelineEntry\x12$\n" + - "\rnumHypotheses\x18\x01 \x01(\rR\rnumHypotheses\x12:\n" + - "\n" + - "hypotheses\x18\x02 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"\xce\x01\n" + - "\"InvestigateHypothesesTimelineEntry\x12\x1c\n" + - "\tnumProven\x18\x01 \x01(\rR\tnumProven\x12\"\n" + - "\fnumDisproven\x18\x02 \x01(\rR\fnumDisproven\x12*\n" + - "\x10numInvestigating\x18\x03 \x01(\rR\x10numInvestigating\x12:\n" + - "\n" + - "hypotheses\x18\x04 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"t\n" + - "\x11HypothesisSummary\x121\n" + - "\x06status\x18\x01 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + - "\x06detail\x18\x03 \x01(\tR\x06detail\"C\n" + - "\x1cCalculatedRisksTimelineEntry\x12#\n" + - "\x05risks\x18\x01 \x03(\v2\r.changes.RiskR\x05risks\"G\n" + - "\x1dCalculatedLabelsTimelineEntry\x12&\n" + - "\x06labels\x18\x01 \x03(\v2\x0e.changes.LabelR\x06labels\"\x9a\x01\n" + - "\x1dChangeValidationTimelineEntry\x12$\n" + - "\rbriefAnalysis\x18\x01 \x01(\tR\rbriefAnalysis\x12S\n" + - "\x13validationChecklist\x18\x02 \x03(\v2!.changes.ChangeValidationCategoryR\x13validationChecklist\"R\n" + - "\x18ChangeValidationCategory\x12\x14\n" + - "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\"0\n" + - "\x0eGetDiffRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"\xdb\x01\n" + - "\x0fGetDiffResponse\x127\n" + - "\rexpectedItems\x18\x01 \x03(\v2\x11.changes.ItemDiffR\rexpectedItems\x12;\n" + - "\x0funexpectedItems\x18\x03 \x03(\v2\x11.changes.ItemDiffR\x0funexpectedItems\x12\x1b\n" + - "\x05edges\x18\x02 \x03(\v2\x05.EdgeR\x05edges\x125\n" + - "\fmissingItems\x18\x04 \x03(\v2\x11.changes.ItemDiffR\fmissingItems\"A\n" + - "\x1fListChangingItemsSummaryRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"R\n" + - " ListChangingItemsSummaryResponse\x12.\n" + - "\x05items\x18\x01 \x03(\v2\x18.changes.ItemDiffSummaryR\x05items\"\xa1\x02\n" + - "\x0eMappedItemDiff\x12%\n" + - "\x04item\x18\x01 \x01(\v2\x11.changes.ItemDiffR\x04item\x12/\n" + - "\fmappingQuery\x18\x02 \x01(\v2\x06.QueryH\x00R\fmappingQuery\x88\x01\x01\x124\n" + - "\fmappingError\x18\x03 \x01(\v2\v.QueryErrorH\x01R\fmappingError\x88\x01\x01\x12L\n" + - "\x0emapping_status\x18\x04 \x01(\x0e2 .changes.MappedItemMappingStatusH\x02R\rmappingStatus\x88\x01\x01B\x0f\n" + - "\r_mappingQueryB\x0f\n" + - "\r_mappingErrorB\x11\n" + - "\x0f_mapping_status\"\xa1\x04\n" + - "\x1aStartChangeAnalysisRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\x12=\n" + - "\rchangingItems\x18\x02 \x03(\v2\x17.changes.MappedItemDiffR\rchangingItems\x12\\\n" + - "\x19blastRadiusConfigOverride\x18\x03 \x01(\v2\x19.config.BlastRadiusConfigH\x00R\x19blastRadiusConfigOverride\x88\x01\x01\x12e\n" + - "\x1croutineChangesConfigOverride\x18\x05 \x01(\v2\x1c.config.RoutineChangesConfigH\x01R\x1croutineChangesConfigOverride\x88\x01\x01\x12t\n" + - "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01B\x1c\n" + - "\x1a_blastRadiusConfigOverrideB\x1f\n" + - "\x1d_routineChangesConfigOverrideB$\n" + - "\"_githubOrganisationProfileOverrideJ\x04\b\x04\x10\x05\"\x1d\n" + - "\x1bStartChangeAnalysisResponse\"\x96\x01\n" + - "\x16ListHomeChangesRequest\x122\n" + - "\n" + - "pagination\x18\x01 \x01(\v2\x12.PaginationRequestR\n" + - "pagination\x12<\n" + - "\afilters\x18\x02 \x01(\v2\x1d.changes.ChangeFiltersRequestH\x00R\afilters\x88\x01\x01B\n" + - "\n" + - "\b_filters\"\x82\x02\n" + - "\x14ChangeFiltersRequest\x12\x14\n" + - "\x05repos\x18\x01 \x03(\tR\x05repos\x12,\n" + - "\x05risks\x18\x03 \x03(\x0e2\x16.changes.Risk.SeverityR\x05risks\x12\x18\n" + - "\aauthors\x18\x04 \x03(\tR\aauthors\x121\n" + - "\bstatuses\x18\x05 \x03(\x0e2\x15.changes.ChangeStatusR\bstatuses\x12-\n" + - "\tsortOrder\x18\x06 \x01(\x0e2\n" + - ".SortOrderH\x00R\tsortOrder\x88\x01\x01\x12\x16\n" + - "\x06labels\x18\a \x03(\tR\x06labelsB\f\n" + - "\n" + - "_sortOrderJ\x04\b\x02\x10\x03\"\x80\x01\n" + - "\x17ListHomeChangesResponse\x120\n" + - "\achanges\x18\x01 \x03(\v2\x16.changes.ChangeSummaryR\achanges\x123\n" + - "\n" + - "pagination\x18\x02 \x01(\v2\x13.PaginationResponseR\n" + - "pagination\"\x1e\n" + - "\x1cPopulateChangeFiltersRequest\"O\n" + - "\x1dPopulateChangeFiltersResponse\x12\x14\n" + - "\x05repos\x18\x01 \x03(\tR\x05repos\x12\x18\n" + - "\aauthors\x18\x02 \x03(\tR\aauthors\"\x8d\x01\n" + - "\x0fItemDiffSummary\x12\x1e\n" + - "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12/\n" + - "\x06status\x18\x04 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\x12)\n" + - "\vhealthAfter\x18\x05 \x01(\x0e2\a.HealthR\vhealthAfter\"\xd7\x01\n" + - "\bItemDiff\x12#\n" + - "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceH\x00R\x04item\x88\x01\x01\x12/\n" + - "\x06status\x18\x02 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\x12\x1d\n" + - "\x06before\x18\x03 \x01(\v2\x05.ItemR\x06before\x12\x1b\n" + - "\x05after\x18\x04 \x01(\v2\x05.ItemR\x05after\x120\n" + - "\x13modificationSummary\x18\x05 \x01(\tR\x13modificationSummaryB\a\n" + - "\x05_item\"\x9f\x01\n" + - "\fEnrichedTags\x12?\n" + - "\btagValue\x18\x12 \x03(\v2#.changes.EnrichedTags.TagValueEntryR\btagValue\x1aN\n" + - "\rTagValueEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12'\n" + - "\x05value\x18\x02 \x01(\v2\x11.changes.TagValueR\x05value:\x028\x01\"\x8d\x01\n" + - "\bTagValue\x12;\n" + - "\fuserTagValue\x18\x01 \x01(\v2\x15.changes.UserTagValueH\x00R\fuserTagValue\x12;\n" + - "\fautoTagValue\x18\x02 \x01(\v2\x15.changes.AutoTagValueH\x00R\fautoTagValueB\a\n" + - "\x05value\"$\n" + - "\fUserTagValue\x12\x14\n" + - "\x05value\x18\x01 \x01(\tR\x05value\"B\n" + - "\fAutoTagValue\x12\x14\n" + - "\x05value\x18\x01 \x01(\tR\x05value\x12\x1c\n" + - "\treasoning\x18\x02 \x01(\tR\treasoning\"\xcb\x01\n" + - "\x05Label\x12&\n" + - "\x04type\x18\x01 \x01(\x0e2\x12.changes.LabelTypeR\x04type\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + - "\x06colour\x18\x03 \x01(\tR\x06colour\x12$\n" + - "\rlabelRuleUUID\x18\x04 \x01(\fR\rlabelRuleUUID\x12.\n" + - "\x12autoLabelReasoning\x18\x05 \x01(\tR\x12autoLabelReasoning\x12\x18\n" + - "\askipped\x18\x06 \x01(\bR\askipped\"\xa1\x06\n" + - "\rChangeSummary\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\x12-\n" + - "\x06status\x18\x03 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\x12\x1e\n" + - "\n" + - "ticketLink\x18\x04 \x01(\tR\n" + - "ticketLink\x128\n" + - "\tcreatedAt\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12 \n" + - "\vcreatorName\x18\x06 \x01(\tR\vcreatorName\x12\"\n" + - "\fcreatorEmail\x18\x0f \x01(\tR\fcreatorEmail\x12*\n" + - "\x10numAffectedItems\x18\t \x01(\x05R\x10numAffectedItems\x12*\n" + - "\x10numAffectedEdges\x18\n" + - " \x01(\x05R\x10numAffectedEdges\x12\x1e\n" + - "\n" + - "numLowRisk\x18\v \x01(\x05R\n" + - "numLowRisk\x12$\n" + - "\rnumMediumRisk\x18\f \x01(\x05R\rnumMediumRisk\x12 \n" + - "\vnumHighRisk\x18\r \x01(\x05R\vnumHighRisk\x12 \n" + - "\vdescription\x18\x0e \x01(\tR\vdescription\x12\x12\n" + - "\x04repo\x18\x10 \x01(\tR\x04repo\x128\n" + - "\x04tags\x18\x11 \x03(\v2 .changes.ChangeSummary.TagsEntryB\x02\x18\x01R\x04tags\x129\n" + - "\fenrichedTags\x18\x12 \x01(\v2\x15.changes.EnrichedTagsR\fenrichedTags\x12&\n" + - "\x06labels\x18\x13 \x03(\v2\x0e.changes.LabelR\x06labels\x12E\n" + - "\x10githubChangeInfo\x18\x14 \x01(\v2\x19.changes.GithubChangeInfoR\x10githubChangeInfo\x1a7\n" + - "\tTagsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\b\x10\t\"x\n" + - "\x06Change\x123\n" + - "\bmetadata\x18\x01 \x01(\v2\x17.changes.ChangeMetadataR\bmetadata\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.changes.ChangePropertiesR\n" + - "properties\"\xdf\t\n" + - "\x0eChangeMetadata\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x128\n" + - "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x128\n" + - "\tupdatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12-\n" + - "\x06status\x18\x04 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\x12 \n" + - "\vcreatorName\x18\x05 \x01(\tR\vcreatorName\x12\"\n" + - "\fcreatorEmail\x18\x13 \x01(\tR\fcreatorEmail\x12*\n" + - "\x10numAffectedItems\x18\a \x01(\x05R\x10numAffectedItems\x12*\n" + - "\x10numAffectedEdges\x18\x11 \x01(\x05R\x10numAffectedEdges\x12,\n" + - "\x11numUnchangedItems\x18\b \x01(\x05R\x11numUnchangedItems\x12(\n" + - "\x0fnumCreatedItems\x18\t \x01(\x05R\x0fnumCreatedItems\x12(\n" + - "\x0fnumUpdatedItems\x18\n" + - " \x01(\x05R\x0fnumUpdatedItems\x12*\n" + - "\x10numReplacedItems\x18\x12 \x01(\x05R\x10numReplacedItems\x12(\n" + - "\x0fnumDeletedItems\x18\v \x01(\x05R\x0fnumDeletedItems\x12V\n" + - "\x13UnknownHealthChange\x18\f \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13UnknownHealthChange\x12L\n" + - "\x0eOkHealthChange\x18\r \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x0eOkHealthChange\x12V\n" + - "\x13WarningHealthChange\x18\x0e \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13WarningHealthChange\x12R\n" + - "\x11ErrorHealthChange\x18\x0f \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x11ErrorHealthChange\x12V\n" + - "\x13PendingHealthChange\x18\x10 \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13PendingHealthChange\x12E\n" + - "\x10githubChangeInfo\x18\x14 \x01(\v2\x19.changes.GithubChangeInfoR\x10githubChangeInfo\x122\n" + - "\x12total_observations\x18\x15 \x01(\rH\x00R\x11totalObservations\x88\x01\x01\x1a^\n" + - "\fHealthChange\x12\x14\n" + - "\x05added\x18\x01 \x01(\x05R\x05added\x12\x18\n" + - "\aremoved\x18\x02 \x01(\x05R\aremoved\x12\x1e\n" + - "\n" + - "finalTotal\x18\x03 \x01(\x05R\n" + - "finalTotalB\x15\n" + - "\x13_total_observationsJ\x04\b\x06\x10\a\"\x8c\x06\n" + - "\x10ChangeProperties\x12\x14\n" + - "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + - "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1e\n" + - "\n" + - "ticketLink\x18\x04 \x01(\tR\n" + - "ticketLink\x12\x14\n" + - "\x05owner\x18\x05 \x01(\tR\x05owner\x12\x1a\n" + - "\bccEmails\x18\x06 \x01(\tR\bccEmails\x12<\n" + - "\x19changingItemsBookmarkUUID\x18\a \x01(\fR\x19changingItemsBookmarkUUID\x128\n" + - "\x17blastRadiusSnapshotUUID\x18\v \x01(\fR\x17blastRadiusSnapshotUUID\x12:\n" + - "\x18systemBeforeSnapshotUUID\x18\b \x01(\fR\x18systemBeforeSnapshotUUID\x128\n" + - "\x17systemAfterSnapshotUUID\x18\t \x01(\fR\x17systemAfterSnapshotUUID\x129\n" + - "\x0eplannedChanges\x18\f \x03(\v2\x11.changes.ItemDiffR\x0eplannedChanges\x12\x18\n" + - "\arawPlan\x18\r \x01(\tR\arawPlan\x12 \n" + - "\vcodeChanges\x18\x0e \x01(\tR\vcodeChanges\x12\x12\n" + - "\x04repo\x18\x0f \x01(\tR\x04repo\x12;\n" + - "\x04tags\x18\x10 \x03(\v2#.changes.ChangeProperties.TagsEntryB\x02\x18\x01R\x04tags\x129\n" + - "\fenrichedTags\x18\x12 \x01(\v2\x15.changes.EnrichedTagsR\fenrichedTags\x12&\n" + - "\x06labels\x18\x15 \x03(\v2\x0e.changes.LabelR\x06labels\x1a7\n" + - "\tTagsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02J\x04\b\n" + - "\x10\vJ\x04\b\x11\x10\x12J\x04\b\x13\x10\x14J\x04\b\x14\x10\x15\"\xb0\x01\n" + - "\x10GithubChangeInfo\x12&\n" + - "\x0eauthorUsername\x18\x01 \x01(\tR\x0eauthorUsername\x12&\n" + - "\x0eauthorFullName\x18\x02 \x01(\tR\x0eauthorFullName\x12*\n" + - "\x10authorAvatarLink\x18\x03 \x01(\tR\x10authorAvatarLink\x12 \n" + - "\vauthorEmail\x18\x04 \x01(\tR\vauthorEmail\"\x14\n" + - "\x12ListChangesRequest\"@\n" + - "\x13ListChangesResponse\x12)\n" + - "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"K\n" + - "\x1aListChangesByStatusRequest\x12-\n" + - "\x06status\x18\x01 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\"H\n" + - "\x1bListChangesByStatusResponse\x12)\n" + - "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"P\n" + - "\x13CreateChangeRequest\x129\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x19.changes.ChangePropertiesR\n" + - "properties\"?\n" + - "\x14CreateChangeResponse\x12'\n" + - "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\":\n" + - "\x10GetChangeRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + - "\x04slim\x18\x02 \x01(\bR\x04slim\">\n" + - "\x1cGetChangeByTicketLinkRequest\x12\x1e\n" + - "\n" + - "ticketLink\x18\x01 \x01(\tR\n" + - "ticketLink\"\xee\x01\n" + - "\x17GetChangeSummaryRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + - "\x04slim\x18\x02 \x01(\bR\x04slim\x12K\n" + - "\x12changeOutputFormat\x18\x03 \x01(\x0e2\x1b.changes.ChangeOutputFormatR\x12changeOutputFormat\x12F\n" + - "\x12riskSeverityFilter\x18\x04 \x03(\x0e2\x16.changes.Risk.SeverityR\x12riskSeverityFilter\x12\x16\n" + - "\x06appURL\x18\x05 \x01(\tR\x06appURL\"2\n" + - "\x18GetChangeSummaryResponse\x12\x16\n" + - "\x06change\x18\x01 \x01(\tR\x06change\"z\n" + - "\x17GetChangeSignalsRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12K\n" + - "\x12changeOutputFormat\x18\x02 \x01(\x0e2\x1b.changes.ChangeOutputFormatR\x12changeOutputFormat\"4\n" + - "\x18GetChangeSignalsResponse\x12\x18\n" + - "\asignals\x18\x01 \x01(\tR\asignals\"<\n" + - "\x11GetChangeResponse\x12'\n" + - "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\"+\n" + - "\x15GetChangeRisksRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\xf4\x01\n" + - "\x12ChangeRiskMetadata\x12Q\n" + - "\x14changeAnalysisStatus\x18\x01 \x01(\v2\x1d.changes.ChangeAnalysisStatusR\x14changeAnalysisStatus\x12#\n" + - "\x05risks\x18\x05 \x03(\v2\r.changes.RiskR\x05risks\x12\x1e\n" + - "\n" + - "numLowRisk\x18\x06 \x01(\x05R\n" + - "numLowRisk\x12$\n" + - "\rnumMediumRisk\x18\a \x01(\x05R\rnumMediumRisk\x12 \n" + - "\vnumHighRisk\x18\b \x01(\x05R\vnumHighRisk\"e\n" + - "\x16GetChangeRisksResponse\x12K\n" + - "\x12changeRiskMetadata\x18\x01 \x01(\v2\x1b.changes.ChangeRiskMetadataR\x12changeRiskMetadata\"d\n" + - "\x13UpdateChangeRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x129\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x19.changes.ChangePropertiesR\n" + - "properties\"?\n" + - "\x14UpdateChangeResponse\x12'\n" + - "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\")\n" + - "\x13DeleteChangeRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"6\n" + - " ListChangesBySnapshotUUIDRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"N\n" + - "!ListChangesBySnapshotUUIDResponse\x12)\n" + - "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"\x16\n" + - "\x14DeleteChangeResponse\"\x15\n" + - "\x13RefreshStateRequest\"\x16\n" + - "\x14RefreshStateResponse\"4\n" + - "\x12StartChangeRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"\xed\x01\n" + - "\x13StartChangeResponse\x128\n" + - "\x05state\x18\x01 \x01(\x0e2\".changes.StartChangeResponse.StateR\x05state\x12\x1a\n" + - "\bnumItems\x18\x02 \x01(\rR\bnumItems\x12\x1a\n" + - "\bNumEdges\x18\x03 \x01(\rR\bNumEdges\"d\n" + - "\x05State\x12\x15\n" + - "\x11STATE_UNSPECIFIED\x10\x00\x12\x19\n" + - "\x15STATE_TAKING_SNAPSHOT\x10\x01\x12\x19\n" + - "\x15STATE_SAVING_SNAPSHOT\x10\x02\x12\x0e\n" + - "\n" + - "STATE_DONE\x10\x03\"2\n" + - "\x10EndChangeRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"\xe9\x01\n" + - "\x11EndChangeResponse\x126\n" + - "\x05state\x18\x01 \x01(\x0e2 .changes.EndChangeResponse.StateR\x05state\x12\x1a\n" + - "\bnumItems\x18\x02 \x01(\rR\bnumItems\x12\x1a\n" + - "\bNumEdges\x18\x03 \x01(\rR\bNumEdges\"d\n" + - "\x05State\x12\x15\n" + - "\x11STATE_UNSPECIFIED\x10\x00\x12\x19\n" + - "\x15STATE_TAKING_SNAPSHOT\x10\x01\x12\x19\n" + - "\x15STATE_SAVING_SNAPSHOT\x10\x02\x12\x0e\n" + - "\n" + - "STATE_DONE\x10\x03\"\x1b\n" + - "\x19StartChangeSimpleResponse\"_\n" + - "\x17EndChangeSimpleResponse\x12\x16\n" + - "\x06queued\x18\x01 \x01(\bR\x06queued\x12,\n" + - "\x12queued_after_start\x18\x02 \x01(\bR\x10queuedAfterStart\"\x96\x02\n" + - "\x04Risk\x12\x12\n" + - "\x04UUID\x18\x05 \x01(\fR\x04UUID\x12\x14\n" + - "\x05title\x18\x01 \x01(\tR\x05title\x122\n" + - "\bseverity\x18\x02 \x01(\x0e2\x16.changes.Risk.SeverityR\bseverity\x12 \n" + - "\vdescription\x18\x03 \x01(\tR\vdescription\x12.\n" + - "\frelatedItems\x18\x04 \x03(\v2\n" + - ".ReferenceR\frelatedItems\"^\n" + - "\bSeverity\x12\x18\n" + - "\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x10\n" + - "\fSEVERITY_LOW\x10\x01\x12\x13\n" + - "\x0fSEVERITY_MEDIUM\x10\x02\x12\x11\n" + - "\rSEVERITY_HIGH\x10\x03\"\xca\x01\n" + - "\x14ChangeAnalysisStatus\x12<\n" + - "\x06status\x18\x01 \x01(\x0e2$.changes.ChangeAnalysisStatus.StatusR\x06status\"n\n" + - "\x06Status\x12\x16\n" + - "\x12STATUS_UNSPECIFIED\x10\x00\x12\x15\n" + - "\x11STATUS_INPROGRESS\x10\x01\x12\x12\n" + - "\x0eSTATUS_SKIPPED\x10\x02\x12\x0f\n" + - "\vSTATUS_DONE\x10\x03\x12\x10\n" + - "\fSTATUS_ERROR\x10\x04J\x04\b\x05\x10\x06\"4\n" + - "\x16GenerateRiskFixRequest\x12\x1a\n" + - "\briskUUID\x18\x01 \x01(\fR\briskUUID\"?\n" + - "\x17GenerateRiskFixResponse\x12$\n" + - "\rfixSuggestion\x18\x01 \x01(\tR\rfixSuggestion*\xf6\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\x120\n" + - ",MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION\x10\x04*\xf0\x01\n" + - "\x17MappedItemMappingStatus\x12*\n" + - "&MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED\x10\x00\x12&\n" + - "\"MAPPED_ITEM_MAPPING_STATUS_SUCCESS\x10\x01\x12*\n" + - "&MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED\x10\x02\x12/\n" + - "+MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION\x10\x03\x12$\n" + - " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xf9\x01\n" + - "\x10HypothesisStatus\x12.\n" + - "*INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED\x10\x00\x12*\n" + - "&INVESTIGATED_HYPOTHESIS_STATUS_FORMING\x10\x01\x120\n" + - ",INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING\x10\x02\x12)\n" + - "%INVESTIGATED_HYPOTHESIS_STATUS_PROVEN\x10\x03\x12,\n" + - "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04*_\n" + - "\x19ChangeTimelineEntryStatus\x12\x0f\n" + - "\vUNSPECIFIED\x10\x00\x12\v\n" + - "\aPENDING\x10\x01\x12\x0f\n" + - "\vIN_PROGRESS\x10\x02\x12\b\n" + - "\x04DONE\x10\x03\x12\t\n" + - "\x05ERROR\x10\x04*\xcb\x01\n" + - "\x0eItemDiffStatus\x12 \n" + - "\x1cITEM_DIFF_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + - "\x1aITEM_DIFF_STATUS_UNCHANGED\x10\x01\x12\x1c\n" + - "\x18ITEM_DIFF_STATUS_CREATED\x10\x02\x12\x1c\n" + - "\x18ITEM_DIFF_STATUS_UPDATED\x10\x03\x12\x1c\n" + - "\x18ITEM_DIFF_STATUS_DELETED\x10\x04\x12\x1d\n" + - "\x19ITEM_DIFF_STATUS_REPLACED\x10\x05*|\n" + - "\x12ChangeOutputFormat\x12$\n" + - " CHANGE_OUTPUT_FORMAT_UNSPECIFIED\x10\x00\x12\x1d\n" + - "\x19CHANGE_OUTPUT_FORMAT_JSON\x10\x01\x12!\n" + - "\x1dCHANGE_OUTPUT_FORMAT_MARKDOWN\x10\x02*Q\n" + - "\tLabelType\x12\x1a\n" + - "\x16LABEL_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + - "\x0fLABEL_TYPE_AUTO\x10\x01\x12\x13\n" + - "\x0fLABEL_TYPE_USER\x10\x02*\xa0\x01\n" + - "\fChangeStatus\x12\x1d\n" + - "\x19CHANGE_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + - "\x16CHANGE_STATUS_DEFINING\x10\x01\x12\x1b\n" + - "\x17CHANGE_STATUS_HAPPENING\x10\x02\x12 \n" + - "\x18CHANGE_STATUS_PROCESSING\x10\x03\x1a\x02\b\x01\x12\x16\n" + - "\x12CHANGE_STATUS_DONE\x10\x042\xad\x10\n" + - "\x0eChangesService\x12H\n" + - "\vListChanges\x12\x1b.changes.ListChangesRequest\x1a\x1c.changes.ListChangesResponse\x12`\n" + - "\x13ListChangesByStatus\x12#.changes.ListChangesByStatusRequest\x1a$.changes.ListChangesByStatusResponse\x12K\n" + - "\fCreateChange\x12\x1c.changes.CreateChangeRequest\x1a\x1d.changes.CreateChangeResponse\x12B\n" + - "\tGetChange\x12\x19.changes.GetChangeRequest\x1a\x1a.changes.GetChangeResponse\x12Z\n" + - "\x15GetChangeByTicketLink\x12%.changes.GetChangeByTicketLinkRequest\x1a\x1a.changes.GetChangeResponse\x12W\n" + - "\x10GetChangeSummary\x12 .changes.GetChangeSummaryRequest\x1a!.changes.GetChangeSummaryResponse\x12`\n" + - "\x13GetChangeTimelineV2\x12#.changes.GetChangeTimelineV2Request\x1a$.changes.GetChangeTimelineV2Response\x12Q\n" + - "\x0eGetChangeRisks\x12\x1e.changes.GetChangeRisksRequest\x1a\x1f.changes.GetChangeRisksResponse\x12K\n" + - "\fUpdateChange\x12\x1c.changes.UpdateChangeRequest\x1a\x1d.changes.UpdateChangeResponse\x12K\n" + - "\fDeleteChange\x12\x1c.changes.DeleteChangeRequest\x1a\x1d.changes.DeleteChangeResponse\x12r\n" + - "\x19ListChangesBySnapshotUUID\x12).changes.ListChangesBySnapshotUUIDRequest\x1a*.changes.ListChangesBySnapshotUUIDResponse\x12K\n" + - "\fRefreshState\x12\x1c.changes.RefreshStateRequest\x1a\x1d.changes.RefreshStateResponse\x12J\n" + - "\vStartChange\x12\x1b.changes.StartChangeRequest\x1a\x1c.changes.StartChangeResponse0\x01\x12D\n" + - "\tEndChange\x12\x19.changes.EndChangeRequest\x1a\x1a.changes.EndChangeResponse0\x01\x12T\n" + - "\x11StartChangeSimple\x12\x1b.changes.StartChangeRequest\x1a\".changes.StartChangeSimpleResponse\x12N\n" + - "\x0fEndChangeSimple\x12\x19.changes.EndChangeRequest\x1a .changes.EndChangeSimpleResponse\x12T\n" + - "\x0fListHomeChanges\x12\x1f.changes.ListHomeChangesRequest\x1a .changes.ListHomeChangesResponse\x12`\n" + - "\x13StartChangeAnalysis\x12#.changes.StartChangeAnalysisRequest\x1a$.changes.StartChangeAnalysisResponse\x12o\n" + - "\x18ListChangingItemsSummary\x12(.changes.ListChangingItemsSummaryRequest\x1a).changes.ListChangingItemsSummaryResponse\x12<\n" + - "\aGetDiff\x12\x17.changes.GetDiffRequest\x1a\x18.changes.GetDiffResponse\x12f\n" + - "\x15PopulateChangeFilters\x12%.changes.PopulateChangeFiltersRequest\x1a&.changes.PopulateChangeFiltersResponse\x12T\n" + - "\x0fGenerateRiskFix\x12\x1f.changes.GenerateRiskFixRequest\x1a .changes.GenerateRiskFixResponse\x12c\n" + - "\x14GetHypothesesDetails\x12$.changes.GetHypothesesDetailsRequest\x1a%.changes.GetHypothesesDetailsResponse\x12W\n" + - "\x10GetChangeSignals\x12 .changes.GetChangeSignalsRequest\x1a!.changes.GetChangeSignalsResponse2\xfc\x04\n" + - "\fLabelService\x12Q\n" + - "\x0eListLabelRules\x12\x1e.changes.ListLabelRulesRequest\x1a\x1f.changes.ListLabelRulesResponse\x12T\n" + - "\x0fCreateLabelRule\x12\x1f.changes.CreateLabelRuleRequest\x1a .changes.CreateLabelRuleResponse\x12K\n" + - "\fGetLabelRule\x12\x1c.changes.GetLabelRuleRequest\x1a\x1d.changes.GetLabelRuleResponse\x12T\n" + - "\x0fUpdateLabelRule\x12\x1f.changes.UpdateLabelRuleRequest\x1a .changes.UpdateLabelRuleResponse\x12T\n" + - "\x0fDeleteLabelRule\x12\x1f.changes.DeleteLabelRuleRequest\x1a .changes.DeleteLabelRuleResponse\x12P\n" + - "\rTestLabelRule\x12\x1d.changes.TestLabelRuleRequest\x1a\x1e.changes.TestLabelRuleResponse0\x01\x12x\n" + - "\x1bReapplyLabelRuleInTimeRange\x12+.changes.ReapplyLabelRuleInTimeRangeRequest\x1a,.changes.ReapplyLabelRuleInTimeRangeResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_changes_proto_rawDescOnce sync.Once - file_changes_proto_rawDescData []byte -) - -func file_changes_proto_rawDescGZIP() []byte { - file_changes_proto_rawDescOnce.Do(func() { - file_changes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_changes_proto_rawDesc), len(file_changes_proto_rawDesc))) - }) - return file_changes_proto_rawDescData -} - -var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 12) -var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 97) -var file_changes_proto_goTypes = []any{ - (MappedItemTimelineStatus)(0), // 0: changes.MappedItemTimelineStatus - (MappedItemMappingStatus)(0), // 1: changes.MappedItemMappingStatus - (HypothesisStatus)(0), // 2: changes.HypothesisStatus - (ChangeTimelineEntryStatus)(0), // 3: changes.ChangeTimelineEntryStatus - (ItemDiffStatus)(0), // 4: changes.ItemDiffStatus - (ChangeOutputFormat)(0), // 5: changes.ChangeOutputFormat - (LabelType)(0), // 6: changes.LabelType - (ChangeStatus)(0), // 7: changes.ChangeStatus - (StartChangeResponse_State)(0), // 8: changes.StartChangeResponse.State - (EndChangeResponse_State)(0), // 9: changes.EndChangeResponse.State - (Risk_Severity)(0), // 10: changes.Risk.Severity - (ChangeAnalysisStatus_Status)(0), // 11: changes.ChangeAnalysisStatus.Status - (*LabelRule)(nil), // 12: changes.LabelRule - (*LabelRuleMetadata)(nil), // 13: changes.LabelRuleMetadata - (*LabelRuleProperties)(nil), // 14: changes.LabelRuleProperties - (*ListLabelRulesRequest)(nil), // 15: changes.ListLabelRulesRequest - (*ListLabelRulesResponse)(nil), // 16: changes.ListLabelRulesResponse - (*CreateLabelRuleRequest)(nil), // 17: changes.CreateLabelRuleRequest - (*CreateLabelRuleResponse)(nil), // 18: changes.CreateLabelRuleResponse - (*GetLabelRuleRequest)(nil), // 19: changes.GetLabelRuleRequest - (*GetLabelRuleResponse)(nil), // 20: changes.GetLabelRuleResponse - (*UpdateLabelRuleRequest)(nil), // 21: changes.UpdateLabelRuleRequest - (*UpdateLabelRuleResponse)(nil), // 22: changes.UpdateLabelRuleResponse - (*DeleteLabelRuleRequest)(nil), // 23: changes.DeleteLabelRuleRequest - (*DeleteLabelRuleResponse)(nil), // 24: changes.DeleteLabelRuleResponse - (*TestLabelRuleRequest)(nil), // 25: changes.TestLabelRuleRequest - (*TestLabelRuleResponse)(nil), // 26: changes.TestLabelRuleResponse - (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 27: changes.ReapplyLabelRuleInTimeRangeRequest - (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 28: changes.ReapplyLabelRuleInTimeRangeResponse - (*GetHypothesesDetailsRequest)(nil), // 29: changes.GetHypothesesDetailsRequest - (*GetHypothesesDetailsResponse)(nil), // 30: changes.GetHypothesesDetailsResponse - (*HypothesesDetails)(nil), // 31: changes.HypothesesDetails - (*GetChangeTimelineV2Request)(nil), // 32: changes.GetChangeTimelineV2Request - (*GetChangeTimelineV2Response)(nil), // 33: changes.GetChangeTimelineV2Response - (*ChangeTimelineEntryV2)(nil), // 34: changes.ChangeTimelineEntryV2 - (*EmptyContent)(nil), // 35: changes.EmptyContent - (*MappedItemTimelineSummary)(nil), // 36: changes.MappedItemTimelineSummary - (*MappedItemsTimelineEntry)(nil), // 37: changes.MappedItemsTimelineEntry - (*CalculatedBlastRadiusTimelineEntry)(nil), // 38: changes.CalculatedBlastRadiusTimelineEntry - (*RecordObservationsTimelineEntry)(nil), // 39: changes.RecordObservationsTimelineEntry - (*FormHypothesesTimelineEntry)(nil), // 40: changes.FormHypothesesTimelineEntry - (*InvestigateHypothesesTimelineEntry)(nil), // 41: changes.InvestigateHypothesesTimelineEntry - (*HypothesisSummary)(nil), // 42: changes.HypothesisSummary - (*CalculatedRisksTimelineEntry)(nil), // 43: changes.CalculatedRisksTimelineEntry - (*CalculatedLabelsTimelineEntry)(nil), // 44: changes.CalculatedLabelsTimelineEntry - (*ChangeValidationTimelineEntry)(nil), // 45: changes.ChangeValidationTimelineEntry - (*ChangeValidationCategory)(nil), // 46: changes.ChangeValidationCategory - (*GetDiffRequest)(nil), // 47: changes.GetDiffRequest - (*GetDiffResponse)(nil), // 48: changes.GetDiffResponse - (*ListChangingItemsSummaryRequest)(nil), // 49: changes.ListChangingItemsSummaryRequest - (*ListChangingItemsSummaryResponse)(nil), // 50: changes.ListChangingItemsSummaryResponse - (*MappedItemDiff)(nil), // 51: changes.MappedItemDiff - (*StartChangeAnalysisRequest)(nil), // 52: changes.StartChangeAnalysisRequest - (*StartChangeAnalysisResponse)(nil), // 53: changes.StartChangeAnalysisResponse - (*ListHomeChangesRequest)(nil), // 54: changes.ListHomeChangesRequest - (*ChangeFiltersRequest)(nil), // 55: changes.ChangeFiltersRequest - (*ListHomeChangesResponse)(nil), // 56: changes.ListHomeChangesResponse - (*PopulateChangeFiltersRequest)(nil), // 57: changes.PopulateChangeFiltersRequest - (*PopulateChangeFiltersResponse)(nil), // 58: changes.PopulateChangeFiltersResponse - (*ItemDiffSummary)(nil), // 59: changes.ItemDiffSummary - (*ItemDiff)(nil), // 60: changes.ItemDiff - (*EnrichedTags)(nil), // 61: changes.EnrichedTags - (*TagValue)(nil), // 62: changes.TagValue - (*UserTagValue)(nil), // 63: changes.UserTagValue - (*AutoTagValue)(nil), // 64: changes.AutoTagValue - (*Label)(nil), // 65: changes.Label - (*ChangeSummary)(nil), // 66: changes.ChangeSummary - (*Change)(nil), // 67: changes.Change - (*ChangeMetadata)(nil), // 68: changes.ChangeMetadata - (*ChangeProperties)(nil), // 69: changes.ChangeProperties - (*GithubChangeInfo)(nil), // 70: changes.GithubChangeInfo - (*ListChangesRequest)(nil), // 71: changes.ListChangesRequest - (*ListChangesResponse)(nil), // 72: changes.ListChangesResponse - (*ListChangesByStatusRequest)(nil), // 73: changes.ListChangesByStatusRequest - (*ListChangesByStatusResponse)(nil), // 74: changes.ListChangesByStatusResponse - (*CreateChangeRequest)(nil), // 75: changes.CreateChangeRequest - (*CreateChangeResponse)(nil), // 76: changes.CreateChangeResponse - (*GetChangeRequest)(nil), // 77: changes.GetChangeRequest - (*GetChangeByTicketLinkRequest)(nil), // 78: changes.GetChangeByTicketLinkRequest - (*GetChangeSummaryRequest)(nil), // 79: changes.GetChangeSummaryRequest - (*GetChangeSummaryResponse)(nil), // 80: changes.GetChangeSummaryResponse - (*GetChangeSignalsRequest)(nil), // 81: changes.GetChangeSignalsRequest - (*GetChangeSignalsResponse)(nil), // 82: changes.GetChangeSignalsResponse - (*GetChangeResponse)(nil), // 83: changes.GetChangeResponse - (*GetChangeRisksRequest)(nil), // 84: changes.GetChangeRisksRequest - (*ChangeRiskMetadata)(nil), // 85: changes.ChangeRiskMetadata - (*GetChangeRisksResponse)(nil), // 86: changes.GetChangeRisksResponse - (*UpdateChangeRequest)(nil), // 87: changes.UpdateChangeRequest - (*UpdateChangeResponse)(nil), // 88: changes.UpdateChangeResponse - (*DeleteChangeRequest)(nil), // 89: changes.DeleteChangeRequest - (*ListChangesBySnapshotUUIDRequest)(nil), // 90: changes.ListChangesBySnapshotUUIDRequest - (*ListChangesBySnapshotUUIDResponse)(nil), // 91: changes.ListChangesBySnapshotUUIDResponse - (*DeleteChangeResponse)(nil), // 92: changes.DeleteChangeResponse - (*RefreshStateRequest)(nil), // 93: changes.RefreshStateRequest - (*RefreshStateResponse)(nil), // 94: changes.RefreshStateResponse - (*StartChangeRequest)(nil), // 95: changes.StartChangeRequest - (*StartChangeResponse)(nil), // 96: changes.StartChangeResponse - (*EndChangeRequest)(nil), // 97: changes.EndChangeRequest - (*EndChangeResponse)(nil), // 98: changes.EndChangeResponse - (*StartChangeSimpleResponse)(nil), // 99: changes.StartChangeSimpleResponse - (*EndChangeSimpleResponse)(nil), // 100: changes.EndChangeSimpleResponse - (*Risk)(nil), // 101: changes.Risk - (*ChangeAnalysisStatus)(nil), // 102: changes.ChangeAnalysisStatus - (*GenerateRiskFixRequest)(nil), // 103: changes.GenerateRiskFixRequest - (*GenerateRiskFixResponse)(nil), // 104: changes.GenerateRiskFixResponse - nil, // 105: changes.EnrichedTags.TagValueEntry - nil, // 106: changes.ChangeSummary.TagsEntry - (*ChangeMetadata_HealthChange)(nil), // 107: changes.ChangeMetadata.HealthChange - nil, // 108: changes.ChangeProperties.TagsEntry - (*timestamppb.Timestamp)(nil), // 109: google.protobuf.Timestamp - (*Edge)(nil), // 110: Edge - (*Query)(nil), // 111: Query - (*QueryError)(nil), // 112: QueryError - (*BlastRadiusConfig)(nil), // 113: config.BlastRadiusConfig - (*RoutineChangesConfig)(nil), // 114: config.RoutineChangesConfig - (*GithubOrganisationProfile)(nil), // 115: config.GithubOrganisationProfile - (*PaginationRequest)(nil), // 116: PaginationRequest - (SortOrder)(0), // 117: SortOrder - (*PaginationResponse)(nil), // 118: PaginationResponse - (*Reference)(nil), // 119: Reference - (Health)(0), // 120: Health - (*Item)(nil), // 121: Item -} -var file_changes_proto_depIdxs = []int32{ - 13, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata - 14, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties - 109, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 12, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule - 14, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 12, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule - 12, // 7: changes.GetLabelRuleResponse.rule:type_name -> changes.LabelRule - 14, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 12, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule - 14, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 65, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label - 109, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp - 109, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp - 31, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails - 2, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus - 34, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 - 3, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus - 109, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp - 109, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp - 37, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry - 38, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry - 43, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry - 35, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent - 45, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry - 44, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry - 40, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry - 41, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry - 39, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry - 0, // 29: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus - 51, // 30: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff - 36, // 31: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary - 42, // 32: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 42, // 33: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 2, // 34: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus - 101, // 35: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk - 65, // 36: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label - 46, // 37: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory - 60, // 38: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff - 60, // 39: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff - 110, // 40: changes.GetDiffResponse.edges:type_name -> Edge - 60, // 41: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff - 59, // 42: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary - 60, // 43: changes.MappedItemDiff.item:type_name -> changes.ItemDiff - 111, // 44: changes.MappedItemDiff.mappingQuery:type_name -> Query - 112, // 45: changes.MappedItemDiff.mappingError:type_name -> QueryError - 1, // 46: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus - 51, // 47: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff - 113, // 48: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig - 114, // 49: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig - 115, // 50: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile - 116, // 51: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest - 55, // 52: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest - 10, // 53: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity - 7, // 54: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus - 117, // 55: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder - 66, // 56: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary - 118, // 57: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse - 119, // 58: changes.ItemDiffSummary.item:type_name -> Reference - 4, // 59: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus - 120, // 60: changes.ItemDiffSummary.healthAfter:type_name -> Health - 119, // 61: changes.ItemDiff.item:type_name -> Reference - 4, // 62: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus - 121, // 63: changes.ItemDiff.before:type_name -> Item - 121, // 64: changes.ItemDiff.after:type_name -> Item - 105, // 65: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry - 63, // 66: changes.TagValue.userTagValue:type_name -> changes.UserTagValue - 64, // 67: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue - 6, // 68: changes.Label.type:type_name -> changes.LabelType - 7, // 69: changes.ChangeSummary.status:type_name -> changes.ChangeStatus - 109, // 70: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp - 106, // 71: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry - 61, // 72: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags - 65, // 73: changes.ChangeSummary.labels:type_name -> changes.Label - 70, // 74: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo - 68, // 75: changes.Change.metadata:type_name -> changes.ChangeMetadata - 69, // 76: changes.Change.properties:type_name -> changes.ChangeProperties - 109, // 77: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 78: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 7, // 79: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus - 107, // 80: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 81: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 82: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 83: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 84: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 70, // 85: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo - 60, // 86: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff - 108, // 87: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry - 61, // 88: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags - 65, // 89: changes.ChangeProperties.labels:type_name -> changes.Label - 67, // 90: changes.ListChangesResponse.changes:type_name -> changes.Change - 7, // 91: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus - 67, // 92: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change - 69, // 93: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 94: changes.CreateChangeResponse.change:type_name -> changes.Change - 5, // 95: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 10, // 96: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity - 5, // 97: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 67, // 98: changes.GetChangeResponse.change:type_name -> changes.Change - 102, // 99: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus - 101, // 100: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk - 85, // 101: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata - 69, // 102: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 103: changes.UpdateChangeResponse.change:type_name -> changes.Change - 67, // 104: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change - 8, // 105: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State - 9, // 106: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State - 10, // 107: changes.Risk.severity:type_name -> changes.Risk.Severity - 119, // 108: changes.Risk.relatedItems:type_name -> Reference - 11, // 109: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status - 62, // 110: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue - 71, // 111: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest - 73, // 112: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest - 75, // 113: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest - 77, // 114: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest - 78, // 115: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest - 79, // 116: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest - 32, // 117: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request - 84, // 118: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest - 87, // 119: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest - 89, // 120: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest - 90, // 121: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest - 93, // 122: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest - 95, // 123: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest - 97, // 124: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest - 95, // 125: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest - 97, // 126: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest - 54, // 127: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest - 52, // 128: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest - 49, // 129: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest - 47, // 130: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest - 57, // 131: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest - 103, // 132: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest - 29, // 133: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest - 81, // 134: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest - 15, // 135: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest - 17, // 136: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest - 19, // 137: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest - 21, // 138: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest - 23, // 139: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest - 25, // 140: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest - 27, // 141: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest - 72, // 142: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse - 74, // 143: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse - 76, // 144: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse - 83, // 145: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse - 83, // 146: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse - 80, // 147: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse - 33, // 148: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response - 86, // 149: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse - 88, // 150: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse - 92, // 151: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse - 91, // 152: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse - 94, // 153: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse - 96, // 154: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse - 98, // 155: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse - 99, // 156: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse - 100, // 157: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse - 56, // 158: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse - 53, // 159: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse - 50, // 160: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse - 48, // 161: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse - 58, // 162: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse - 104, // 163: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse - 30, // 164: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse - 82, // 165: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse - 16, // 166: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse - 18, // 167: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse - 20, // 168: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse - 22, // 169: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse - 24, // 170: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse - 26, // 171: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse - 28, // 172: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse - 142, // [142:173] is the sub-list for method output_type - 111, // [111:142] is the sub-list for method input_type - 111, // [111:111] is the sub-list for extension type_name - 111, // [111:111] is the sub-list for extension extendee - 0, // [0:111] is the sub-list for field type_name -} - -func init() { file_changes_proto_init() } -func file_changes_proto_init() { - if File_changes_proto != nil { - return - } - file_config_proto_init() - file_items_proto_init() - file_util_proto_init() - file_changes_proto_msgTypes[22].OneofWrappers = []any{ - (*ChangeTimelineEntryV2_MappedItems)(nil), - (*ChangeTimelineEntryV2_CalculatedBlastRadius)(nil), - (*ChangeTimelineEntryV2_CalculatedRisks)(nil), - (*ChangeTimelineEntryV2_Error)(nil), - (*ChangeTimelineEntryV2_StatusMessage)(nil), - (*ChangeTimelineEntryV2_Empty)(nil), - (*ChangeTimelineEntryV2_ChangeValidation)(nil), - (*ChangeTimelineEntryV2_CalculatedLabels)(nil), - (*ChangeTimelineEntryV2_FormHypotheses)(nil), - (*ChangeTimelineEntryV2_InvestigateHypotheses)(nil), - (*ChangeTimelineEntryV2_RecordObservations)(nil), - } - file_changes_proto_msgTypes[24].OneofWrappers = []any{} - file_changes_proto_msgTypes[39].OneofWrappers = []any{} - file_changes_proto_msgTypes[40].OneofWrappers = []any{} - file_changes_proto_msgTypes[42].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[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: 12, - NumMessages: 97, - NumExtensions: 0, - NumServices: 2, - }, - GoTypes: file_changes_proto_goTypes, - DependencyIndexes: file_changes_proto_depIdxs, - EnumInfos: file_changes_proto_enumTypes, - MessageInfos: file_changes_proto_msgTypes, - }.Build() - File_changes_proto = out.File - file_changes_proto_goTypes = nil - file_changes_proto_depIdxs = nil -} diff --git a/sdp-go/changes_test.go b/sdp-go/changes_test.go deleted file mode 100644 index 26300a24..00000000 --- a/sdp-go/changes_test.go +++ /dev/null @@ -1,474 +0,0 @@ -package sdp - -import ( - "strings" - "testing" -) - -func TestFindInProgressEntry(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - entries []*ChangeTimelineEntryV2 - expectedName string - expectedStatus ChangeTimelineEntryStatus - expectError bool - }{ - { - name: "nil entries", - entries: nil, - expectedName: "", - expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, - expectError: true, - }, - { - name: "empty entries", - entries: []*ChangeTimelineEntryV2{}, - expectedName: "", - expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, - expectError: true, - }, - { - name: "in progress entry", - entries: []*ChangeTimelineEntryV2{ - { - Name: "entry1", - Status: ChangeTimelineEntryStatus_IN_PROGRESS, - }, - { - Name: "entry2", - Status: ChangeTimelineEntryStatus_PENDING, - }, - }, - expectedName: "entry1", - expectedStatus: ChangeTimelineEntryStatus_IN_PROGRESS, - expectError: false, - }, - { - name: "pending entry", - entries: []*ChangeTimelineEntryV2{ - { - Name: "entry1", - Status: ChangeTimelineEntryStatus_DONE, - }, - { - Name: "entry2", - Status: ChangeTimelineEntryStatus_PENDING, - }, - }, - expectedName: "entry2", - expectedStatus: ChangeTimelineEntryStatus_PENDING, - expectError: false, - }, - { - name: "error entry", - entries: []*ChangeTimelineEntryV2{ - { - Name: "entry1", - Status: ChangeTimelineEntryStatus_DONE, - }, - { - Name: "entry2", - Status: ChangeTimelineEntryStatus_ERROR, - }, - }, - expectedName: "entry2", - expectedStatus: ChangeTimelineEntryStatus_ERROR, - expectError: false, - }, - { - name: "no in progress entry", - entries: []*ChangeTimelineEntryV2{ - { - Name: "entry1", - Status: ChangeTimelineEntryStatus_DONE, - }, - { - Name: "entry2", - Status: ChangeTimelineEntryStatus_UNSPECIFIED, - }, - }, - expectedName: "", - expectedStatus: ChangeTimelineEntryStatus_DONE, - expectError: false, - }, - { - name: "unknown status", - entries: []*ChangeTimelineEntryV2{ - { - Name: "entry1", - Status: 100, // some unknown status - }, - }, - expectedName: "", - expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - name, _, status, err := TimelineFindInProgressEntry(tt.entries) - - if tt.expectError && err == nil { - t.Errorf("Expected an error, got nil") - } - - if !tt.expectError && err != nil { - t.Errorf("Expected no error, got %v", err) - } - - if name != tt.expectedName { - t.Errorf("Expected name %s, got %s", tt.expectedName, name) - } - - if status != tt.expectedStatus { - t.Errorf("Expected status %s, got %s", tt.expectedStatus, status) - } - }) - } -} - -func TestTimelineEntryContentDescription(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - entry *ChangeTimelineEntryV2 - expected string - }{ - { - name: "mapped items", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_MappedItems{ - MappedItems: &MappedItemsTimelineEntry{ - MappedItems: []*MappedItemDiff{{}, {}, {}}, - }, - }, - }, - expected: "3 mapped items", - }, - { - name: "calculated blast radius", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_CalculatedBlastRadius{ - CalculatedBlastRadius: &CalculatedBlastRadiusTimelineEntry{ - NumItems: 10, - NumEdges: 25, - }, - }, - }, - expected: "10 items, 25 edges", - }, - { - name: "calculated risks", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_CalculatedRisks{ - CalculatedRisks: &CalculatedRisksTimelineEntry{ - Risks: []*Risk{{}, {}}, - }, - }, - }, - expected: "2 risks", - }, - { - name: "calculated labels", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_CalculatedLabels{ - CalculatedLabels: &CalculatedLabelsTimelineEntry{ - Labels: []*Label{{}, {}, {}, {}}, - }, - }, - }, - expected: "4 labels", - }, - { - name: "change validation", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_ChangeValidation{ - ChangeValidation: &ChangeValidationTimelineEntry{ - ValidationChecklist: []*ChangeValidationCategory{{}}, - }, - }, - }, - expected: "1 validation categories", - }, - { - name: "form hypotheses", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_FormHypotheses{ - FormHypotheses: &FormHypothesesTimelineEntry{ - NumHypotheses: 5, - }, - }, - }, - expected: "5 hypotheses", - }, - { - name: "investigate hypotheses", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_InvestigateHypotheses{ - InvestigateHypotheses: &InvestigateHypothesesTimelineEntry{ - NumProven: 2, - NumDisproven: 3, - NumInvestigating: 1, - }, - }, - }, - expected: "2 proven, 3 disproven, 1 investigating", - }, - { - name: "record observations", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_RecordObservations{ - RecordObservations: &RecordObservationsTimelineEntry{ - NumObservations: 42, - }, - }, - }, - expected: "42 observations", - }, - { - name: "error content", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_Error{ - Error: "something went wrong", - }, - }, - expected: "something went wrong", - }, - { - name: "status message", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_StatusMessage{ - StatusMessage: "processing data", - }, - }, - expected: "processing data", - }, - { - name: "empty content", - entry: &ChangeTimelineEntryV2{ - Content: &ChangeTimelineEntryV2_Empty{ - Empty: &EmptyContent{}, - }, - }, - expected: "", - }, - { - name: "nil content", - entry: &ChangeTimelineEntryV2{ - Content: nil, - }, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := TimelineEntryContentDescription(tt.entry) - if result != tt.expected { - t.Errorf("Expected %q, got %q", tt.expected, result) - } - }) - } -} - -func TestValidateRoutineChangesConfig(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config *RoutineChangesYAML - wantErr bool - errContains string - }{ - { - name: "valid config", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: 7.0, - Sensitivity: 0.5, - }, - wantErr: false, - }, - { - name: "valid config with minimum values", - config: &RoutineChangesYAML{ - EventsPerDay: 1.0, - DurationInDays: 1.0, - Sensitivity: 0.0, - }, - wantErr: false, - }, - { - name: "events_per_day less than 1", - config: &RoutineChangesYAML{ - EventsPerDay: 0.5, - DurationInDays: 7.0, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "events_per_day must be greater than 1", - }, - { - name: "events_per_day equals 0", - config: &RoutineChangesYAML{ - EventsPerDay: 0.0, - DurationInDays: 7.0, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "events_per_day must be greater than 1", - }, - { - name: "events_per_day negative", - config: &RoutineChangesYAML{ - EventsPerDay: -1.0, - DurationInDays: 7.0, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "events_per_day must be greater than 1", - }, - { - name: "duration_in_days less than 1", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: 0.5, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "duration_in_days must be greater than 1", - }, - { - name: "duration_in_days equals 0", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: 0.0, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "duration_in_days must be greater than 1", - }, - { - name: "duration_in_days negative", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: -1.0, - Sensitivity: 0.5, - }, - wantErr: true, - errContains: "duration_in_days must be greater than 1", - }, - { - name: "sensitivity negative", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: 7.0, - Sensitivity: -0.1, - }, - wantErr: true, - errContains: "sensitivity must be 0 or higher", - }, - { - name: "multiple invalid fields - events_per_day checked first", - config: &RoutineChangesYAML{ - EventsPerDay: 0.0, - DurationInDays: 0.0, - Sensitivity: -1.0, - }, - wantErr: true, - errContains: "events_per_day must be greater than 1", - }, - { - name: "multiple invalid fields - duration_in_days checked second", - config: &RoutineChangesYAML{ - EventsPerDay: 10.0, - DurationInDays: 0.0, - Sensitivity: -1.0, - }, - wantErr: true, - errContains: "duration_in_days must be greater than 1", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateRoutineChangesConfig(tt.config) - if (err != nil) != tt.wantErr { - t.Errorf("validateRoutineChangesConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && tt.errContains != "" { - if err == nil { - t.Errorf("validateRoutineChangesConfig() expected error containing %q, got nil", tt.errContains) - } else if !strings.Contains(err.Error(), tt.errContains) { - t.Errorf("validateRoutineChangesConfig() error = %v, want error containing %q", err, tt.errContains) - } - } - }) - } -} - -func TestYamlStringToSignalConfig_NilCombinations(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - yamlString string - wantErr bool - wantRoutine bool - wantGithub bool - }{ - { - name: "both nil -> error", - yamlString: "{}\n", - wantErr: true, - wantRoutine: false, - wantGithub: false, - }, - { - name: "only routine present", - yamlString: "routine_changes_config:\n sensitivity: 0\n duration_in_days: 1\n events_per_day: 1\n", - wantErr: false, - wantRoutine: true, - wantGithub: false, - }, - { - name: "only github present", - yamlString: "github_organisation_profile:\n primary_branch_name: main\n", - wantErr: false, - wantRoutine: false, - wantGithub: true, - }, - { - name: "both present", - yamlString: "routine_changes_config:\n sensitivity: 0\n duration_in_days: 1\n events_per_day: 1\ngithub_organisation_profile:\n primary_branch_name: main\n", - wantErr: false, - wantRoutine: true, - wantGithub: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := YamlStringToSignalConfig(tt.yamlString) - if (err != nil) != tt.wantErr { - t.Errorf("YamlStringToSignalConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr { - return - } - - if (got.RoutineChangesConfig != nil) != tt.wantRoutine { - t.Errorf("RoutineChangesConfig presence = %v, want %v", got.RoutineChangesConfig != nil, tt.wantRoutine) - } - if (got.GithubOrganisationProfile != nil) != tt.wantGithub { - t.Errorf("GithubOrganisationProfile presence = %v, want %v", got.GithubOrganisationProfile != nil, tt.wantGithub) - } - }) - } -} diff --git a/sdp-go/changetimeline.go b/sdp-go/changetimeline.go deleted file mode 100644 index d76c5b4f..00000000 --- a/sdp-go/changetimeline.go +++ /dev/null @@ -1,164 +0,0 @@ -package sdp - -// If you add/delete/move an entry here, make sure to update/check the following: -// - the PopulateChangeTimelineV2 function -// - 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. -// Use the .Label field for database lookups and the .Name field for user-facing display. - -type ChangeTimelineEntryV2ID struct { - // The internal label for the entry, this is used to identify the entry in - // the database and tell whether two entries are the same type of thing. - // This means that if we want to change the way an entry behaves, we can - // create a new label and keep the old one for backwards compatibility. - Label string - // The name of the entry, this is the user facing name of the entry and can - // be changed safely. This is stored in both the code and the database, the - // reason we store it in the code is so that we know what value to populate - // in the database when we create the timeline entries in the first place, - // when returning the timeline to the user we use the name from the database - // which means that old changes will still show the old name. - 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 - // the mapping queries that were sent up, and running them against the - // gateway to see whether any of them resolve into real items. - ChangeTimelineEntryV2IDMapResources = ChangeTimelineEntryV2ID{ - Label: "mapped_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 - // taking the mapped resources and running them through the blast radius - // simulation to see how many items are in the blast radius. - ChangeTimelineEntryV2IDCalculatedBlastRadius = ChangeTimelineEntryV2ID{ - Label: "calculated_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: "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 - // no longer showing risks directly in the timeline. The risk calculation - // still happens, but the timeline focuses on Observations -> Hypotheses -> - // Investigations instead. This means that this step will be no longer used - // after Dec '25 - ChangeTimelineEntryV2IDCalculatedRisks = ChangeTimelineEntryV2ID{ - Label: "calculated_risks", - Name: "Calculated Risks", - } - // Tracks the application of auto-label rules for a change - ChangeTimelineEntryV2IDCalculatedLabels = ChangeTimelineEntryV2ID{ - Label: "calculated_labels", - Name: "Apply auto labels", - } - // Tracks the application of auto tags for a change - ChangeTimelineEntryV2IDAutoTagging = ChangeTimelineEntryV2ID{ - Label: "auto_tagging", - Name: "Auto Tagging", - } - // Tracks the validation of a change. This happens after the change is - // complete and at time of writing is not generally available - ChangeTimelineEntryV2IDChangeValidation = ChangeTimelineEntryV2ID{ - Label: "change_validation", - Name: "Change Validation", - } - // This is the entry that tracks observations being recorded during blast radius simulation - ChangeTimelineEntryV2IDRecordObservations = ChangeTimelineEntryV2ID{ - Label: "record_observations", - Name: "Record observations", - } - // This is the entry that tracks hypotheses being formed from observations via batch processing - ChangeTimelineEntryV2IDFormHypotheses = ChangeTimelineEntryV2ID{ - Label: "form_hypotheses", - Name: "Form hypotheses", - } - // This is the entry that tracks investigation of hypotheses via one-shot analysis - ChangeTimelineEntryV2IDInvestigateHypotheses = ChangeTimelineEntryV2ID{ - Label: "investigate_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 deleted file mode 100644 index 391d4cb4..00000000 --- a/sdp-go/changetimeline_test.go +++ /dev/null @@ -1,154 +0,0 @@ -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/cli.pb.go b/sdp-go/cli.pb.go deleted file mode 100644 index dc282b3b..00000000 --- a/sdp-go/cli.pb.go +++ /dev/null @@ -1,270 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: cli.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetConfigRequest) Reset() { - *x = GetConfigRequest{} - mi := &file_cli_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetConfigRequest) ProtoMessage() {} - -func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_cli_proto_msgTypes[0] - 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 GetConfigRequest.ProtoReflect.Descriptor instead. -func (*GetConfigRequest) Descriptor() ([]byte, []int) { - return file_cli_proto_rawDescGZIP(), []int{0} -} - -func (x *GetConfigRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -type GetConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetConfigResponse) Reset() { - *x = GetConfigResponse{} - mi := &file_cli_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetConfigResponse) ProtoMessage() {} - -func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_cli_proto_msgTypes[1] - 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 GetConfigResponse.ProtoReflect.Descriptor instead. -func (*GetConfigResponse) Descriptor() ([]byte, []int) { - return file_cli_proto_rawDescGZIP(), []int{1} -} - -func (x *GetConfigResponse) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type SetConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetConfigRequest) Reset() { - *x = SetConfigRequest{} - mi := &file_cli_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetConfigRequest) ProtoMessage() {} - -func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_cli_proto_msgTypes[2] - 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 SetConfigRequest.ProtoReflect.Descriptor instead. -func (*SetConfigRequest) Descriptor() ([]byte, []int) { - return file_cli_proto_rawDescGZIP(), []int{2} -} - -func (x *SetConfigRequest) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *SetConfigRequest) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - -type SetConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SetConfigResponse) Reset() { - *x = SetConfigResponse{} - mi := &file_cli_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SetConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SetConfigResponse) ProtoMessage() {} - -func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_cli_proto_msgTypes[3] - 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 SetConfigResponse.ProtoReflect.Descriptor instead. -func (*SetConfigResponse) Descriptor() ([]byte, []int) { - return file_cli_proto_rawDescGZIP(), []int{3} -} - -var File_cli_proto protoreflect.FileDescriptor - -const file_cli_proto_rawDesc = "" + - "\n" + - "\tcli.proto\x12\x03cli\"$\n" + - "\x10GetConfigRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\")\n" + - "\x11GetConfigResponse\x12\x14\n" + - "\x05value\x18\x01 \x01(\tR\x05value\":\n" + - "\x10SetConfigRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value\"\x13\n" + - "\x11SetConfigResponse2\x8b\x01\n" + - "\rConfigService\x12<\n" + - "\tGetConfig\x12\x15.cli.GetConfigRequest\x1a\x16.cli.GetConfigResponse\"\x00\x12<\n" + - "\tSetConfig\x12\x15.cli.SetConfigRequest\x1a\x16.cli.SetConfigResponse\"\x00B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_cli_proto_rawDescOnce sync.Once - file_cli_proto_rawDescData []byte -) - -func file_cli_proto_rawDescGZIP() []byte { - file_cli_proto_rawDescOnce.Do(func() { - file_cli_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cli_proto_rawDesc), len(file_cli_proto_rawDesc))) - }) - return file_cli_proto_rawDescData -} - -var file_cli_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_cli_proto_goTypes = []any{ - (*GetConfigRequest)(nil), // 0: cli.GetConfigRequest - (*GetConfigResponse)(nil), // 1: cli.GetConfigResponse - (*SetConfigRequest)(nil), // 2: cli.SetConfigRequest - (*SetConfigResponse)(nil), // 3: cli.SetConfigResponse -} -var file_cli_proto_depIdxs = []int32{ - 0, // 0: cli.ConfigService.GetConfig:input_type -> cli.GetConfigRequest - 2, // 1: cli.ConfigService.SetConfig:input_type -> cli.SetConfigRequest - 1, // 2: cli.ConfigService.GetConfig:output_type -> cli.GetConfigResponse - 3, // 3: cli.ConfigService.SetConfig:output_type -> cli.SetConfigResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_cli_proto_init() } -func file_cli_proto_init() { - if File_cli_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_cli_proto_rawDesc), len(file_cli_proto_rawDesc)), - NumEnums: 0, - NumMessages: 4, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_cli_proto_goTypes, - DependencyIndexes: file_cli_proto_depIdxs, - MessageInfos: file_cli_proto_msgTypes, - }.Build() - File_cli_proto = out.File - file_cli_proto_goTypes = nil - file_cli_proto_depIdxs = nil -} diff --git a/sdp-go/compare.go b/sdp-go/compare.go deleted file mode 100644 index 211d4b95..00000000 --- a/sdp-go/compare.go +++ /dev/null @@ -1,41 +0,0 @@ -package sdp - -import "fmt" - -// Comparer is an object that can be compared for the purposes of sorting. -// Basically anything that implements this interface is sortable -type Comparer interface { - Compare(b *Item) int -} - -// Compare compares two Items for the purposes of sorting. This sorts based on -// the string conversion of the Type, followed by the UniqueAttribute -func (i *Item) Compare(r *Item) (int, error) { - // Convert to strings - right := fmt.Sprintf("%v: %v", r.GetType(), r.UniqueAttributeValue()) - left := fmt.Sprintf("%v: %v", i.GetType(), i.UniqueAttributeValue()) - - // Compare the strings and return the value - switch { - case left > right: - return 1, nil - case left < right: - return -1, nil - default: - return 0, nil - } -} - -// CompareError is returned when two Items cannot be compared because their -// UniqueAttributeValue() is not sortable -type CompareError Item - -// Error returns the string when the error is handled -func (c *CompareError) Error() string { - return (fmt.Sprintf( - "Item %v unique attribute: %v of type %v does not implement interface fmt.Stringer. Cannot sort.", - c.Type, - c.UniqueAttribute, - c.Type, - )) -} diff --git a/sdp-go/config.pb.go b/sdp-go/config.pb.go deleted file mode 100644 index a3f022db..00000000 --- a/sdp-go/config.pb.go +++ /dev/null @@ -1,1815 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: config.proto - -package sdp - -import ( - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type AccountConfig_BlastRadiusPreset int32 - -const ( - // 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 - // An optimised balance between time taken and discovery. - AccountConfig_DETAILED AccountConfig_BlastRadiusPreset = 2 - // Discovers all possible dependencies, might take a long time and - // discover items that are less likely to be relevant to a change. - AccountConfig_FULL AccountConfig_BlastRadiusPreset = 3 -) - -// Enum value maps for AccountConfig_BlastRadiusPreset. -var ( - AccountConfig_BlastRadiusPreset_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "QUICK", - 2: "DETAILED", - 3: "FULL", - } - AccountConfig_BlastRadiusPreset_value = map[string]int32{ - "UNSPECIFIED": 0, - "QUICK": 1, - "DETAILED": 2, - "FULL": 3, - } -) - -func (x AccountConfig_BlastRadiusPreset) Enum() *AccountConfig_BlastRadiusPreset { - p := new(AccountConfig_BlastRadiusPreset) - *p = x - return p -} - -func (x AccountConfig_BlastRadiusPreset) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (AccountConfig_BlastRadiusPreset) Descriptor() protoreflect.EnumDescriptor { - return file_config_proto_enumTypes[0].Descriptor() -} - -func (AccountConfig_BlastRadiusPreset) Type() protoreflect.EnumType { - return &file_config_proto_enumTypes[0] -} - -func (x AccountConfig_BlastRadiusPreset) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use AccountConfig_BlastRadiusPreset.Descriptor instead. -func (AccountConfig_BlastRadiusPreset) EnumDescriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{1, 0} -} - -type GetHcpConfigResponse_Status int32 - -const ( - // The HCP Run Task configuration is active and can be used - GetHcpConfigResponse_CONFIGURED GetHcpConfigResponse_Status = 0 - // The HCP Run Task configuration is not fully configured and needs to - // be recreated, this is usually due to the API key being revoked or the - // user not completing the authorisation process - GetHcpConfigResponse_ERROR GetHcpConfigResponse_Status = 1 -) - -// Enum value maps for GetHcpConfigResponse_Status. -var ( - GetHcpConfigResponse_Status_name = map[int32]string{ - 0: "CONFIGURED", - 1: "ERROR", - } - GetHcpConfigResponse_Status_value = map[string]int32{ - "CONFIGURED": 0, - "ERROR": 1, - } -) - -func (x GetHcpConfigResponse_Status) Enum() *GetHcpConfigResponse_Status { - p := new(GetHcpConfigResponse_Status) - *p = x - return p -} - -func (x GetHcpConfigResponse_Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (GetHcpConfigResponse_Status) Descriptor() protoreflect.EnumDescriptor { - return file_config_proto_enumTypes[1].Descriptor() -} - -func (GetHcpConfigResponse_Status) Type() protoreflect.EnumType { - return &file_config_proto_enumTypes[1] -} - -func (x GetHcpConfigResponse_Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use GetHcpConfigResponse_Status.Descriptor instead. -func (GetHcpConfigResponse_Status) EnumDescriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{10, 0} -} - -type RoutineChangesConfig_DurationUnit int32 - -const ( - // Days - RoutineChangesConfig_DAYS RoutineChangesConfig_DurationUnit = 0 - // Weeks - RoutineChangesConfig_WEEKS RoutineChangesConfig_DurationUnit = 1 - // Months - RoutineChangesConfig_MONTHS RoutineChangesConfig_DurationUnit = 2 -) - -// Enum value maps for RoutineChangesConfig_DurationUnit. -var ( - RoutineChangesConfig_DurationUnit_name = map[int32]string{ - 0: "DAYS", - 1: "WEEKS", - 2: "MONTHS", - } - RoutineChangesConfig_DurationUnit_value = map[string]int32{ - "DAYS": 0, - "WEEKS": 1, - "MONTHS": 2, - } -) - -func (x RoutineChangesConfig_DurationUnit) Enum() *RoutineChangesConfig_DurationUnit { - p := new(RoutineChangesConfig_DurationUnit) - *p = x - return p -} - -func (x RoutineChangesConfig_DurationUnit) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (RoutineChangesConfig_DurationUnit) Descriptor() protoreflect.EnumDescriptor { - return file_config_proto_enumTypes[2].Descriptor() -} - -func (RoutineChangesConfig_DurationUnit) Type() protoreflect.EnumType { - return &file_config_proto_enumTypes[2] -} - -func (x RoutineChangesConfig_DurationUnit) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use RoutineChangesConfig_DurationUnit.Descriptor instead. -func (RoutineChangesConfig_DurationUnit) EnumDescriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{19, 0} -} - -// The config that is used when calculating the blast radius for a change, this -// does not affect manually requested blast radii vie the "Explore" view or the -// API -type BlastRadiusConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The maximum number of items that can be returned in a single blast radius - // request. Once a request has hit this limit, all currently running - // requests will be cancelled and the blast radius returned as-is - MaxItems int32 `protobuf:"varint,1,opt,name=maxItems,proto3" json:"maxItems,omitempty"` - // How deeply to link when calculating the blast radius for a change. This - // 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"` - // 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() { - *x = BlastRadiusConfig{} - mi := &file_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BlastRadiusConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BlastRadiusConfig) ProtoMessage() {} - -func (x *BlastRadiusConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[0] - 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 BlastRadiusConfig.ProtoReflect.Descriptor instead. -func (*BlastRadiusConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{0} -} - -func (x *BlastRadiusConfig) GetMaxItems() int32 { - if x != nil { - return x.MaxItems - } - return 0 -} - -func (x *BlastRadiusConfig) GetLinkDepth() int32 { - if x != nil { - return x.LinkDepth - } - return 0 -} - -func (x *BlastRadiusConfig) GetChangeAnalysisTargetDuration() *durationpb.Duration { - if x != nil { - return x.ChangeAnalysisTargetDuration - } - return nil -} - -// 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. 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 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 - // reduces the number low-quality and low-severity risks, and focuses more - // on risks that we have additional context for. If you find that Overmind's - // risks are "too obvious" then this might be a good setting to enable. - SkipUnmappedChangesForRisks bool `protobuf:"varint,3,opt,name=skipUnmappedChangesForRisks,proto3" json:"skipUnmappedChangesForRisks,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AccountConfig) Reset() { - *x = AccountConfig{} - mi := &file_config_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AccountConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AccountConfig) ProtoMessage() {} - -func (x *AccountConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[1] - 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 AccountConfig.ProtoReflect.Descriptor instead. -func (*AccountConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{1} -} - -func (x *AccountConfig) GetBlastRadiusPreset() AccountConfig_BlastRadiusPreset { - if x != nil { - return x.BlastRadiusPreset - } - return AccountConfig_UNSPECIFIED -} - -func (x *AccountConfig) GetBlastRadius() *BlastRadiusConfig { - if x != nil { - return x.BlastRadius - } - return nil -} - -func (x *AccountConfig) GetSkipUnmappedChangesForRisks() bool { - if x != nil { - return x.SkipUnmappedChangesForRisks - } - return false -} - -type GetAccountConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAccountConfigRequest) Reset() { - *x = GetAccountConfigRequest{} - mi := &file_config_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAccountConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAccountConfigRequest) ProtoMessage() {} - -func (x *GetAccountConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[2] - 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 GetAccountConfigRequest.ProtoReflect.Descriptor instead. -func (*GetAccountConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{2} -} - -type GetAccountConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetAccountConfigResponse) Reset() { - *x = GetAccountConfigResponse{} - mi := &file_config_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetAccountConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetAccountConfigResponse) ProtoMessage() {} - -func (x *GetAccountConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[3] - 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 GetAccountConfigResponse.ProtoReflect.Descriptor instead. -func (*GetAccountConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{3} -} - -func (x *GetAccountConfigResponse) GetConfig() *AccountConfig { - if x != nil { - return x.Config - } - return nil -} - -// Updates the account config for the user's account. -type UpdateAccountConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAccountConfigRequest) Reset() { - *x = UpdateAccountConfigRequest{} - mi := &file_config_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAccountConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAccountConfigRequest) ProtoMessage() {} - -func (x *UpdateAccountConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[4] - 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 UpdateAccountConfigRequest.ProtoReflect.Descriptor instead. -func (*UpdateAccountConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{4} -} - -func (x *UpdateAccountConfigRequest) GetConfig() *AccountConfig { - if x != nil { - return x.Config - } - return nil -} - -type UpdateAccountConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateAccountConfigResponse) Reset() { - *x = UpdateAccountConfigResponse{} - mi := &file_config_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateAccountConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateAccountConfigResponse) ProtoMessage() {} - -func (x *UpdateAccountConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[5] - 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 UpdateAccountConfigResponse.ProtoReflect.Descriptor instead. -func (*UpdateAccountConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{5} -} - -func (x *UpdateAccountConfigResponse) GetConfig() *AccountConfig { - if x != nil { - return x.Config - } - return nil -} - -type CreateHcpConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The URL that the user should be redirected to after the whole process is - // over. This should be a page in the frontend, probably the HCP Terraform - // Integration page. - FinalFrontendRedirect string `protobuf:"bytes,1,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateHcpConfigRequest) Reset() { - *x = CreateHcpConfigRequest{} - mi := &file_config_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateHcpConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateHcpConfigRequest) ProtoMessage() {} - -func (x *CreateHcpConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[6] - 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 CreateHcpConfigRequest.ProtoReflect.Descriptor instead. -func (*CreateHcpConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{6} -} - -func (x *CreateHcpConfigRequest) GetFinalFrontendRedirect() string { - if x != nil { - return x.FinalFrontendRedirect - } - return "" -} - -type CreateHcpConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The configuration of the HCP Run Task that was created - Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - // The API Key response for the API key that backs this integration. This - // API will have been created but not yet authorised, the user must still be - // redirected to the authorizeURL to complete the process. - ApiKey *CreateAPIKeyResponse `protobuf:"bytes,2,opt,name=apiKey,proto3" json:"apiKey,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateHcpConfigResponse) Reset() { - *x = CreateHcpConfigResponse{} - mi := &file_config_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateHcpConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateHcpConfigResponse) ProtoMessage() {} - -func (x *CreateHcpConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[7] - 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 CreateHcpConfigResponse.ProtoReflect.Descriptor instead. -func (*CreateHcpConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{7} -} - -func (x *CreateHcpConfigResponse) GetConfig() *HcpConfig { - if x != nil { - return x.Config - } - return nil -} - -func (x *CreateHcpConfigResponse) GetApiKey() *CreateAPIKeyResponse { - if x != nil { - return x.ApiKey - } - return nil -} - -type HcpConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the Endpoint URL for the HCP Run Task configuration - Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` - // the HMAC secret for the HCP Run Task configuration - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *HcpConfig) Reset() { - *x = HcpConfig{} - mi := &file_config_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *HcpConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*HcpConfig) ProtoMessage() {} - -func (x *HcpConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[8] - 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 HcpConfig.ProtoReflect.Descriptor instead. -func (*HcpConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{8} -} - -func (x *HcpConfig) GetEndpoint() string { - if x != nil { - return x.Endpoint - } - return "" -} - -func (x *HcpConfig) GetSecret() string { - if x != nil { - return x.Secret - } - return "" -} - -type GetHcpConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHcpConfigRequest) Reset() { - *x = GetHcpConfigRequest{} - mi := &file_config_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHcpConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHcpConfigRequest) ProtoMessage() {} - -func (x *GetHcpConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[9] - 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 GetHcpConfigRequest.ProtoReflect.Descriptor instead. -func (*GetHcpConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{9} -} - -type GetHcpConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - Status GetHcpConfigResponse_Status `protobuf:"varint,2,opt,name=status,proto3,enum=config.GetHcpConfigResponse_Status" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHcpConfigResponse) Reset() { - *x = GetHcpConfigResponse{} - mi := &file_config_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHcpConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHcpConfigResponse) ProtoMessage() {} - -func (x *GetHcpConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[10] - 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 GetHcpConfigResponse.ProtoReflect.Descriptor instead. -func (*GetHcpConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{10} -} - -func (x *GetHcpConfigResponse) GetConfig() *HcpConfig { - if x != nil { - return x.Config - } - return nil -} - -func (x *GetHcpConfigResponse) GetStatus() GetHcpConfigResponse_Status { - if x != nil { - return x.Status - } - return GetHcpConfigResponse_CONFIGURED -} - -type DeleteHcpConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteHcpConfigRequest) Reset() { - *x = DeleteHcpConfigRequest{} - mi := &file_config_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteHcpConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteHcpConfigRequest) ProtoMessage() {} - -func (x *DeleteHcpConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[11] - 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 DeleteHcpConfigRequest.ProtoReflect.Descriptor instead. -func (*DeleteHcpConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{11} -} - -type DeleteHcpConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteHcpConfigResponse) Reset() { - *x = DeleteHcpConfigResponse{} - mi := &file_config_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteHcpConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteHcpConfigResponse) ProtoMessage() {} - -func (x *DeleteHcpConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[12] - 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 DeleteHcpConfigResponse.ProtoReflect.Descriptor instead. -func (*DeleteHcpConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{12} -} - -// Account Signal config -type GetSignalConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSignalConfigRequest) Reset() { - *x = GetSignalConfigRequest{} - mi := &file_config_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSignalConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSignalConfigRequest) ProtoMessage() {} - -func (x *GetSignalConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSignalConfigRequest.ProtoReflect.Descriptor instead. -func (*GetSignalConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{13} -} - -type GetSignalConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSignalConfigResponse) Reset() { - *x = GetSignalConfigResponse{} - mi := &file_config_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSignalConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSignalConfigResponse) ProtoMessage() {} - -func (x *GetSignalConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSignalConfigResponse.ProtoReflect.Descriptor instead. -func (*GetSignalConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{14} -} - -func (x *GetSignalConfigResponse) GetConfig() *SignalConfig { - if x != nil { - return x.Config - } - return nil -} - -// Updates the signal config for the account. -type UpdateSignalConfigRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSignalConfigRequest) Reset() { - *x = UpdateSignalConfigRequest{} - mi := &file_config_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSignalConfigRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSignalConfigRequest) ProtoMessage() {} - -func (x *UpdateSignalConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[15] - 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 UpdateSignalConfigRequest.ProtoReflect.Descriptor instead. -func (*UpdateSignalConfigRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{15} -} - -func (x *UpdateSignalConfigRequest) GetConfig() *SignalConfig { - if x != nil { - return x.Config - } - return nil -} - -type UpdateSignalConfigResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSignalConfigResponse) Reset() { - *x = UpdateSignalConfigResponse{} - mi := &file_config_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSignalConfigResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSignalConfigResponse) ProtoMessage() {} - -func (x *UpdateSignalConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[16] - 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 UpdateSignalConfigResponse.ProtoReflect.Descriptor instead. -func (*UpdateSignalConfigResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{16} -} - -func (x *UpdateSignalConfigResponse) GetConfig() *SignalConfig { - if x != nil { - return x.Config - } - return nil -} - -type SignalConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Config for aggregation parameters, such as alpha - AggregationConfig *AggregationConfig `protobuf:"bytes,1,opt,name=aggregationConfig,proto3" json:"aggregationConfig,omitempty"` - // Config for routine changes, such as events per day and duration - RoutineChangesConfig *RoutineChangesConfig `protobuf:"bytes,2,opt,name=routineChangesConfig,proto3" json:"routineChangesConfig,omitempty"` - // Config for Github app profile, such as primary branch name - GithubOrganisationProfile *GithubOrganisationProfile `protobuf:"bytes,3,opt,name=githubOrganisationProfile,proto3,oneof" json:"githubOrganisationProfile,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SignalConfig) Reset() { - *x = SignalConfig{} - mi := &file_config_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SignalConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SignalConfig) ProtoMessage() {} - -func (x *SignalConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SignalConfig.ProtoReflect.Descriptor instead. -func (*SignalConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{17} -} - -func (x *SignalConfig) GetAggregationConfig() *AggregationConfig { - if x != nil { - return x.AggregationConfig - } - return nil -} - -func (x *SignalConfig) GetRoutineChangesConfig() *RoutineChangesConfig { - if x != nil { - return x.RoutineChangesConfig - } - return nil -} - -func (x *SignalConfig) GetGithubOrganisationProfile() *GithubOrganisationProfile { - if x != nil { - return x.GithubOrganisationProfile - } - return nil -} - -type AggregationConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Alpha parameter for aggregation: controls the weighting of recent data versus older data - // Must be positive (greater than 0) as it's the temperature parameter for exponential decay - Alpha float32 `protobuf:"fixed32,1,opt,name=alpha,proto3" json:"alpha,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AggregationConfig) Reset() { - *x = AggregationConfig{} - mi := &file_config_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AggregationConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AggregationConfig) ProtoMessage() {} - -func (x *AggregationConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AggregationConfig.ProtoReflect.Descriptor instead. -func (*AggregationConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{18} -} - -func (x *AggregationConfig) GetAlpha() float32 { - if x != nil { - return x.Alpha - } - return 0 -} - -type RoutineChangesConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The user will see the format of "12 changes per day for 3 weeks" with the user able to change these values i.e. - // Events per days, weeks, or months - EventsPer float32 `protobuf:"fixed32,1,opt,name=eventsPer,proto3" json:"eventsPer,omitempty"` - EventsPerUnit RoutineChangesConfig_DurationUnit `protobuf:"varint,2,opt,name=eventsPerUnit,proto3,enum=config.RoutineChangesConfig_DurationUnit" json:"eventsPerUnit,omitempty"` - // Duration the number of days, weeks, or months over which routine changes are considered. - Duration float32 `protobuf:"fixed32,3,opt,name=duration,proto3" json:"duration,omitempty"` - // Specifies the unit of time for the duration field in routine changes. - // The available units are days, weeks, and months. - DurationUnit RoutineChangesConfig_DurationUnit `protobuf:"varint,4,opt,name=durationUnit,proto3,enum=config.RoutineChangesConfig_DurationUnit" json:"durationUnit,omitempty"` - // Sensitivity parameter that controls the threshold for detecting routine changes. - // A higher sensitivity value makes the detection more responsive to smaller changes, - // while a lower value makes it less responsive. - Sensitivity float32 `protobuf:"fixed32,5,opt,name=sensitivity,proto3" json:"sensitivity,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RoutineChangesConfig) Reset() { - *x = RoutineChangesConfig{} - mi := &file_config_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RoutineChangesConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RoutineChangesConfig) ProtoMessage() {} - -func (x *RoutineChangesConfig) ProtoReflect() protoreflect.Message { - mi := &file_config_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 RoutineChangesConfig.ProtoReflect.Descriptor instead. -func (*RoutineChangesConfig) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{19} -} - -func (x *RoutineChangesConfig) GetEventsPer() float32 { - if x != nil { - return x.EventsPer - } - return 0 -} - -func (x *RoutineChangesConfig) GetEventsPerUnit() RoutineChangesConfig_DurationUnit { - if x != nil { - return x.EventsPerUnit - } - return RoutineChangesConfig_DAYS -} - -func (x *RoutineChangesConfig) GetDuration() float32 { - if x != nil { - return x.Duration - } - return 0 -} - -func (x *RoutineChangesConfig) GetDurationUnit() RoutineChangesConfig_DurationUnit { - if x != nil { - return x.DurationUnit - } - return RoutineChangesConfig_DAYS -} - -func (x *RoutineChangesConfig) GetSensitivity() float32 { - if x != nil { - return x.Sensitivity - } - return 0 -} - -// no parameters required, we will look up the installation ID from the account ID -type GetGithubAppInformationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetGithubAppInformationRequest) Reset() { - *x = GetGithubAppInformationRequest{} - mi := &file_config_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetGithubAppInformationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetGithubAppInformationRequest) ProtoMessage() {} - -func (x *GetGithubAppInformationRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[20] - 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 GetGithubAppInformationRequest.ProtoReflect.Descriptor instead. -func (*GetGithubAppInformationRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{20} -} - -// it will be used to display information to the github integrations page, it is not used for signal processing -type GithubAppInformation struct { - state protoimpl.MessageState `protogen:"open.v1"` - InstallationID int64 `protobuf:"varint,1,opt,name=installationID,proto3" json:"installationID,omitempty"` - InstalledBy string `protobuf:"bytes,2,opt,name=installedBy,proto3" json:"installedBy,omitempty"` - InstalledAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=installedAt,proto3" json:"installedAt,omitempty"` - OrganisationName string `protobuf:"bytes,4,opt,name=organisationName,proto3" json:"organisationName,omitempty"` - ActiveRepositoryCount int64 `protobuf:"varint,5,opt,name=activeRepositoryCount,proto3" json:"activeRepositoryCount,omitempty"` - ContributorCount int64 `protobuf:"varint,6,opt,name=contributorCount,proto3" json:"contributorCount,omitempty"` - BotAutomationPercentage int64 `protobuf:"varint,7,opt,name=botAutomationPercentage,proto3" json:"botAutomationPercentage,omitempty"` - AverageMergeTime string `protobuf:"bytes,8,opt,name=averageMergeTime,proto3" json:"averageMergeTime,omitempty"` - AverageCommitFrequency string `protobuf:"bytes,9,opt,name=averageCommitFrequency,proto3" json:"averageCommitFrequency,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GithubAppInformation) Reset() { - *x = GithubAppInformation{} - mi := &file_config_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GithubAppInformation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GithubAppInformation) ProtoMessage() {} - -func (x *GithubAppInformation) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[21] - 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 GithubAppInformation.ProtoReflect.Descriptor instead. -func (*GithubAppInformation) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{21} -} - -func (x *GithubAppInformation) GetInstallationID() int64 { - if x != nil { - return x.InstallationID - } - return 0 -} - -func (x *GithubAppInformation) GetInstalledBy() string { - if x != nil { - return x.InstalledBy - } - return "" -} - -func (x *GithubAppInformation) GetInstalledAt() *timestamppb.Timestamp { - if x != nil { - return x.InstalledAt - } - return nil -} - -func (x *GithubAppInformation) GetOrganisationName() string { - if x != nil { - return x.OrganisationName - } - return "" -} - -func (x *GithubAppInformation) GetActiveRepositoryCount() int64 { - if x != nil { - return x.ActiveRepositoryCount - } - return 0 -} - -func (x *GithubAppInformation) GetContributorCount() int64 { - if x != nil { - return x.ContributorCount - } - return 0 -} - -func (x *GithubAppInformation) GetBotAutomationPercentage() int64 { - if x != nil { - return x.BotAutomationPercentage - } - return 0 -} - -func (x *GithubAppInformation) GetAverageMergeTime() string { - if x != nil { - return x.AverageMergeTime - } - return "" -} - -func (x *GithubAppInformation) GetAverageCommitFrequency() string { - if x != nil { - return x.AverageCommitFrequency - } - return "" -} - -// this is all the information required to display the github app information -type GetGithubAppInformationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - GithubAppInformation *GithubAppInformation `protobuf:"bytes,1,opt,name=githubAppInformation,proto3" json:"githubAppInformation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetGithubAppInformationResponse) Reset() { - *x = GetGithubAppInformationResponse{} - mi := &file_config_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetGithubAppInformationResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetGithubAppInformationResponse) ProtoMessage() {} - -func (x *GetGithubAppInformationResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[22] - 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 GetGithubAppInformationResponse.ProtoReflect.Descriptor instead. -func (*GetGithubAppInformationResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{22} -} - -func (x *GetGithubAppInformationResponse) GetGithubAppInformation() *GithubAppInformation { - if x != nil { - return x.GithubAppInformation - } - return nil -} - -// no parameters required, we will look up the installation ID from the account ID -type RegenerateGithubAppProfileRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RegenerateGithubAppProfileRequest) Reset() { - *x = RegenerateGithubAppProfileRequest{} - mi := &file_config_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RegenerateGithubAppProfileRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RegenerateGithubAppProfileRequest) ProtoMessage() {} - -func (x *RegenerateGithubAppProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[23] - 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 RegenerateGithubAppProfileRequest.ProtoReflect.Descriptor instead. -func (*RegenerateGithubAppProfileRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{23} -} - -// information stored in the database, used by signal processing in change analysis -type GithubOrganisationProfile struct { - state protoimpl.MessageState `protogen:"open.v1"` - PrimaryBranchName string `protobuf:"bytes,1,opt,name=primaryBranchName,proto3" json:"primaryBranchName,omitempty"` - // signal scores that will be given for each hour of the day, 0-23, from -5.0 to 5.0 - HourlyScores []float64 `protobuf:"fixed64,2,rep,packed,name=hourlyScores,proto3" json:"hourlyScores,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GithubOrganisationProfile) Reset() { - *x = GithubOrganisationProfile{} - mi := &file_config_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GithubOrganisationProfile) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GithubOrganisationProfile) ProtoMessage() {} - -func (x *GithubOrganisationProfile) ProtoReflect() protoreflect.Message { - mi := &file_config_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 GithubOrganisationProfile.ProtoReflect.Descriptor instead. -func (*GithubOrganisationProfile) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{24} -} - -func (x *GithubOrganisationProfile) GetPrimaryBranchName() string { - if x != nil { - return x.PrimaryBranchName - } - return "" -} - -func (x *GithubOrganisationProfile) GetHourlyScores() []float64 { - if x != nil { - return x.HourlyScores - } - return nil -} - -type RegenerateGithubAppProfileResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - GithubOrganisationProfile *GithubOrganisationProfile `protobuf:"bytes,1,opt,name=githubOrganisationProfile,proto3" json:"githubOrganisationProfile,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RegenerateGithubAppProfileResponse) Reset() { - *x = RegenerateGithubAppProfileResponse{} - mi := &file_config_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RegenerateGithubAppProfileResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RegenerateGithubAppProfileResponse) ProtoMessage() {} - -func (x *RegenerateGithubAppProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[25] - 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 RegenerateGithubAppProfileResponse.ProtoReflect.Descriptor instead. -func (*RegenerateGithubAppProfileResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{25} -} - -func (x *RegenerateGithubAppProfileResponse) GetGithubOrganisationProfile() *GithubOrganisationProfile { - if x != nil { - return x.GithubOrganisationProfile - } - return nil -} - -// no parameters required, we will look up the installation ID from the account ID -type DeleteGithubAppProfileAndGithubInstallationIDRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) Reset() { - *x = DeleteGithubAppProfileAndGithubInstallationIDRequest{} - mi := &file_config_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoMessage() {} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[26] - 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 DeleteGithubAppProfileAndGithubInstallationIDRequest.ProtoReflect.Descriptor instead. -func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{26} -} - -// status codes to indicate if the deletion was successful -type DeleteGithubAppProfileAndGithubInstallationIDResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) Reset() { - *x = DeleteGithubAppProfileAndGithubInstallationIDResponse{} - mi := &file_config_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoMessage() {} - -func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_config_proto_msgTypes[27] - 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 DeleteGithubAppProfileAndGithubInstallationIDResponse.ProtoReflect.Descriptor instead. -func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) Descriptor() ([]byte, []int) { - return file_config_proto_rawDescGZIP(), []int{27} -} - -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\"\xd8\x01\n" + - "\x11BlastRadiusConfig\x12\x1a\n" + - "\bmaxItems\x18\x01 \x01(\x05R\bmaxItems\x12\x1c\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\"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" + - "\f_blastRadius\"\x19\n" + - "\x17GetAccountConfigRequest\"I\n" + - "\x18GetAccountConfigResponse\x12-\n" + - "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"K\n" + - "\x1aUpdateAccountConfigRequest\x12-\n" + - "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"L\n" + - "\x1bUpdateAccountConfigResponse\x12-\n" + - "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"N\n" + - "\x16CreateHcpConfigRequest\x124\n" + - "\x15finalFrontendRedirect\x18\x01 \x01(\tR\x15finalFrontendRedirect\"{\n" + - "\x17CreateHcpConfigResponse\x12)\n" + - "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x125\n" + - "\x06apiKey\x18\x02 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\x06apiKey\"?\n" + - "\tHcpConfig\x12\x1a\n" + - "\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" + - "\x06secret\x18\x02 \x01(\tR\x06secret\"\x15\n" + - "\x13GetHcpConfigRequest\"\xa3\x01\n" + - "\x14GetHcpConfigResponse\x12)\n" + - "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x12;\n" + - "\x06status\x18\x02 \x01(\x0e2#.config.GetHcpConfigResponse.StatusR\x06status\"#\n" + - "\x06Status\x12\x0e\n" + - "\n" + - "CONFIGURED\x10\x00\x12\t\n" + - "\x05ERROR\x10\x01\"\x18\n" + - "\x16DeleteHcpConfigRequest\"\x19\n" + - "\x17DeleteHcpConfigResponse\"\x18\n" + - "\x16GetSignalConfigRequest\"G\n" + - "\x17GetSignalConfigResponse\x12,\n" + - "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"I\n" + - "\x19UpdateSignalConfigRequest\x12,\n" + - "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"J\n" + - "\x1aUpdateSignalConfigResponse\x12,\n" + - "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"\xad\x02\n" + - "\fSignalConfig\x12G\n" + - "\x11aggregationConfig\x18\x01 \x01(\v2\x19.config.AggregationConfigR\x11aggregationConfig\x12P\n" + - "\x14routineChangesConfig\x18\x02 \x01(\v2\x1c.config.RoutineChangesConfigR\x14routineChangesConfig\x12d\n" + - "\x19githubOrganisationProfile\x18\x03 \x01(\v2!.config.GithubOrganisationProfileH\x00R\x19githubOrganisationProfile\x88\x01\x01B\x1c\n" + - "\x1a_githubOrganisationProfile\"5\n" + - "\x11AggregationConfig\x12 \n" + - "\x05alpha\x18\x01 \x01(\x02B\n" + - "\xbaH\a\n" + - "\x05%\x00\x00\x00\x00R\x05alpha\"\xc3\x02\n" + - "\x14RoutineChangesConfig\x12\x1c\n" + - "\teventsPer\x18\x01 \x01(\x02R\teventsPer\x12O\n" + - "\reventsPerUnit\x18\x02 \x01(\x0e2).config.RoutineChangesConfig.DurationUnitR\reventsPerUnit\x12\x1a\n" + - "\bduration\x18\x03 \x01(\x02R\bduration\x12M\n" + - "\fdurationUnit\x18\x04 \x01(\x0e2).config.RoutineChangesConfig.DurationUnitR\fdurationUnit\x12 \n" + - "\vsensitivity\x18\x05 \x01(\x02R\vsensitivity\"/\n" + - "\fDurationUnit\x12\b\n" + - "\x04DAYS\x10\x00\x12\t\n" + - "\x05WEEKS\x10\x01\x12\n" + - "\n" + - "\x06MONTHS\x10\x02\" \n" + - "\x1eGetGithubAppInformationRequest\"\xca\x03\n" + - "\x14GithubAppInformation\x12&\n" + - "\x0einstallationID\x18\x01 \x01(\x03R\x0einstallationID\x12 \n" + - "\vinstalledBy\x18\x02 \x01(\tR\vinstalledBy\x12<\n" + - "\vinstalledAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\vinstalledAt\x12*\n" + - "\x10organisationName\x18\x04 \x01(\tR\x10organisationName\x124\n" + - "\x15activeRepositoryCount\x18\x05 \x01(\x03R\x15activeRepositoryCount\x12*\n" + - "\x10contributorCount\x18\x06 \x01(\x03R\x10contributorCount\x128\n" + - "\x17botAutomationPercentage\x18\a \x01(\x03R\x17botAutomationPercentage\x12*\n" + - "\x10averageMergeTime\x18\b \x01(\tR\x10averageMergeTime\x126\n" + - "\x16averageCommitFrequency\x18\t \x01(\tR\x16averageCommitFrequency\"s\n" + - "\x1fGetGithubAppInformationResponse\x12P\n" + - "\x14githubAppInformation\x18\x01 \x01(\v2\x1c.config.GithubAppInformationR\x14githubAppInformation\"#\n" + - "!RegenerateGithubAppProfileRequest\"\x8f\x01\n" + - "\x19GithubOrganisationProfile\x12,\n" + - "\x11primaryBranchName\x18\x01 \x01(\tR\x11primaryBranchName\x12D\n" + - "\fhourlyScores\x18\x02 \x03(\x01B \xbaH\x1d\x92\x01\x1a\b\x18\x10\x18\"\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x14@)\x00\x00\x00\x00\x00\x00\x14\xc0R\fhourlyScores\"\x85\x01\n" + - "\"RegenerateGithubAppProfileResponse\x12_\n" + - "\x19githubOrganisationProfile\x18\x01 \x01(\v2!.config.GithubOrganisationProfileR\x19githubOrganisationProfile\"6\n" + - "4DeleteGithubAppProfileAndGithubInstallationIDRequest\"7\n" + - "5DeleteGithubAppProfileAndGithubInstallationIDResponse2\x81\b\n" + - "\x14ConfigurationService\x12U\n" + - "\x10GetAccountConfig\x12\x1f.config.GetAccountConfigRequest\x1a .config.GetAccountConfigResponse\x12^\n" + - "\x13UpdateAccountConfig\x12\".config.UpdateAccountConfigRequest\x1a#.config.UpdateAccountConfigResponse\x12R\n" + - "\x0fCreateHcpConfig\x12\x1e.config.CreateHcpConfigRequest\x1a\x1f.config.CreateHcpConfigResponse\x12I\n" + - "\fGetHcpConfig\x12\x1b.config.GetHcpConfigRequest\x1a\x1c.config.GetHcpConfigResponse\x12R\n" + - "\x0fDeleteHcpConfig\x12\x1e.config.DeleteHcpConfigRequest\x1a\x1f.config.DeleteHcpConfigResponse\x12R\n" + - "\x0fGetSignalConfig\x12\x1e.config.GetSignalConfigRequest\x1a\x1f.config.GetSignalConfigResponse\x12[\n" + - "\x12UpdateSignalConfig\x12!.config.UpdateSignalConfigRequest\x1a\".config.UpdateSignalConfigResponse\x12j\n" + - "\x17GetGithubAppInformation\x12&.config.GetGithubAppInformationRequest\x1a'.config.GetGithubAppInformationResponse\x12s\n" + - "\x1aRegenerateGithubAppProfile\x12).config.RegenerateGithubAppProfileRequest\x1a*.config.RegenerateGithubAppProfileResponse\x12\xac\x01\n" + - "-DeleteGithubAppProfileAndGithubInstallationID\x12<.config.DeleteGithubAppProfileAndGithubInstallationIDRequest\x1a=.config.DeleteGithubAppProfileAndGithubInstallationIDResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_config_proto_rawDescOnce sync.Once - file_config_proto_rawDescData []byte -) - -func file_config_proto_rawDescGZIP() []byte { - file_config_proto_rawDescOnce.Do(func() { - file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) - }) - return file_config_proto_rawDescData -} - -var file_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 28) -var file_config_proto_goTypes = []any{ - (AccountConfig_BlastRadiusPreset)(0), // 0: config.AccountConfig.BlastRadiusPreset - (GetHcpConfigResponse_Status)(0), // 1: config.GetHcpConfigResponse.Status - (RoutineChangesConfig_DurationUnit)(0), // 2: config.RoutineChangesConfig.DurationUnit - (*BlastRadiusConfig)(nil), // 3: config.BlastRadiusConfig - (*AccountConfig)(nil), // 4: config.AccountConfig - (*GetAccountConfigRequest)(nil), // 5: config.GetAccountConfigRequest - (*GetAccountConfigResponse)(nil), // 6: config.GetAccountConfigResponse - (*UpdateAccountConfigRequest)(nil), // 7: config.UpdateAccountConfigRequest - (*UpdateAccountConfigResponse)(nil), // 8: config.UpdateAccountConfigResponse - (*CreateHcpConfigRequest)(nil), // 9: config.CreateHcpConfigRequest - (*CreateHcpConfigResponse)(nil), // 10: config.CreateHcpConfigResponse - (*HcpConfig)(nil), // 11: config.HcpConfig - (*GetHcpConfigRequest)(nil), // 12: config.GetHcpConfigRequest - (*GetHcpConfigResponse)(nil), // 13: config.GetHcpConfigResponse - (*DeleteHcpConfigRequest)(nil), // 14: config.DeleteHcpConfigRequest - (*DeleteHcpConfigResponse)(nil), // 15: config.DeleteHcpConfigResponse - (*GetSignalConfigRequest)(nil), // 16: config.GetSignalConfigRequest - (*GetSignalConfigResponse)(nil), // 17: config.GetSignalConfigResponse - (*UpdateSignalConfigRequest)(nil), // 18: config.UpdateSignalConfigRequest - (*UpdateSignalConfigResponse)(nil), // 19: config.UpdateSignalConfigResponse - (*SignalConfig)(nil), // 20: config.SignalConfig - (*AggregationConfig)(nil), // 21: config.AggregationConfig - (*RoutineChangesConfig)(nil), // 22: config.RoutineChangesConfig - (*GetGithubAppInformationRequest)(nil), // 23: config.GetGithubAppInformationRequest - (*GithubAppInformation)(nil), // 24: config.GithubAppInformation - (*GetGithubAppInformationResponse)(nil), // 25: config.GetGithubAppInformationResponse - (*RegenerateGithubAppProfileRequest)(nil), // 26: config.RegenerateGithubAppProfileRequest - (*GithubOrganisationProfile)(nil), // 27: config.GithubOrganisationProfile - (*RegenerateGithubAppProfileResponse)(nil), // 28: config.RegenerateGithubAppProfileResponse - (*DeleteGithubAppProfileAndGithubInstallationIDRequest)(nil), // 29: config.DeleteGithubAppProfileAndGithubInstallationIDRequest - (*DeleteGithubAppProfileAndGithubInstallationIDResponse)(nil), // 30: config.DeleteGithubAppProfileAndGithubInstallationIDResponse - (*durationpb.Duration)(nil), // 31: google.protobuf.Duration - (*CreateAPIKeyResponse)(nil), // 32: apikeys.CreateAPIKeyResponse - (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp -} -var file_config_proto_depIdxs = []int32{ - 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 - 4, // 4: config.UpdateAccountConfigRequest.config:type_name -> config.AccountConfig - 4, // 5: config.UpdateAccountConfigResponse.config:type_name -> config.AccountConfig - 11, // 6: config.CreateHcpConfigResponse.config:type_name -> config.HcpConfig - 32, // 7: config.CreateHcpConfigResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse - 11, // 8: config.GetHcpConfigResponse.config:type_name -> config.HcpConfig - 1, // 9: config.GetHcpConfigResponse.status:type_name -> config.GetHcpConfigResponse.Status - 20, // 10: config.GetSignalConfigResponse.config:type_name -> config.SignalConfig - 20, // 11: config.UpdateSignalConfigRequest.config:type_name -> config.SignalConfig - 20, // 12: config.UpdateSignalConfigResponse.config:type_name -> config.SignalConfig - 21, // 13: config.SignalConfig.aggregationConfig:type_name -> config.AggregationConfig - 22, // 14: config.SignalConfig.routineChangesConfig:type_name -> config.RoutineChangesConfig - 27, // 15: config.SignalConfig.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile - 2, // 16: config.RoutineChangesConfig.eventsPerUnit:type_name -> config.RoutineChangesConfig.DurationUnit - 2, // 17: config.RoutineChangesConfig.durationUnit:type_name -> config.RoutineChangesConfig.DurationUnit - 33, // 18: config.GithubAppInformation.installedAt:type_name -> google.protobuf.Timestamp - 24, // 19: config.GetGithubAppInformationResponse.githubAppInformation:type_name -> config.GithubAppInformation - 27, // 20: config.RegenerateGithubAppProfileResponse.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile - 5, // 21: config.ConfigurationService.GetAccountConfig:input_type -> config.GetAccountConfigRequest - 7, // 22: config.ConfigurationService.UpdateAccountConfig:input_type -> config.UpdateAccountConfigRequest - 9, // 23: config.ConfigurationService.CreateHcpConfig:input_type -> config.CreateHcpConfigRequest - 12, // 24: config.ConfigurationService.GetHcpConfig:input_type -> config.GetHcpConfigRequest - 14, // 25: config.ConfigurationService.DeleteHcpConfig:input_type -> config.DeleteHcpConfigRequest - 16, // 26: config.ConfigurationService.GetSignalConfig:input_type -> config.GetSignalConfigRequest - 18, // 27: config.ConfigurationService.UpdateSignalConfig:input_type -> config.UpdateSignalConfigRequest - 23, // 28: config.ConfigurationService.GetGithubAppInformation:input_type -> config.GetGithubAppInformationRequest - 26, // 29: config.ConfigurationService.RegenerateGithubAppProfile:input_type -> config.RegenerateGithubAppProfileRequest - 29, // 30: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:input_type -> config.DeleteGithubAppProfileAndGithubInstallationIDRequest - 6, // 31: config.ConfigurationService.GetAccountConfig:output_type -> config.GetAccountConfigResponse - 8, // 32: config.ConfigurationService.UpdateAccountConfig:output_type -> config.UpdateAccountConfigResponse - 10, // 33: config.ConfigurationService.CreateHcpConfig:output_type -> config.CreateHcpConfigResponse - 13, // 34: config.ConfigurationService.GetHcpConfig:output_type -> config.GetHcpConfigResponse - 15, // 35: config.ConfigurationService.DeleteHcpConfig:output_type -> config.DeleteHcpConfigResponse - 17, // 36: config.ConfigurationService.GetSignalConfig:output_type -> config.GetSignalConfigResponse - 19, // 37: config.ConfigurationService.UpdateSignalConfig:output_type -> config.UpdateSignalConfigResponse - 25, // 38: config.ConfigurationService.GetGithubAppInformation:output_type -> config.GetGithubAppInformationResponse - 28, // 39: config.ConfigurationService.RegenerateGithubAppProfile:output_type -> config.RegenerateGithubAppProfileResponse - 30, // 40: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:output_type -> config.DeleteGithubAppProfileAndGithubInstallationIDResponse - 31, // [31:41] is the sub-list for method output_type - 21, // [21:31] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name -} - -func init() { file_config_proto_init() } -func file_config_proto_init() { - if File_config_proto != nil { - return - } - file_apikeys_proto_init() - file_config_proto_msgTypes[0].OneofWrappers = []any{} - file_config_proto_msgTypes[1].OneofWrappers = []any{} - file_config_proto_msgTypes[17].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), - NumEnums: 3, - NumMessages: 28, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_config_proto_goTypes, - DependencyIndexes: file_config_proto_depIdxs, - EnumInfos: file_config_proto_enumTypes, - MessageInfos: file_config_proto_msgTypes, - }.Build() - File_config_proto = out.File - file_config_proto_goTypes = nil - file_config_proto_depIdxs = nil -} diff --git a/sdp-go/connection.go b/sdp-go/connection.go deleted file mode 100644 index f125afc0..00000000 --- a/sdp-go/connection.go +++ /dev/null @@ -1,180 +0,0 @@ -package sdp - -import ( - "context" - "fmt" - reflect "reflect" - - "github.com/nats-io/nats.go" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" -) - -// EncodedConnection is an interface that allows messages to be published to it. -// In production this would always be filled by a *nats.EncodedConn, however in -// testing we will mock this with something that does nothing -type EncodedConnection interface { - Publish(ctx context.Context, subj string, m proto.Message) error - PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error - PublishMsg(ctx context.Context, msg *nats.Msg) error - Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) - QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) - RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) - - Status() nats.Status - Stats() nats.Statistics - LastError() error - - Drain() error - Close() - - Underlying() *nats.Conn - Drop() -} - -type EncodedConnectionImpl struct { - Conn *nats.Conn -} - -// assert interface implementation -var _ EncodedConnection = (*EncodedConnectionImpl)(nil) - -func recordMessage(ctx context.Context, name, subj, typ, msg string) { - log.WithContext(ctx).WithFields(log.Fields{ - "msg": msg, - "subj": subj, - "typ": typ, - }).Trace(name) - // avoid spamming honeycomb - if log.GetLevel() == log.TraceLevel { - span := trace.SpanFromContext(ctx) - span.AddEvent(name, trace.WithAttributes( - attribute.String("ovm.sdp.subject", subj), - attribute.String("ovm.sdp.message", msg), - )) - } -} - -func (ec *EncodedConnectionImpl) Publish(ctx context.Context, subj string, m proto.Message) error { - // TODO: protojson.Format is pretty expensive, replace with summarized data - recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) - - data, err := proto.Marshal(m) - if err != nil { - return err - } - - msg := &nats.Msg{ - Subject: subj, - Data: data, - } - InjectOtelTraceContext(ctx, msg) - return ec.Conn.PublishMsg(msg) -} - -func (ec *EncodedConnectionImpl) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { - // TODO: protojson.Format is pretty expensive, replace with summarized data - recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) - - data, err := proto.Marshal(m) - if err != nil { - return err - } - - msg := &nats.Msg{ - Subject: subj, - Data: data, - } - msg.Header.Add("reply-to", replyTo) - InjectOtelTraceContext(ctx, msg) - return ec.Conn.PublishMsg(msg) -} - -func (ec *EncodedConnectionImpl) PublishMsg(ctx context.Context, msg *nats.Msg) error { - recordMessage(ctx, "Publish", msg.Subject, "[]byte", "binary") - - InjectOtelTraceContext(ctx, msg) - return ec.Conn.PublishMsg(msg) -} - -// Subscribe Use genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling -func (ec *EncodedConnectionImpl) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { - return ec.Conn.Subscribe(subj, cb) -} - -// QueueSubscribe Use genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling -func (ec *EncodedConnectionImpl) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { - return ec.Conn.QueueSubscribe(subj, queue, cb) -} - -func (ec *EncodedConnectionImpl) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { - recordMessage(ctx, "RequestMsg", msg.Subject, "[]byte", "binary") - InjectOtelTraceContext(ctx, msg) - reply, err := ec.Conn.RequestMsgWithContext(ctx, msg) - - if err != nil { - recordMessage(ctx, "RequestMsg Error", msg.Subject, fmt.Sprint(reflect.TypeOf(err)), err.Error()) - } else { - recordMessage(ctx, "RequestMsg Reply", msg.Subject, "[]byte", "binary") - } - return reply, err -} - -func (ec *EncodedConnectionImpl) Drain() error { - return ec.Conn.Drain() -} -func (ec *EncodedConnectionImpl) Close() { - ec.Conn.Close() -} - -func (ec *EncodedConnectionImpl) Status() nats.Status { - return ec.Conn.Status() -} - -func (ec *EncodedConnectionImpl) Stats() nats.Statistics { - return ec.Conn.Stats() -} - -func (ec *EncodedConnectionImpl) LastError() error { - return ec.Conn.LastError() -} - -func (ec *EncodedConnectionImpl) Underlying() *nats.Conn { - return ec.Conn -} - -// Drop Drops the underlying connection completely -func (ec *EncodedConnectionImpl) Drop() { - ec.Conn = nil -} - -// Unmarshal Does a proto.Unmarshal and logs errors in a consistent way. The -// user should still validate that the message is valid as it's possible to -// unmarshal data from one message format into another without an error. -// Validation should be based on the type that the data is being unmarshaled -// into. -func Unmarshal(ctx context.Context, b []byte, m proto.Message) error { - err := proto.Unmarshal(b, m) - if err != nil { - recordMessage(ctx, "Unmarshal err", "unknown", fmt.Sprint(reflect.TypeOf(err)), err.Error()) - log.WithContext(ctx).Errorf("Error parsing message: %v", err) - trace.SpanFromContext(ctx).SetStatus(codes.Error, fmt.Sprintf("Error parsing message: %v", err)) - return err - } - - recordMessage(ctx, "Unmarshal", "unknown", fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) - return nil -} - -//go:generate go run genhandler.go Query -//go:generate go run genhandler.go QueryResponse -//go:generate go run genhandler.go CancelQuery - -//go:generate go run genhandler.go GatewayResponse - -//go:generate go run genhandler.go NATSGetLogRecordsRequest -//go:generate go run genhandler.go NATSGetLogRecordsResponse diff --git a/sdp-go/connection_test.go b/sdp-go/connection_test.go deleted file mode 100644 index 13661c22..00000000 --- a/sdp-go/connection_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package sdp - -import ( - "context" - "testing" -) - -// This is an example of a Query with a timeout. This attribute was removed and -// replaced with a `reserved` field. Therefore it is being used to test how we -// handle older messages -var exampleRemovedAttribute = []byte{ - 0xa, 0x3, 0x66, 0x6f, 0x6f, 0x1a, 0x3, 0x66, 0x6f, 0x6f, 0x22, 0x4, 0x8, - 0xa, 0x10, 0x1, 0x2a, 0x3, 0x66, 0x6f, 0x6f, 0x30, 0x1, 0x3a, 0x10, 0x4e, - 0x43, 0x68, 0xd9, 0x17, 0xd4, 0x4d, 0x83, 0xa9, 0xe6, 0xf5, 0x3a, 0xec, - 0xc7, 0xe7, 0xf0, 0x42, 0x2, 0x8, 0xa, -} - -func TestUnmarshal(t *testing.T) { - ctx := context.Background() - query := new(Query) - - err := Unmarshal(ctx, exampleRemovedAttribute, query) - - if err != nil { - t.Error(err) - } -} diff --git a/sdp-go/encoder_test.go b/sdp-go/encoder_test.go deleted file mode 100644 index c92b03bf..00000000 --- a/sdp-go/encoder_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package sdp - -import ( - "context" - "testing" - "time" - - "github.com/google/uuid" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var _u = uuid.New() - -var query = Query{ - Type: "user", - Method: QueryMethod_LIST, - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 10, - }, - Scope: "test", - UUID: _u[:], - Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), -} - -var itemAttributes = ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "foo": { - Kind: &structpb.Value_StringValue{ - StringValue: "bar", - }, - }, - }, - }, -} - -var metadata = Metadata{ - SourceName: "users", - SourceQuery: &Query{ - Type: "user", - Method: QueryMethod_LIST, - Query: "*", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 12, - }, - Scope: "testScope", - }, - Timestamp: timestamppb.Now(), - SourceDuration: &durationpb.Duration{ - Seconds: 1, - Nanos: 1, - }, - SourceDurationPerItem: &durationpb.Duration{ - Seconds: 0, - Nanos: 500, - }, -} - -var item = Item{ - Type: "user", - UniqueAttribute: "name", - Attributes: &itemAttributes, - Metadata: &metadata, -} - -var items = Items{ - Items: []*Item{ - &item, - }, -} - -var reference = Reference{ - Type: "user", - UniqueAttributeValue: "dylan", - Scope: "test", -} - -var queryError = QueryError{ - ErrorType: QueryError_OTHER, - ErrorString: "uh oh", - Scope: "test", -} - -var ru = uuid.New() - -var response = Response{ - Responder: "test", - ResponderUUID: ru[:], - State: ResponderState_WORKING, - NextUpdateIn: &durationpb.Duration{ - Seconds: 10, - Nanos: 0, - }, -} - -var messages = []proto.Message{ - &query, - &itemAttributes, - &metadata, - &item, - &items, - &reference, - &queryError, - &response, -} - -// TestEncode Make sure that we can encode all of the message types without -// raising any errors -func TestEncode(t *testing.T) { - for _, message := range messages { - _, err := proto.Marshal(message) - if err != nil { - t.Error(err) - } - } -} - -var decodeTests = []struct { - Message proto.Message - Target proto.Message -}{ - { - Message: &query, - Target: &Query{}, - }, - { - Message: &itemAttributes, - Target: &ItemAttributes{}, - }, - { - Message: &metadata, - Target: &Metadata{}, - }, - { - Message: &item, - Target: &Item{}, - }, - { - Message: &items, - Target: &Items{}, - }, - { - Message: &reference, - Target: &Reference{}, - }, - { - Message: &queryError, - Target: &QueryError{}, - }, - { - Message: &response, - Target: &Response{}, - }, -} - -// TestDecode Make sure that we can decode all of the message -func TestDecode(t *testing.T) { - for _, decTest := range decodeTests { - // Marshal to binary - b, err := proto.Marshal(decTest.Message) - - if err != nil { - t.Fatal(err) - } - - err = Unmarshal(context.Background(), b, decTest.Target) - - if err != nil { - t.Error(err) - } - } -} diff --git a/sdp-go/errors.go b/sdp-go/errors.go deleted file mode 100644 index 4bfe3a53..00000000 --- a/sdp-go/errors.go +++ /dev/null @@ -1,54 +0,0 @@ -package sdp - -import ( - "errors" - "fmt" - - "github.com/google/uuid" -) - -const ErrorTemplate string = `%v - -ErrorType: %v -Scope: %v -SourceName: %v -ItemType: %v -ResponderName: %v` - -// assert interface -var _ error = (*QueryError)(nil) - -func (e *QueryError) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(e.GetUUID()) - if err != nil { - return nil - } - return &u -} - -// Ensure that the QueryError is seen as a valid error in golang -func (e *QueryError) Error() string { - return fmt.Sprintf( - ErrorTemplate, - e.GetErrorString(), - e.GetErrorType().String(), - e.GetScope(), - e.GetSourceName(), - e.GetItemType(), - e.GetResponderName(), - ) -} - -// NewQueryError converts a regular error to a QueryError of type -// OTHER. If the input error is already a QueryError then it is preserved -func NewQueryError(err error) *QueryError { - var sdpErr *QueryError - if errors.As(err, &sdpErr) { - return sdpErr - } - - return &QueryError{ - ErrorType: QueryError_OTHER, - ErrorString: err.Error(), - } -} diff --git a/sdp-go/gateway.go b/sdp-go/gateway.go deleted file mode 100644 index 4bd5abf8..00000000 --- a/sdp-go/gateway.go +++ /dev/null @@ -1,190 +0,0 @@ -package sdp - -import ( - "encoding/hex" - - "github.com/google/uuid" -) - -// Equal Returns whether two statuses are functionally equal -func (x *GatewayRequestStatus) Equal(y *GatewayRequestStatus) bool { - if x == nil { - if y == nil { - return true - } else { - return false - } - } - - if (x.GetSummary() == nil || y.GetSummary() == nil) && x.GetSummary() != y.GetSummary() { - // If one of them is nil, and they aren't both nil - return false - } - - if x.GetSummary() != nil && y.GetSummary() != nil { - if x.GetSummary().GetWorking() != y.GetSummary().GetWorking() { - return false - } - if x.GetSummary().GetStalled() != y.GetSummary().GetStalled() { - return false - } - if x.GetSummary().GetComplete() != y.GetSummary().GetComplete() { - return false - } - if x.GetSummary().GetError() != y.GetSummary().GetError() { - return false - } - if x.GetSummary().GetCancelled() != y.GetSummary().GetCancelled() { - return false - } - if x.GetSummary().GetResponders() != y.GetSummary().GetResponders() { - return false - } - } - - if x.GetPostProcessingComplete() != y.GetPostProcessingComplete() { - return false - } - - return true -} - -// Whether the gateway request is complete -func (x *GatewayRequestStatus) Done() bool { - return x.GetPostProcessingComplete() && x.GetSummary().GetWorking() == 0 -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *StoreBookmark) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err != nil { - return "" - } - return u.String() - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *BookmarkStoreResult) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *LoadBookmark) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *BookmarkLoadResult) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *StoreSnapshot) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *SnapshotStoreResult) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *LoadSnapshot) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *SnapshotLoadResult) GetMsgIDLogString() string { - bs := x.GetMsgID() - if len(bs) == 0 { - return "" - } - if len(bs) == 16 { - u, err := uuid.FromBytes(bs) - if err == nil { - return u.String() - } - } - return hex.EncodeToString(bs) -} - -// GetMsgIDLogString returns the correlation ID as string for logging -func (x *QueryStatus) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *LoadSnapshot) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetUUID()) - if err != nil { - return nil - } - return &u -} diff --git a/sdp-go/gateway.pb.go b/sdp-go/gateway.pb.go deleted file mode 100644 index 66435b0f..00000000 --- a/sdp-go/gateway.pb.go +++ /dev/null @@ -1,2336 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: gateway.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// A union of all request made to the gateway. -type GatewayRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to RequestType: - // - // *GatewayRequest_Query - // *GatewayRequest_CancelQuery - // *GatewayRequest_Expand - // *GatewayRequest_StoreSnapshot - // *GatewayRequest_LoadSnapshot - // *GatewayRequest_StoreBookmark - // *GatewayRequest_LoadBookmark - // *GatewayRequest_ChatMessage - RequestType isGatewayRequest_RequestType `protobuf_oneof:"request_type"` - MinStatusInterval *durationpb.Duration `protobuf:"bytes,2,opt,name=minStatusInterval,proto3,oneof" json:"minStatusInterval,omitempty"` // Minimum time between status updates. Setting this value too low can result in too many status messages - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GatewayRequest) Reset() { - *x = GatewayRequest{} - mi := &file_gateway_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GatewayRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GatewayRequest) ProtoMessage() {} - -func (x *GatewayRequest) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[0] - 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 GatewayRequest.ProtoReflect.Descriptor instead. -func (*GatewayRequest) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{0} -} - -func (x *GatewayRequest) GetRequestType() isGatewayRequest_RequestType { - if x != nil { - return x.RequestType - } - return nil -} - -func (x *GatewayRequest) GetQuery() *Query { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_Query); ok { - return x.Query - } - } - return nil -} - -func (x *GatewayRequest) GetCancelQuery() *CancelQuery { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_CancelQuery); ok { - return x.CancelQuery - } - } - return nil -} - -func (x *GatewayRequest) GetExpand() *Expand { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_Expand); ok { - return x.Expand - } - } - return nil -} - -func (x *GatewayRequest) GetStoreSnapshot() *StoreSnapshot { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_StoreSnapshot); ok { - return x.StoreSnapshot - } - } - return nil -} - -func (x *GatewayRequest) GetLoadSnapshot() *LoadSnapshot { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_LoadSnapshot); ok { - return x.LoadSnapshot - } - } - return nil -} - -func (x *GatewayRequest) GetStoreBookmark() *StoreBookmark { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_StoreBookmark); ok { - return x.StoreBookmark - } - } - return nil -} - -func (x *GatewayRequest) GetLoadBookmark() *LoadBookmark { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_LoadBookmark); ok { - return x.LoadBookmark - } - } - return nil -} - -func (x *GatewayRequest) GetChatMessage() *ChatMessage { - if x != nil { - if x, ok := x.RequestType.(*GatewayRequest_ChatMessage); ok { - return x.ChatMessage - } - } - return nil -} - -func (x *GatewayRequest) GetMinStatusInterval() *durationpb.Duration { - if x != nil { - return x.MinStatusInterval - } - return nil -} - -type isGatewayRequest_RequestType interface { - isGatewayRequest_RequestType() -} - -type GatewayRequest_Query struct { - // Adds a new query for items to the session, starting it immediately - Query *Query `protobuf:"bytes,1,opt,name=query,proto3,oneof"` -} - -type GatewayRequest_CancelQuery struct { - // Cancel a running query - CancelQuery *CancelQuery `protobuf:"bytes,3,opt,name=cancelQuery,proto3,oneof"` -} - -type GatewayRequest_Expand struct { - // Expand all linked items for the given item - Expand *Expand `protobuf:"bytes,7,opt,name=expand,proto3,oneof"` -} - -type GatewayRequest_StoreSnapshot struct { - // store the current session state as snapshot - StoreSnapshot *StoreSnapshot `protobuf:"bytes,10,opt,name=storeSnapshot,proto3,oneof"` -} - -type GatewayRequest_LoadSnapshot struct { - // load a snapshot into the current state - LoadSnapshot *LoadSnapshot `protobuf:"bytes,11,opt,name=loadSnapshot,proto3,oneof"` -} - -type GatewayRequest_StoreBookmark struct { - // store the current set of queries as bookmarks - StoreBookmark *StoreBookmark `protobuf:"bytes,14,opt,name=storeBookmark,proto3,oneof"` -} - -type GatewayRequest_LoadBookmark struct { - // load and execute a bookmark into the current state - LoadBookmark *LoadBookmark `protobuf:"bytes,15,opt,name=loadBookmark,proto3,oneof"` -} - -type GatewayRequest_ChatMessage struct { - // // cancel the loading of a Bookmark - // CancelLoadBookmark cancelLoadBookmark = ??; - // // undo the loading of a Bookmark - // UndoLoadBookmark undoLoadBookmark = ??; - ChatMessage *ChatMessage `protobuf:"bytes,16,opt,name=chatMessage,proto3,oneof"` -} - -func (*GatewayRequest_Query) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_CancelQuery) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_Expand) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_StoreSnapshot) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_LoadSnapshot) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_StoreBookmark) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_LoadBookmark) isGatewayRequest_RequestType() {} - -func (*GatewayRequest_ChatMessage) isGatewayRequest_RequestType() {} - -// The gateway will always respond with this type of message, -// however the purpose of it is purely as a wrapper to the many different types -// of messages that the gateway can send -type GatewayResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to ResponseType: - // - // *GatewayResponse_NewItem - // *GatewayResponse_NewEdge - // *GatewayResponse_Status - // *GatewayResponse_Error - // *GatewayResponse_QueryError - // *GatewayResponse_DeleteItem - // *GatewayResponse_DeleteEdge - // *GatewayResponse_UpdateItem - // *GatewayResponse_SnapshotStoreResult - // *GatewayResponse_SnapshotLoadResult - // *GatewayResponse_BookmarkStoreResult - // *GatewayResponse_BookmarkLoadResult - // *GatewayResponse_QueryStatus - // *GatewayResponse_ChatResponse - // *GatewayResponse_ToolStart - // *GatewayResponse_ToolFinish - ResponseType isGatewayResponse_ResponseType `protobuf_oneof:"response_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GatewayResponse) Reset() { - *x = GatewayResponse{} - mi := &file_gateway_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GatewayResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GatewayResponse) ProtoMessage() {} - -func (x *GatewayResponse) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[1] - 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 GatewayResponse.ProtoReflect.Descriptor instead. -func (*GatewayResponse) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{1} -} - -func (x *GatewayResponse) GetResponseType() isGatewayResponse_ResponseType { - if x != nil { - return x.ResponseType - } - return nil -} - -func (x *GatewayResponse) GetNewItem() *Item { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_NewItem); ok { - return x.NewItem - } - } - return nil -} - -func (x *GatewayResponse) GetNewEdge() *Edge { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_NewEdge); ok { - return x.NewEdge - } - } - return nil -} - -func (x *GatewayResponse) GetStatus() *GatewayRequestStatus { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_Status); ok { - return x.Status - } - } - return nil -} - -func (x *GatewayResponse) GetError() string { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_Error); ok { - return x.Error - } - } - return "" -} - -func (x *GatewayResponse) GetQueryError() *QueryError { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_QueryError); ok { - return x.QueryError - } - } - return nil -} - -func (x *GatewayResponse) GetDeleteItem() *Reference { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_DeleteItem); ok { - return x.DeleteItem - } - } - return nil -} - -func (x *GatewayResponse) GetDeleteEdge() *Edge { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_DeleteEdge); ok { - return x.DeleteEdge - } - } - return nil -} - -func (x *GatewayResponse) GetUpdateItem() *Item { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_UpdateItem); ok { - return x.UpdateItem - } - } - return nil -} - -func (x *GatewayResponse) GetSnapshotStoreResult() *SnapshotStoreResult { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_SnapshotStoreResult); ok { - return x.SnapshotStoreResult - } - } - return nil -} - -func (x *GatewayResponse) GetSnapshotLoadResult() *SnapshotLoadResult { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_SnapshotLoadResult); ok { - return x.SnapshotLoadResult - } - } - return nil -} - -func (x *GatewayResponse) GetBookmarkStoreResult() *BookmarkStoreResult { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_BookmarkStoreResult); ok { - return x.BookmarkStoreResult - } - } - return nil -} - -func (x *GatewayResponse) GetBookmarkLoadResult() *BookmarkLoadResult { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_BookmarkLoadResult); ok { - return x.BookmarkLoadResult - } - } - return nil -} - -func (x *GatewayResponse) GetQueryStatus() *QueryStatus { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_QueryStatus); ok { - return x.QueryStatus - } - } - return nil -} - -func (x *GatewayResponse) GetChatResponse() *ChatResponse { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_ChatResponse); ok { - return x.ChatResponse - } - } - return nil -} - -func (x *GatewayResponse) GetToolStart() *ToolStart { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_ToolStart); ok { - return x.ToolStart - } - } - return nil -} - -func (x *GatewayResponse) GetToolFinish() *ToolFinish { - if x != nil { - if x, ok := x.ResponseType.(*GatewayResponse_ToolFinish); ok { - return x.ToolFinish - } - } - return nil -} - -type isGatewayResponse_ResponseType interface { - isGatewayResponse_ResponseType() -} - -type GatewayResponse_NewItem struct { - NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered -} - -type GatewayResponse_NewEdge struct { - NewEdge *Edge `protobuf:"bytes,3,opt,name=newEdge,proto3,oneof"` // A new edge between two items -} - -type GatewayResponse_Status struct { - Status *GatewayRequestStatus `protobuf:"bytes,4,opt,name=status,proto3,oneof"` // Status of the overall request -} - -type GatewayResponse_Error struct { - Error string `protobuf:"bytes,5,opt,name=error,proto3,oneof"` // An error that means the request couldn't be executed -} - -type GatewayResponse_QueryError struct { - QueryError *QueryError `protobuf:"bytes,6,opt,name=queryError,proto3,oneof"` // A new error that was encountered as part of a query -} - -type GatewayResponse_DeleteItem struct { - DeleteItem *Reference `protobuf:"bytes,7,opt,name=deleteItem,proto3,oneof"` // An item that should be deleted from local state -} - -type GatewayResponse_DeleteEdge struct { - DeleteEdge *Edge `protobuf:"bytes,8,opt,name=deleteEdge,proto3,oneof"` // An edge that should be deleted form local state -} - -type GatewayResponse_UpdateItem struct { - UpdateItem *Item `protobuf:"bytes,9,opt,name=updateItem,proto3,oneof"` // An item that has already been sent, but contains new data, it should be updated to reflect this version -} - -type GatewayResponse_SnapshotStoreResult struct { - SnapshotStoreResult *SnapshotStoreResult `protobuf:"bytes,11,opt,name=snapshotStoreResult,proto3,oneof"` -} - -type GatewayResponse_SnapshotLoadResult struct { - SnapshotLoadResult *SnapshotLoadResult `protobuf:"bytes,12,opt,name=snapshotLoadResult,proto3,oneof"` -} - -type GatewayResponse_BookmarkStoreResult struct { - BookmarkStoreResult *BookmarkStoreResult `protobuf:"bytes,15,opt,name=bookmarkStoreResult,proto3,oneof"` -} - -type GatewayResponse_BookmarkLoadResult struct { - BookmarkLoadResult *BookmarkLoadResult `protobuf:"bytes,16,opt,name=bookmarkLoadResult,proto3,oneof"` -} - -type GatewayResponse_QueryStatus struct { - QueryStatus *QueryStatus `protobuf:"bytes,17,opt,name=queryStatus,proto3,oneof"` // Status of requested queries -} - -type GatewayResponse_ChatResponse struct { - ChatResponse *ChatResponse `protobuf:"bytes,18,opt,name=chatResponse,proto3,oneof"` -} - -type GatewayResponse_ToolStart struct { - ToolStart *ToolStart `protobuf:"bytes,19,opt,name=toolStart,proto3,oneof"` -} - -type GatewayResponse_ToolFinish struct { - ToolFinish *ToolFinish `protobuf:"bytes,20,opt,name=toolFinish,proto3,oneof"` -} - -func (*GatewayResponse_NewItem) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_NewEdge) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_Status) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_Error) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_QueryError) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_DeleteItem) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_DeleteEdge) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_UpdateItem) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_SnapshotStoreResult) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_SnapshotLoadResult) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_BookmarkStoreResult) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_BookmarkLoadResult) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_QueryStatus) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_ChatResponse) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_ToolStart) isGatewayResponse_ResponseType() {} - -func (*GatewayResponse_ToolFinish) isGatewayResponse_ResponseType() {} - -// Contains the status of the gateway request. -type GatewayRequestStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - Summary *GatewayRequestStatus_Summary `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` - // Whether all items have finished being processed by the gateway. It is - // possible for all responders to be complete, but the gateway is still - // working. A request should only be considered complete when all working == - // 0 and postProcessingComplete == true - PostProcessingComplete bool `protobuf:"varint,4,opt,name=postProcessingComplete,proto3" json:"postProcessingComplete,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GatewayRequestStatus) Reset() { - *x = GatewayRequestStatus{} - mi := &file_gateway_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GatewayRequestStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GatewayRequestStatus) ProtoMessage() {} - -func (x *GatewayRequestStatus) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[2] - 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 GatewayRequestStatus.ProtoReflect.Descriptor instead. -func (*GatewayRequestStatus) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{2} -} - -func (x *GatewayRequestStatus) GetSummary() *GatewayRequestStatus_Summary { - if x != nil { - return x.Summary - } - return nil -} - -func (x *GatewayRequestStatus) GetPostProcessingComplete() bool { - if x != nil { - return x.PostProcessingComplete - } - return false -} - -// Ask the gateway to store the current state as bookmark with the specified details. -// Returns a BookmarkStored message when the bookmark is stored -type StoreBookmark struct { - state protoimpl.MessageState `protogen:"open.v1"` - // user supplied name of this bookmark - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // user supplied description of this bookmark - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - // a correlation ID to match up requests and responses. set this to a value unique per connection - MsgID []byte `protobuf:"bytes,3,opt,name=msgID,proto3" json:"msgID,omitempty"` - // whether this bookmark should be stored as a system bookmark. System - // bookmarks are hidden and can only be returned via the UUID, they don't - // show up in lists - IsSystem bool `protobuf:"varint,4,opt,name=isSystem,proto3" json:"isSystem,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StoreBookmark) Reset() { - *x = StoreBookmark{} - mi := &file_gateway_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StoreBookmark) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StoreBookmark) ProtoMessage() {} - -func (x *StoreBookmark) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[3] - 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 StoreBookmark.ProtoReflect.Descriptor instead. -func (*StoreBookmark) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{3} -} - -func (x *StoreBookmark) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *StoreBookmark) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *StoreBookmark) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -func (x *StoreBookmark) GetIsSystem() bool { - if x != nil { - return x.IsSystem - } - return false -} - -// After a bookmark is successfully stored, this reply with the new bookmark's details is sent. -type BookmarkStoreResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` - // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID - MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` - // UUID of the newly created bookmark - BookmarkID []byte `protobuf:"bytes,5,opt,name=bookmarkID,proto3" json:"bookmarkID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BookmarkStoreResult) Reset() { - *x = BookmarkStoreResult{} - mi := &file_gateway_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BookmarkStoreResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BookmarkStoreResult) ProtoMessage() {} - -func (x *BookmarkStoreResult) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[4] - 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 BookmarkStoreResult.ProtoReflect.Descriptor instead. -func (*BookmarkStoreResult) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{4} -} - -func (x *BookmarkStoreResult) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *BookmarkStoreResult) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -func (x *BookmarkStoreResult) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -func (x *BookmarkStoreResult) GetBookmarkID() []byte { - if x != nil { - return x.BookmarkID - } - return nil -} - -// Ask the gateway to load the specified bookmark into the current state. -// Results are streamed to the client in the same way query results are. -type LoadBookmark struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id of the bookmark to load - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // a correlation ID to match up requests and responses. set this to a value unique per connection - MsgID []byte `protobuf:"bytes,2,opt,name=msgID,proto3" json:"msgID,omitempty"` - // set to true to force fetching fresh data - IgnoreCache bool `protobuf:"varint,3,opt,name=ignoreCache,proto3" json:"ignoreCache,omitempty"` - // The time at which the gateway should stop processing the queries spawned by this request - Deadline *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deadline,proto3" json:"deadline,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LoadBookmark) Reset() { - *x = LoadBookmark{} - mi := &file_gateway_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LoadBookmark) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LoadBookmark) ProtoMessage() {} - -func (x *LoadBookmark) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[5] - 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 LoadBookmark.ProtoReflect.Descriptor instead. -func (*LoadBookmark) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{5} -} - -func (x *LoadBookmark) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *LoadBookmark) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -func (x *LoadBookmark) GetIgnoreCache() bool { - if x != nil { - return x.IgnoreCache - } - return false -} - -func (x *LoadBookmark) GetDeadline() *timestamppb.Timestamp { - if x != nil { - return x.Deadline - } - return nil -} - -type BookmarkLoadResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` - // UUIDs of all queries that have been started as a result of loading this bookmark - StartedQueryUUIDs [][]byte `protobuf:"bytes,3,rep,name=startedQueryUUIDs,proto3" json:"startedQueryUUIDs,omitempty"` - // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID - MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BookmarkLoadResult) Reset() { - *x = BookmarkLoadResult{} - mi := &file_gateway_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BookmarkLoadResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BookmarkLoadResult) ProtoMessage() {} - -func (x *BookmarkLoadResult) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[6] - 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 BookmarkLoadResult.ProtoReflect.Descriptor instead. -func (*BookmarkLoadResult) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{6} -} - -func (x *BookmarkLoadResult) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *BookmarkLoadResult) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -func (x *BookmarkLoadResult) GetStartedQueryUUIDs() [][]byte { - if x != nil { - return x.StartedQueryUUIDs - } - return nil -} - -func (x *BookmarkLoadResult) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -// Ask the gateway to store the current state as snapshot with the specified details. -// Returns a SnapshotStored message when the snapshot is stored -type StoreSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - // user supplied name of this snapshot - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // user supplied description of this snapshot - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - // a correlation ID to match up requests and responses. set this to a value unique per connection - MsgID []byte `protobuf:"bytes,3,opt,name=msgID,proto3" json:"msgID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *StoreSnapshot) Reset() { - *x = StoreSnapshot{} - mi := &file_gateway_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *StoreSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StoreSnapshot) ProtoMessage() {} - -func (x *StoreSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[7] - 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 StoreSnapshot.ProtoReflect.Descriptor instead. -func (*StoreSnapshot) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{7} -} - -func (x *StoreSnapshot) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *StoreSnapshot) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *StoreSnapshot) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -// After a snapshot is successfully stored, this reply with the new snapshot's details is sent. -type SnapshotStoreResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` - // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID - MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` - SnapshotID []byte `protobuf:"bytes,5,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` // The UUID of the newly stored snapshot - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SnapshotStoreResult) Reset() { - *x = SnapshotStoreResult{} - mi := &file_gateway_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SnapshotStoreResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SnapshotStoreResult) ProtoMessage() {} - -func (x *SnapshotStoreResult) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[8] - 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 SnapshotStoreResult.ProtoReflect.Descriptor instead. -func (*SnapshotStoreResult) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{8} -} - -func (x *SnapshotStoreResult) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *SnapshotStoreResult) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -func (x *SnapshotStoreResult) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -func (x *SnapshotStoreResult) GetSnapshotID() []byte { - if x != nil { - return x.SnapshotID - } - return nil -} - -// Ask the gateway to load the specified snapshot into the current state. -// Results are streamed to the client in the same way query results are. -type LoadSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id of the snapshot to load - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // a correlation ID to match up requests and responses. set this to a value unique per connection - MsgID []byte `protobuf:"bytes,2,opt,name=msgID,proto3" json:"msgID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LoadSnapshot) Reset() { - *x = LoadSnapshot{} - mi := &file_gateway_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LoadSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LoadSnapshot) ProtoMessage() {} - -func (x *LoadSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[9] - 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 LoadSnapshot.ProtoReflect.Descriptor instead. -func (*LoadSnapshot) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{9} -} - -func (x *LoadSnapshot) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *LoadSnapshot) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -type SnapshotLoadResult struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` - // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID - MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SnapshotLoadResult) Reset() { - *x = SnapshotLoadResult{} - mi := &file_gateway_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SnapshotLoadResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SnapshotLoadResult) ProtoMessage() {} - -func (x *SnapshotLoadResult) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[10] - 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 SnapshotLoadResult.ProtoReflect.Descriptor instead. -func (*SnapshotLoadResult) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{10} -} - -func (x *SnapshotLoadResult) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -func (x *SnapshotLoadResult) GetErrorMessage() string { - if x != nil { - return x.ErrorMessage - } - return "" -} - -func (x *SnapshotLoadResult) GetMsgID() []byte { - if x != nil { - return x.MsgID - } - return nil -} - -type ChatMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The message to create - // - // Types that are valid to be assigned to RequestType: - // - // *ChatMessage_Text - // *ChatMessage_Cancel - RequestType isChatMessage_RequestType `protobuf_oneof:"request_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatMessage) Reset() { - *x = ChatMessage{} - mi := &file_gateway_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatMessage) ProtoMessage() {} - -func (x *ChatMessage) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[11] - 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 ChatMessage.ProtoReflect.Descriptor instead. -func (*ChatMessage) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{11} -} - -func (x *ChatMessage) GetRequestType() isChatMessage_RequestType { - if x != nil { - return x.RequestType - } - return nil -} - -func (x *ChatMessage) GetText() string { - if x != nil { - if x, ok := x.RequestType.(*ChatMessage_Text); ok { - return x.Text - } - } - return "" -} - -func (x *ChatMessage) GetCancel() bool { - if x != nil { - if x, ok := x.RequestType.(*ChatMessage_Cancel); ok { - return x.Cancel - } - } - return false -} - -type isChatMessage_RequestType interface { - isChatMessage_RequestType() -} - -type ChatMessage_Text struct { - Text string `protobuf:"bytes,1,opt,name=text,proto3,oneof"` -} - -type ChatMessage_Cancel struct { - // Cancel the last message sent to openAI, includes the message and tools that were started - Cancel bool `protobuf:"varint,2,opt,name=cancel,proto3,oneof"` -} - -func (*ChatMessage_Text) isChatMessage_RequestType() {} - -func (*ChatMessage_Cancel) isChatMessage_RequestType() {} - -type ToolMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A unique ID that tracks this tool call and can be used to correlate messages - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ToolMetadata) Reset() { - *x = ToolMetadata{} - mi := &file_gateway_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ToolMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ToolMetadata) ProtoMessage() {} - -func (x *ToolMetadata) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[12] - 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 ToolMetadata.ProtoReflect.Descriptor instead. -func (*ToolMetadata) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{12} -} - -func (x *ToolMetadata) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type QueryToolStart struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - Method QueryMethod `protobuf:"varint,2,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` - Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` - Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *QueryToolStart) Reset() { - *x = QueryToolStart{} - mi := &file_gateway_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *QueryToolStart) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryToolStart) ProtoMessage() {} - -func (x *QueryToolStart) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use QueryToolStart.ProtoReflect.Descriptor instead. -func (*QueryToolStart) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{13} -} - -func (x *QueryToolStart) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *QueryToolStart) GetMethod() QueryMethod { - if x != nil { - return x.Method - } - return QueryMethod_GET -} - -func (x *QueryToolStart) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *QueryToolStart) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -type QueryToolFinish struct { - state protoimpl.MessageState `protogen:"open.v1"` - NumItems int32 `protobuf:"varint,1,opt,name=numItems,proto3" json:"numItems,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *QueryToolFinish) Reset() { - *x = QueryToolFinish{} - mi := &file_gateway_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *QueryToolFinish) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryToolFinish) ProtoMessage() {} - -func (x *QueryToolFinish) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use QueryToolFinish.ProtoReflect.Descriptor instead. -func (*QueryToolFinish) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{14} -} - -func (x *QueryToolFinish) GetNumItems() int32 { - if x != nil { - return x.NumItems - } - return 0 -} - -type RelationshipToolStart struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` - Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RelationshipToolStart) Reset() { - *x = RelationshipToolStart{} - mi := &file_gateway_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RelationshipToolStart) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RelationshipToolStart) ProtoMessage() {} - -func (x *RelationshipToolStart) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[15] - 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 RelationshipToolStart.ProtoReflect.Descriptor instead. -func (*RelationshipToolStart) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{15} -} - -func (x *RelationshipToolStart) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *RelationshipToolStart) GetUniqueAttributeValue() string { - if x != nil { - return x.UniqueAttributeValue - } - return "" -} - -func (x *RelationshipToolStart) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -type RelationshipToolFinish struct { - state protoimpl.MessageState `protogen:"open.v1"` - NumItems int32 `protobuf:"varint,1,opt,name=numItems,proto3" json:"numItems,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RelationshipToolFinish) Reset() { - *x = RelationshipToolFinish{} - mi := &file_gateway_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RelationshipToolFinish) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RelationshipToolFinish) ProtoMessage() {} - -func (x *RelationshipToolFinish) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[16] - 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 RelationshipToolFinish.ProtoReflect.Descriptor instead. -func (*RelationshipToolFinish) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{16} -} - -func (x *RelationshipToolFinish) GetNumItems() int32 { - if x != nil { - return x.NumItems - } - return 0 -} - -type ChangesByReferenceToolStart struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` - Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangesByReferenceToolStart) Reset() { - *x = ChangesByReferenceToolStart{} - mi := &file_gateway_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangesByReferenceToolStart) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangesByReferenceToolStart) ProtoMessage() {} - -func (x *ChangesByReferenceToolStart) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChangesByReferenceToolStart.ProtoReflect.Descriptor instead. -func (*ChangesByReferenceToolStart) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{17} -} - -func (x *ChangesByReferenceToolStart) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *ChangesByReferenceToolStart) GetUniqueAttributeValue() string { - if x != nil { - return x.UniqueAttributeValue - } - return "" -} - -func (x *ChangesByReferenceToolStart) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -type ChangeByReferenceSummary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` // from ChangeProperties - UUID []byte `protobuf:"bytes,2,opt,name=UUID,proto3" json:"UUID,omitempty"` // from ChangeMetadata - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=createdAt,proto3" json:"createdAt,omitempty"` // From ChangeMetadata - Owner string `protobuf:"bytes,4,opt,name=owner,proto3" json:"owner,omitempty"` // From ChangeProperties - NumAffectedItems int32 `protobuf:"varint,5,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` // From ChangeMetadata - ChangeStatus ChangeStatus `protobuf:"varint,6,opt,name=changeStatus,proto3,enum=changes.ChangeStatus" json:"changeStatus,omitempty"` // From ChangeMetadata - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangeByReferenceSummary) Reset() { - *x = ChangeByReferenceSummary{} - mi := &file_gateway_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangeByReferenceSummary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangeByReferenceSummary) ProtoMessage() {} - -func (x *ChangeByReferenceSummary) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ChangeByReferenceSummary.ProtoReflect.Descriptor instead. -func (*ChangeByReferenceSummary) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{18} -} - -func (x *ChangeByReferenceSummary) GetTitle() string { - if x != nil { - return x.Title - } - return "" -} - -func (x *ChangeByReferenceSummary) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *ChangeByReferenceSummary) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *ChangeByReferenceSummary) GetOwner() string { - if x != nil { - return x.Owner - } - return "" -} - -func (x *ChangeByReferenceSummary) GetNumAffectedItems() int32 { - if x != nil { - return x.NumAffectedItems - } - return 0 -} - -func (x *ChangeByReferenceSummary) GetChangeStatus() ChangeStatus { - if x != nil { - return x.ChangeStatus - } - return ChangeStatus_CHANGE_STATUS_UNSPECIFIED -} - -type ChangesByReferenceToolFinish struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeSummaries []*ChangeByReferenceSummary `protobuf:"bytes,1,rep,name=changeSummaries,proto3" json:"changeSummaries,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChangesByReferenceToolFinish) Reset() { - *x = ChangesByReferenceToolFinish{} - mi := &file_gateway_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChangesByReferenceToolFinish) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChangesByReferenceToolFinish) ProtoMessage() {} - -func (x *ChangesByReferenceToolFinish) ProtoReflect() protoreflect.Message { - mi := &file_gateway_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 ChangesByReferenceToolFinish.ProtoReflect.Descriptor instead. -func (*ChangesByReferenceToolFinish) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{19} -} - -func (x *ChangesByReferenceToolFinish) GetChangeSummaries() []*ChangeByReferenceSummary { - if x != nil { - return x.ChangeSummaries - } - return nil -} - -type ToolStart struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *ToolMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // Types that are valid to be assigned to ToolType: - // - // *ToolStart_Query - // *ToolStart_Relationship - // *ToolStart_ChangesByReference - ToolType isToolStart_ToolType `protobuf_oneof:"tool_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ToolStart) Reset() { - *x = ToolStart{} - mi := &file_gateway_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ToolStart) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ToolStart) ProtoMessage() {} - -func (x *ToolStart) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[20] - 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 ToolStart.ProtoReflect.Descriptor instead. -func (*ToolStart) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{20} -} - -func (x *ToolStart) GetMetadata() *ToolMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *ToolStart) GetToolType() isToolStart_ToolType { - if x != nil { - return x.ToolType - } - return nil -} - -func (x *ToolStart) GetQuery() *QueryToolStart { - if x != nil { - if x, ok := x.ToolType.(*ToolStart_Query); ok { - return x.Query - } - } - return nil -} - -func (x *ToolStart) GetRelationship() *RelationshipToolStart { - if x != nil { - if x, ok := x.ToolType.(*ToolStart_Relationship); ok { - return x.Relationship - } - } - return nil -} - -func (x *ToolStart) GetChangesByReference() *ChangesByReferenceToolStart { - if x != nil { - if x, ok := x.ToolType.(*ToolStart_ChangesByReference); ok { - return x.ChangesByReference - } - } - return nil -} - -type isToolStart_ToolType interface { - isToolStart_ToolType() -} - -type ToolStart_Query struct { - Query *QueryToolStart `protobuf:"bytes,2,opt,name=query,proto3,oneof"` -} - -type ToolStart_Relationship struct { - Relationship *RelationshipToolStart `protobuf:"bytes,3,opt,name=relationship,proto3,oneof"` -} - -type ToolStart_ChangesByReference struct { - ChangesByReference *ChangesByReferenceToolStart `protobuf:"bytes,4,opt,name=changesByReference,proto3,oneof"` -} - -func (*ToolStart_Query) isToolStart_ToolType() {} - -func (*ToolStart_Relationship) isToolStart_ToolType() {} - -func (*ToolStart_ChangesByReference) isToolStart_ToolType() {} - -type ToolFinish struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *ToolMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` - // Types that are valid to be assigned to ToolType: - // - // *ToolFinish_Query - // *ToolFinish_Relationship - // *ToolFinish_ChangesByReference - ToolType isToolFinish_ToolType `protobuf_oneof:"tool_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ToolFinish) Reset() { - *x = ToolFinish{} - mi := &file_gateway_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ToolFinish) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ToolFinish) ProtoMessage() {} - -func (x *ToolFinish) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[21] - 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 ToolFinish.ProtoReflect.Descriptor instead. -func (*ToolFinish) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{21} -} - -func (x *ToolFinish) GetMetadata() *ToolMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *ToolFinish) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -func (x *ToolFinish) GetToolType() isToolFinish_ToolType { - if x != nil { - return x.ToolType - } - return nil -} - -func (x *ToolFinish) GetQuery() *QueryToolFinish { - if x != nil { - if x, ok := x.ToolType.(*ToolFinish_Query); ok { - return x.Query - } - } - return nil -} - -func (x *ToolFinish) GetRelationship() *RelationshipToolFinish { - if x != nil { - if x, ok := x.ToolType.(*ToolFinish_Relationship); ok { - return x.Relationship - } - } - return nil -} - -func (x *ToolFinish) GetChangesByReference() *ChangesByReferenceToolFinish { - if x != nil { - if x, ok := x.ToolType.(*ToolFinish_ChangesByReference); ok { - return x.ChangesByReference - } - } - return nil -} - -type isToolFinish_ToolType interface { - isToolFinish_ToolType() -} - -type ToolFinish_Query struct { - Query *QueryToolFinish `protobuf:"bytes,3,opt,name=query,proto3,oneof"` -} - -type ToolFinish_Relationship struct { - Relationship *RelationshipToolFinish `protobuf:"bytes,4,opt,name=relationship,proto3,oneof"` -} - -type ToolFinish_ChangesByReference struct { - ChangesByReference *ChangesByReferenceToolFinish `protobuf:"bytes,5,opt,name=changesByReference,proto3,oneof"` -} - -func (*ToolFinish_Query) isToolFinish_ToolType() {} - -func (*ToolFinish_Relationship) isToolFinish_ToolType() {} - -func (*ToolFinish_ChangesByReference) isToolFinish_ToolType() {} - -type ChatResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` - Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ChatResponse) Reset() { - *x = ChatResponse{} - mi := &file_gateway_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ChatResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ChatResponse) ProtoMessage() {} - -func (x *ChatResponse) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[22] - 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 ChatResponse.ProtoReflect.Descriptor instead. -func (*ChatResponse) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{22} -} - -func (x *ChatResponse) GetText() string { - if x != nil { - return x.Text - } - return "" -} - -func (x *ChatResponse) GetError() string { - if x != nil { - return x.Error - } - return "" -} - -type GatewayRequestStatus_Summary struct { - state protoimpl.MessageState `protogen:"open.v1"` - Working int32 `protobuf:"varint,1,opt,name=working,proto3" json:"working,omitempty"` - Stalled int32 `protobuf:"varint,2,opt,name=stalled,proto3" json:"stalled,omitempty"` - Complete int32 `protobuf:"varint,3,opt,name=complete,proto3" json:"complete,omitempty"` - Error int32 `protobuf:"varint,4,opt,name=error,proto3" json:"error,omitempty"` - Cancelled int32 `protobuf:"varint,5,opt,name=cancelled,proto3" json:"cancelled,omitempty"` - Responders int32 `protobuf:"varint,6,opt,name=responders,proto3" json:"responders,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GatewayRequestStatus_Summary) Reset() { - *x = GatewayRequestStatus_Summary{} - mi := &file_gateway_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GatewayRequestStatus_Summary) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GatewayRequestStatus_Summary) ProtoMessage() {} - -func (x *GatewayRequestStatus_Summary) ProtoReflect() protoreflect.Message { - mi := &file_gateway_proto_msgTypes[23] - 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 GatewayRequestStatus_Summary.ProtoReflect.Descriptor instead. -func (*GatewayRequestStatus_Summary) Descriptor() ([]byte, []int) { - return file_gateway_proto_rawDescGZIP(), []int{2, 0} -} - -func (x *GatewayRequestStatus_Summary) GetWorking() int32 { - if x != nil { - return x.Working - } - return 0 -} - -func (x *GatewayRequestStatus_Summary) GetStalled() int32 { - if x != nil { - return x.Stalled - } - return 0 -} - -func (x *GatewayRequestStatus_Summary) GetComplete() int32 { - if x != nil { - return x.Complete - } - return 0 -} - -func (x *GatewayRequestStatus_Summary) GetError() int32 { - if x != nil { - return x.Error - } - return 0 -} - -func (x *GatewayRequestStatus_Summary) GetCancelled() int32 { - if x != nil { - return x.Cancelled - } - return 0 -} - -func (x *GatewayRequestStatus_Summary) GetResponders() int32 { - if x != nil { - return x.Responders - } - return 0 -} - -var File_gateway_proto protoreflect.FileDescriptor - -const file_gateway_proto_rawDesc = "" + - "\n" + - "\rgateway.proto\x12\agateway\x1a\rchanges.proto\x1a\vitems.proto\x1a\x0fresponses.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xad\x04\n" + - "\x0eGatewayRequest\x12\x1e\n" + - "\x05query\x18\x01 \x01(\v2\x06.QueryH\x00R\x05query\x120\n" + - "\vcancelQuery\x18\x03 \x01(\v2\f.CancelQueryH\x00R\vcancelQuery\x12!\n" + - "\x06expand\x18\a \x01(\v2\a.ExpandH\x00R\x06expand\x12>\n" + - "\rstoreSnapshot\x18\n" + - " \x01(\v2\x16.gateway.StoreSnapshotH\x00R\rstoreSnapshot\x12;\n" + - "\floadSnapshot\x18\v \x01(\v2\x15.gateway.LoadSnapshotH\x00R\floadSnapshot\x12>\n" + - "\rstoreBookmark\x18\x0e \x01(\v2\x16.gateway.StoreBookmarkH\x00R\rstoreBookmark\x12;\n" + - "\floadBookmark\x18\x0f \x01(\v2\x15.gateway.LoadBookmarkH\x00R\floadBookmark\x128\n" + - "\vchatMessage\x18\x10 \x01(\v2\x14.gateway.ChatMessageH\x00R\vchatMessage\x12L\n" + - "\x11minStatusInterval\x18\x02 \x01(\v2\x19.google.protobuf.DurationH\x01R\x11minStatusInterval\x88\x01\x01B\x0e\n" + - "\frequest_typeB\x14\n" + - "\x12_minStatusInterval\"\x84\a\n" + - "\x0fGatewayResponse\x12!\n" + - "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12!\n" + - "\anewEdge\x18\x03 \x01(\v2\x05.EdgeH\x00R\anewEdge\x127\n" + - "\x06status\x18\x04 \x01(\v2\x1d.gateway.GatewayRequestStatusH\x00R\x06status\x12\x16\n" + - "\x05error\x18\x05 \x01(\tH\x00R\x05error\x12-\n" + - "\n" + - "queryError\x18\x06 \x01(\v2\v.QueryErrorH\x00R\n" + - "queryError\x12,\n" + - "\n" + - "deleteItem\x18\a \x01(\v2\n" + - ".ReferenceH\x00R\n" + - "deleteItem\x12'\n" + - "\n" + - "deleteEdge\x18\b \x01(\v2\x05.EdgeH\x00R\n" + - "deleteEdge\x12'\n" + - "\n" + - "updateItem\x18\t \x01(\v2\x05.ItemH\x00R\n" + - "updateItem\x12P\n" + - "\x13snapshotStoreResult\x18\v \x01(\v2\x1c.gateway.SnapshotStoreResultH\x00R\x13snapshotStoreResult\x12M\n" + - "\x12snapshotLoadResult\x18\f \x01(\v2\x1b.gateway.SnapshotLoadResultH\x00R\x12snapshotLoadResult\x12P\n" + - "\x13bookmarkStoreResult\x18\x0f \x01(\v2\x1c.gateway.BookmarkStoreResultH\x00R\x13bookmarkStoreResult\x12M\n" + - "\x12bookmarkLoadResult\x18\x10 \x01(\v2\x1b.gateway.BookmarkLoadResultH\x00R\x12bookmarkLoadResult\x120\n" + - "\vqueryStatus\x18\x11 \x01(\v2\f.QueryStatusH\x00R\vqueryStatus\x12;\n" + - "\fchatResponse\x18\x12 \x01(\v2\x15.gateway.ChatResponseH\x00R\fchatResponse\x122\n" + - "\ttoolStart\x18\x13 \x01(\v2\x12.gateway.ToolStartH\x00R\ttoolStart\x125\n" + - "\n" + - "toolFinish\x18\x14 \x01(\v2\x13.gateway.ToolFinishH\x00R\n" + - "toolFinishB\x0f\n" + - "\rresponse_type\"\xc5\x02\n" + - "\x14GatewayRequestStatus\x12?\n" + - "\asummary\x18\x03 \x01(\v2%.gateway.GatewayRequestStatus.SummaryR\asummary\x126\n" + - "\x16postProcessingComplete\x18\x04 \x01(\bR\x16postProcessingComplete\x1a\xad\x01\n" + - "\aSummary\x12\x18\n" + - "\aworking\x18\x01 \x01(\x05R\aworking\x12\x18\n" + - "\astalled\x18\x02 \x01(\x05R\astalled\x12\x1a\n" + - "\bcomplete\x18\x03 \x01(\x05R\bcomplete\x12\x14\n" + - "\x05error\x18\x04 \x01(\x05R\x05error\x12\x1c\n" + - "\tcancelled\x18\x05 \x01(\x05R\tcancelled\x12\x1e\n" + - "\n" + - "responders\x18\x06 \x01(\x05R\n" + - "respondersJ\x04\b\x01\x10\x02\"w\n" + - "\rStoreBookmark\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x14\n" + - "\x05msgID\x18\x03 \x01(\fR\x05msgID\x12\x1a\n" + - "\bisSystem\x18\x04 \x01(\bR\bisSystem\"\x8f\x01\n" + - "\x13BookmarkStoreResult\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + - "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + - "\x05msgID\x18\x04 \x01(\fR\x05msgID\x12\x1e\n" + - "\n" + - "bookmarkID\x18\x05 \x01(\fR\n" + - "bookmarkIDJ\x04\b\x03\x10\x04\"\x92\x01\n" + - "\fLoadBookmark\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + - "\x05msgID\x18\x02 \x01(\fR\x05msgID\x12 \n" + - "\vignoreCache\x18\x03 \x01(\bR\vignoreCache\x126\n" + - "\bdeadline\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\"\x96\x01\n" + - "\x12BookmarkLoadResult\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + - "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12,\n" + - "\x11startedQueryUUIDs\x18\x03 \x03(\fR\x11startedQueryUUIDs\x12\x14\n" + - "\x05msgID\x18\x04 \x01(\fR\x05msgID\"[\n" + - "\rStoreSnapshot\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x14\n" + - "\x05msgID\x18\x03 \x01(\fR\x05msgID\"\x8f\x01\n" + - "\x13SnapshotStoreResult\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + - "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + - "\x05msgID\x18\x04 \x01(\fR\x05msgID\x12\x1e\n" + - "\n" + - "snapshotID\x18\x05 \x01(\fR\n" + - "snapshotIDJ\x04\b\x03\x10\x04\"8\n" + - "\fLoadSnapshot\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + - "\x05msgID\x18\x02 \x01(\fR\x05msgID\"h\n" + - "\x12SnapshotLoadResult\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + - "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + - "\x05msgID\x18\x04 \x01(\fR\x05msgID\"M\n" + - "\vChatMessage\x12\x14\n" + - "\x04text\x18\x01 \x01(\tH\x00R\x04text\x12\x18\n" + - "\x06cancel\x18\x02 \x01(\bH\x00R\x06cancelB\x0e\n" + - "\frequest_type\"\x1e\n" + - "\fToolMetadata\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\"v\n" + - "\x0eQueryToolStart\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + - "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\x12\x14\n" + - "\x05scope\x18\x04 \x01(\tR\x05scope\"-\n" + - "\x0fQueryToolFinish\x12\x1a\n" + - "\bnumItems\x18\x01 \x01(\x05R\bnumItems\"u\n" + - "\x15RelationshipToolStart\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + - "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + - "\x05scope\x18\x03 \x01(\tR\x05scope\"4\n" + - "\x16RelationshipToolFinish\x12\x1a\n" + - "\bnumItems\x18\x01 \x01(\x05R\bnumItems\"{\n" + - "\x1bChangesByReferenceToolStart\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + - "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + - "\x05scope\x18\x03 \x01(\tR\x05scope\"\xfb\x01\n" + - "\x18ChangeByReferenceSummary\x12\x14\n" + - "\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" + - "\x04UUID\x18\x02 \x01(\fR\x04UUID\x128\n" + - "\tcreatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x14\n" + - "\x05owner\x18\x04 \x01(\tR\x05owner\x12*\n" + - "\x10numAffectedItems\x18\x05 \x01(\x05R\x10numAffectedItems\x129\n" + - "\fchangeStatus\x18\x06 \x01(\x0e2\x15.changes.ChangeStatusR\fchangeStatus\"k\n" + - "\x1cChangesByReferenceToolFinish\x12K\n" + - "\x0fchangeSummaries\x18\x01 \x03(\v2!.gateway.ChangeByReferenceSummaryR\x0fchangeSummaries\"\x9a\x02\n" + - "\tToolStart\x121\n" + - "\bmetadata\x18\x01 \x01(\v2\x15.gateway.ToolMetadataR\bmetadata\x12/\n" + - "\x05query\x18\x02 \x01(\v2\x17.gateway.QueryToolStartH\x00R\x05query\x12D\n" + - "\frelationship\x18\x03 \x01(\v2\x1e.gateway.RelationshipToolStartH\x00R\frelationship\x12V\n" + - "\x12changesByReference\x18\x04 \x01(\v2$.gateway.ChangesByReferenceToolStartH\x00R\x12changesByReferenceB\v\n" + - "\ttool_type\"\xb4\x02\n" + - "\n" + - "ToolFinish\x121\n" + - "\bmetadata\x18\x01 \x01(\v2\x15.gateway.ToolMetadataR\bmetadata\x12\x14\n" + - "\x05error\x18\x02 \x01(\tR\x05error\x120\n" + - "\x05query\x18\x03 \x01(\v2\x18.gateway.QueryToolFinishH\x00R\x05query\x12E\n" + - "\frelationship\x18\x04 \x01(\v2\x1f.gateway.RelationshipToolFinishH\x00R\frelationship\x12W\n" + - "\x12changesByReference\x18\x05 \x01(\v2%.gateway.ChangesByReferenceToolFinishH\x00R\x12changesByReferenceB\v\n" + - "\ttool_type\"8\n" + - "\fChatResponse\x12\x12\n" + - "\x04text\x18\x01 \x01(\tR\x04text\x12\x14\n" + - "\x05error\x18\x02 \x01(\tR\x05errorB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_gateway_proto_rawDescOnce sync.Once - file_gateway_proto_rawDescData []byte -) - -func file_gateway_proto_rawDescGZIP() []byte { - file_gateway_proto_rawDescOnce.Do(func() { - file_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gateway_proto_rawDesc), len(file_gateway_proto_rawDesc))) - }) - return file_gateway_proto_rawDescData -} - -var file_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 24) -var file_gateway_proto_goTypes = []any{ - (*GatewayRequest)(nil), // 0: gateway.GatewayRequest - (*GatewayResponse)(nil), // 1: gateway.GatewayResponse - (*GatewayRequestStatus)(nil), // 2: gateway.GatewayRequestStatus - (*StoreBookmark)(nil), // 3: gateway.StoreBookmark - (*BookmarkStoreResult)(nil), // 4: gateway.BookmarkStoreResult - (*LoadBookmark)(nil), // 5: gateway.LoadBookmark - (*BookmarkLoadResult)(nil), // 6: gateway.BookmarkLoadResult - (*StoreSnapshot)(nil), // 7: gateway.StoreSnapshot - (*SnapshotStoreResult)(nil), // 8: gateway.SnapshotStoreResult - (*LoadSnapshot)(nil), // 9: gateway.LoadSnapshot - (*SnapshotLoadResult)(nil), // 10: gateway.SnapshotLoadResult - (*ChatMessage)(nil), // 11: gateway.ChatMessage - (*ToolMetadata)(nil), // 12: gateway.ToolMetadata - (*QueryToolStart)(nil), // 13: gateway.QueryToolStart - (*QueryToolFinish)(nil), // 14: gateway.QueryToolFinish - (*RelationshipToolStart)(nil), // 15: gateway.RelationshipToolStart - (*RelationshipToolFinish)(nil), // 16: gateway.RelationshipToolFinish - (*ChangesByReferenceToolStart)(nil), // 17: gateway.ChangesByReferenceToolStart - (*ChangeByReferenceSummary)(nil), // 18: gateway.ChangeByReferenceSummary - (*ChangesByReferenceToolFinish)(nil), // 19: gateway.ChangesByReferenceToolFinish - (*ToolStart)(nil), // 20: gateway.ToolStart - (*ToolFinish)(nil), // 21: gateway.ToolFinish - (*ChatResponse)(nil), // 22: gateway.ChatResponse - (*GatewayRequestStatus_Summary)(nil), // 23: gateway.GatewayRequestStatus.Summary - (*Query)(nil), // 24: Query - (*CancelQuery)(nil), // 25: CancelQuery - (*Expand)(nil), // 26: Expand - (*durationpb.Duration)(nil), // 27: google.protobuf.Duration - (*Item)(nil), // 28: Item - (*Edge)(nil), // 29: Edge - (*QueryError)(nil), // 30: QueryError - (*Reference)(nil), // 31: Reference - (*QueryStatus)(nil), // 32: QueryStatus - (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp - (QueryMethod)(0), // 34: QueryMethod - (ChangeStatus)(0), // 35: changes.ChangeStatus -} -var file_gateway_proto_depIdxs = []int32{ - 24, // 0: gateway.GatewayRequest.query:type_name -> Query - 25, // 1: gateway.GatewayRequest.cancelQuery:type_name -> CancelQuery - 26, // 2: gateway.GatewayRequest.expand:type_name -> Expand - 7, // 3: gateway.GatewayRequest.storeSnapshot:type_name -> gateway.StoreSnapshot - 9, // 4: gateway.GatewayRequest.loadSnapshot:type_name -> gateway.LoadSnapshot - 3, // 5: gateway.GatewayRequest.storeBookmark:type_name -> gateway.StoreBookmark - 5, // 6: gateway.GatewayRequest.loadBookmark:type_name -> gateway.LoadBookmark - 11, // 7: gateway.GatewayRequest.chatMessage:type_name -> gateway.ChatMessage - 27, // 8: gateway.GatewayRequest.minStatusInterval:type_name -> google.protobuf.Duration - 28, // 9: gateway.GatewayResponse.newItem:type_name -> Item - 29, // 10: gateway.GatewayResponse.newEdge:type_name -> Edge - 2, // 11: gateway.GatewayResponse.status:type_name -> gateway.GatewayRequestStatus - 30, // 12: gateway.GatewayResponse.queryError:type_name -> QueryError - 31, // 13: gateway.GatewayResponse.deleteItem:type_name -> Reference - 29, // 14: gateway.GatewayResponse.deleteEdge:type_name -> Edge - 28, // 15: gateway.GatewayResponse.updateItem:type_name -> Item - 8, // 16: gateway.GatewayResponse.snapshotStoreResult:type_name -> gateway.SnapshotStoreResult - 10, // 17: gateway.GatewayResponse.snapshotLoadResult:type_name -> gateway.SnapshotLoadResult - 4, // 18: gateway.GatewayResponse.bookmarkStoreResult:type_name -> gateway.BookmarkStoreResult - 6, // 19: gateway.GatewayResponse.bookmarkLoadResult:type_name -> gateway.BookmarkLoadResult - 32, // 20: gateway.GatewayResponse.queryStatus:type_name -> QueryStatus - 22, // 21: gateway.GatewayResponse.chatResponse:type_name -> gateway.ChatResponse - 20, // 22: gateway.GatewayResponse.toolStart:type_name -> gateway.ToolStart - 21, // 23: gateway.GatewayResponse.toolFinish:type_name -> gateway.ToolFinish - 23, // 24: gateway.GatewayRequestStatus.summary:type_name -> gateway.GatewayRequestStatus.Summary - 33, // 25: gateway.LoadBookmark.deadline:type_name -> google.protobuf.Timestamp - 34, // 26: gateway.QueryToolStart.method:type_name -> QueryMethod - 33, // 27: gateway.ChangeByReferenceSummary.createdAt:type_name -> google.protobuf.Timestamp - 35, // 28: gateway.ChangeByReferenceSummary.changeStatus:type_name -> changes.ChangeStatus - 18, // 29: gateway.ChangesByReferenceToolFinish.changeSummaries:type_name -> gateway.ChangeByReferenceSummary - 12, // 30: gateway.ToolStart.metadata:type_name -> gateway.ToolMetadata - 13, // 31: gateway.ToolStart.query:type_name -> gateway.QueryToolStart - 15, // 32: gateway.ToolStart.relationship:type_name -> gateway.RelationshipToolStart - 17, // 33: gateway.ToolStart.changesByReference:type_name -> gateway.ChangesByReferenceToolStart - 12, // 34: gateway.ToolFinish.metadata:type_name -> gateway.ToolMetadata - 14, // 35: gateway.ToolFinish.query:type_name -> gateway.QueryToolFinish - 16, // 36: gateway.ToolFinish.relationship:type_name -> gateway.RelationshipToolFinish - 19, // 37: gateway.ToolFinish.changesByReference:type_name -> gateway.ChangesByReferenceToolFinish - 38, // [38:38] is the sub-list for method output_type - 38, // [38:38] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name -} - -func init() { file_gateway_proto_init() } -func file_gateway_proto_init() { - if File_gateway_proto != nil { - return - } - file_changes_proto_init() - file_items_proto_init() - file_responses_proto_init() - file_gateway_proto_msgTypes[0].OneofWrappers = []any{ - (*GatewayRequest_Query)(nil), - (*GatewayRequest_CancelQuery)(nil), - (*GatewayRequest_Expand)(nil), - (*GatewayRequest_StoreSnapshot)(nil), - (*GatewayRequest_LoadSnapshot)(nil), - (*GatewayRequest_StoreBookmark)(nil), - (*GatewayRequest_LoadBookmark)(nil), - (*GatewayRequest_ChatMessage)(nil), - } - file_gateway_proto_msgTypes[1].OneofWrappers = []any{ - (*GatewayResponse_NewItem)(nil), - (*GatewayResponse_NewEdge)(nil), - (*GatewayResponse_Status)(nil), - (*GatewayResponse_Error)(nil), - (*GatewayResponse_QueryError)(nil), - (*GatewayResponse_DeleteItem)(nil), - (*GatewayResponse_DeleteEdge)(nil), - (*GatewayResponse_UpdateItem)(nil), - (*GatewayResponse_SnapshotStoreResult)(nil), - (*GatewayResponse_SnapshotLoadResult)(nil), - (*GatewayResponse_BookmarkStoreResult)(nil), - (*GatewayResponse_BookmarkLoadResult)(nil), - (*GatewayResponse_QueryStatus)(nil), - (*GatewayResponse_ChatResponse)(nil), - (*GatewayResponse_ToolStart)(nil), - (*GatewayResponse_ToolFinish)(nil), - } - file_gateway_proto_msgTypes[11].OneofWrappers = []any{ - (*ChatMessage_Text)(nil), - (*ChatMessage_Cancel)(nil), - } - file_gateway_proto_msgTypes[20].OneofWrappers = []any{ - (*ToolStart_Query)(nil), - (*ToolStart_Relationship)(nil), - (*ToolStart_ChangesByReference)(nil), - } - file_gateway_proto_msgTypes[21].OneofWrappers = []any{ - (*ToolFinish_Query)(nil), - (*ToolFinish_Relationship)(nil), - (*ToolFinish_ChangesByReference)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_gateway_proto_rawDesc), len(file_gateway_proto_rawDesc)), - NumEnums: 0, - NumMessages: 24, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_gateway_proto_goTypes, - DependencyIndexes: file_gateway_proto_depIdxs, - MessageInfos: file_gateway_proto_msgTypes, - }.Build() - File_gateway_proto = out.File - file_gateway_proto_goTypes = nil - file_gateway_proto_depIdxs = nil -} diff --git a/sdp-go/gateway_test.go b/sdp-go/gateway_test.go deleted file mode 100644 index d82c11c6..00000000 --- a/sdp-go/gateway_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package sdp - -import "testing" - -func TestEqual(t *testing.T) { - x := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 1, - Stalled: 0, - Complete: 1, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - } - - t.Run("with nil summary", func(t *testing.T) { - y := &GatewayRequestStatus{} - - if x.Equal(y) { - t.Error("expected items to be nonequal") - } - }) - - t.Run("with mismatched summary", func(t *testing.T) { - y := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 1, - Stalled: 0, - Complete: 3, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - } - - if x.Equal(y) { - t.Error("expected items to be nonequal") - } - }) - - t.Run("with different postprocessing states", func(t *testing.T) { - y := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 1, - Stalled: 0, - Complete: 1, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - PostProcessingComplete: true, - } - - if x.Equal(y) { - t.Error("expected items to be different") - } - }) - - t.Run("with same everything", func(t *testing.T) { - y := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 1, - Stalled: 0, - Complete: 1, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - } - - if !x.Equal(y) { - t.Error("expected items to be equal") - } - }) -} - -func TestDone(t *testing.T) { - t.Run("with a request that should be done", func(t *testing.T) { - r := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 0, - Stalled: 1, - Complete: 1, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - PostProcessingComplete: true, - } - - if !r.Done() { - t.Error("expected request .Done() to be true") - } - }) - - t.Run("with a request that shouldn't be done", func(t *testing.T) { - r := &GatewayRequestStatus{ - Summary: &GatewayRequestStatus_Summary{ - Working: 1, - Stalled: 0, - Complete: 1, - Error: 1, - Cancelled: 0, - Responders: 3, - }, - PostProcessingComplete: false, - } - - if r.Done() { - t.Error("expected request .Done() to be false") - } - - r.PostProcessingComplete = true - - if r.Done() { - t.Error("expected request .Done() to be false") - } - }) -} diff --git a/sdp-go/genhandler.go b/sdp-go/genhandler.go deleted file mode 100644 index bc0c4224..00000000 --- a/sdp-go/genhandler.go +++ /dev/null @@ -1,108 +0,0 @@ -//go:build ignore - -package main - -import ( - "fmt" - "html/template" - "os" - "strings" -) - -type Args struct { - Type string -} - -func main() { - fmt.Printf("Running %s go on %s\n", os.Args[0], os.Getenv("GOFILE")) - - cwd, err := os.Getwd() - if err != nil { - panic(err) - } - fmt.Printf(" cwd = %s\n", cwd) - fmt.Printf(" os.Args = %#v\n", os.Args) - - for _, ev := range []string{"GOARCH", "GOOS", "GOFILE", "GOLINE", "GOPACKAGE", "DOLLAR"} { - fmt.Println(" ", ev, "=", os.Getenv(ev)) - } - - if len(os.Args) < 2 { - panic("Missing argument, aborting") - } - - v := Args{Type: os.Args[1]} - t := template.New("simple") - t, err = t.Parse(`// Code generated by "genhandler {{.Type}}"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/tracing" -) - -func New{{.Type}}Handler(spanName string, h func(ctx context.Context, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i {{.Type}} - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRaw{{.Type}}Handler(spanName string, h func(ctx context.Context, m *nats.Msg, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i {{.Type}} - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRaw{{.Type}}Handler(spanName string, h func(ctx context.Context, m *nats.Msg, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i {{.Type}} - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} -`) - if err != nil { - panic(err) - } - - f, err := os.Create(fmt.Sprintf("handler_%v.go", strings.ToLower(v.Type))) - if err != nil { - panic(err) - } - defer f.Close() - - fmt.Printf("Generating handler for %v\n", v) - err = t.Execute(f, v) - if err != nil { - panic(err) - } -} diff --git a/sdp-go/graph/main.go b/sdp-go/graph/main.go deleted file mode 100644 index ab04ab91..00000000 --- a/sdp-go/graph/main.go +++ /dev/null @@ -1,362 +0,0 @@ -// This was written as part of an experiment That required the use of the -// pagerank algorithm on Overmind data. This satisfies the interfaces inside the -// gonum package, which means that we can use any of the code in -// [gonum.org/v1/gonum/graph](https://pkg.go.dev/gonum.org/v1/gonum/graph@v0.15.0) -// to analyse our data. -package graph - -import ( - "github.com/overmindtech/cli/sdp-go" - "gonum.org/v1/gonum/graph" - "gonum.org/v1/gonum/graph/set/uid" -) - -/////////// -// Nodes // -/////////// - -var _ graph.Node = &Node{} - -// A node is always an item -type Node struct { - Item *sdp.Item - Weight float64 - Id int64 -} - -// A graph-unique integer ID -func (n *Node) ID() int64 { - return n.Id -} - -var _ graph.Nodes = &Nodes{} - -type Nodes struct { - // The nodes in the iterator - nodes []*Node - - // The current position in the iterator - i int -} - -// Adds a new node to the list -func (n *Nodes) Append(node *Node) { - n.nodes = append(n.nodes, node) -} - -// Next advances the iterator and returns whether the next call to the item -// method will return a non-nil item. -// -// Next should be called prior to any call to the iterator's item retrieval -// method after the iterator has been obtained or reset. -// -// The order of iteration is implementation dependent. -func (n *Nodes) Next() bool { - n.i++ - return n.i-1 < len(n.nodes) -} - -// Len returns the number of items remaining in the iterator. -// -// If the number of items in the iterator is unknown, too large to materialize -// or too costly to calculate then Len may return a negative value. In this case -// the consuming function must be able to operate on the items of the iterator -// directly without materializing the items into a slice. The magnitude of a -// negative length has implementation-dependent semantics. -func (n *Nodes) Len() int { - return len(n.nodes) - n.i -} - -// Reset returns the iterator to its start position. -func (n *Nodes) Reset() { - n.i = 0 -} - -// Node returns the current Node from the iterator. -func (n *Nodes) Node() graph.Node { - // The Next() function gets called *before* the first item is returned, so - // we need to return the item at position i (e.g. 1 is 1st position) rather - // than the actual index i. This allows us to start i at zero which makes a - // lot more sense - getIndex := n.i - 1 - - if getIndex >= len(n.nodes) || getIndex < 0 { - return nil - } - - return n.nodes[getIndex] -} - -/////////// -// Edges // -/////////// - -var _ graph.WeightedEdge = &Edge{} - -type Edge struct { - from *Node - to *Node - weight float64 -} - -// Creates a new edge. The weight of an edge is the sum of the weights of the -// two nodes -func NewEdge(from, to *Node) *Edge { - return &Edge{ - from: from, - to: to, - weight: from.Weight + to.Weight, - } -} - -// From returns the from node of the edge. -func (e *Edge) From() graph.Node { - return e.from -} - -// To returns the to node of the edge. -func (e *Edge) To() graph.Node { - return e.to -} - -// ReversedEdge returns the edge reversal of the receiver if a reversal is valid -// for the data type. When a reversal is valid an edge of the same type as the -// receiver with nodes of the receiver swapped should be returned, otherwise the -// receiver should be returned unaltered. -func (e *Edge) ReversedEdge() graph.Edge { - return nil -} - -func (e *Edge) Weight() float64 { - return e.weight -} - -/////////// -// Graph // -/////////// - -// Assert that SDPGraph satisfies the graph.WeightedDirected interface -var _ graph.WeightedDirected = &SDPGraph{} - -type SDPGraph struct { - uidSet *uid.Set - - nodesByID map[int64]*Node - nodesByGUN map[string]*Node - - // A map of items that have not been seen yet. The key is the GUN of the - // "To" end of the edge, and the value is a slice of nodes that are the - // "From" edges - unseenEdges map[string][]*Node - - edges []*Edge - - undirected bool -} - -// NewSDPGraph creates a new SDPGraph. If undirected is true, the graph will be -// treated as undirected, meaning that all edges will be bidirectional -func NewSDPGraph(undirected bool) *SDPGraph { - return &SDPGraph{ - uidSet: uid.NewSet(), - nodesByID: make(map[int64]*Node), - nodesByGUN: make(map[string]*Node), - unseenEdges: make(map[string][]*Node), - edges: make([]*Edge, 0), - undirected: undirected, - } -} - -// AddItem adds an item to the graph including processing of its edges, returns -// the ID the node was assigned. -func (g *SDPGraph) AddItem(item *sdp.Item, weight float64) int64 { - id := g.uidSet.NewID() - g.uidSet.Use(id) - - // Add the node to the storage - node := Node{ - Item: item, - Weight: weight, - Id: id, - } - g.nodesByID[id] = &node - g.nodesByGUN[item.GloballyUniqueName()] = &node - - // TODO(LIQs): https://github.com/overmindtech/workspace/issues/1228 - // Find all edges and add them - for _, linkedItem := range item.GetLinkedItems() { - // Check if the linked item node exists - linkedItemNode, exists := g.nodesByGUN[linkedItem.GetItem().GloballyUniqueName()] - - if exists { - // Add the edge - g.edges = append(g.edges, NewEdge(&node, linkedItemNode)) - - if g.undirected { - // Also add the reverse edge - g.edges = append(g.edges, NewEdge(linkedItemNode, &node)) - } - } else { - // If the target for the edge doesn't exist, add this to the list to - // be created later - if _, exists := g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()]; !exists { - g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()] = []*Node{&node} - } else { - g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()] = append(g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()], &node) - } - } - } - - // If there are any unseen edges that are now seen, add them - if unseenEdges, exists := g.unseenEdges[item.GloballyUniqueName()]; exists { - for _, unseenEdge := range unseenEdges { - // Add the edge - g.edges = append(g.edges, NewEdge(unseenEdge, &node)) - - if g.undirected { - // Also add the reverse edge - g.edges = append(g.edges, NewEdge(&node, unseenEdge)) - } - } - } - - return id -} - -// HasEdgeFromTo returns whether an edge exists in the graph from u to v with -// the IDs uid and vid. -func (g *SDPGraph) HasEdgeFromTo(uid, vid int64) bool { - for _, edge := range g.edges { - if edge.from.Id == uid && edge.to.Id == vid { - return true - } - } - - return false -} - -// To returns all nodes that can reach directly to the node with the given ID. -// -// To must not return nil. -func (g *SDPGraph) To(id int64) graph.Nodes { - nodes := Nodes{} - - for _, edge := range g.edges { - if edge.to.Id == id { - nodes.Append(edge.to) - } - } - - return &nodes -} - -// WeightedEdge returns the weighted edge from u to v with IDs uid and vid if -// such an edge exists and nil otherwise. The node v must be directly reachable -// from u as defined by the From method. -func (g *SDPGraph) WeightedEdge(uid, vid int64) graph.WeightedEdge { - for _, edge := range g.edges { - if edge.from.Id == uid && edge.to.Id == vid { - return edge - } - } - - return nil -} - -// Weight returns the weight for the edge between x and y with IDs xid and yid -// if Edge(xid, yid) returns a non-nil Edge. If x and y are the same node or -// there is no joining edge between the two nodes the weight value returned is -// implementation dependent. Weight returns true if an edge exists between x and -// y or if x and y have the same ID, false otherwise. -func (g *SDPGraph) Weight(xid, yid int64) (w float64, ok bool) { - edge := g.WeightedEdge(xid, yid) - - if edge == nil { - return 0, false - } - - return edge.Weight(), true -} - -// Node returns the node with the given ID if it exists in the graph, and nil -// otherwise. -func (g *SDPGraph) Node(id int64) graph.Node { - node, exists := g.nodesByID[id] - - if !exists { - return nil - } - - return node -} - -// Gets a node from the graph by it's globally unique name -func (g *SDPGraph) NodeByGloballyUniqueName(globallyUniqueName string) *Node { - node, exists := g.nodesByGUN[globallyUniqueName] - - if !exists { - return nil - } - - return node -} - -// Nodes returns all the nodes in the graph. -// -// Nodes must not return nil. -func (g *SDPGraph) Nodes() graph.Nodes { - nodes := Nodes{} - - for _, node := range g.nodesByID { - nodes.Append(node) - } - - return &nodes -} - -// From returns all nodes that can be reached directly from the node with the -// given ID. -// -// From must not return nil. -func (g *SDPGraph) From(id int64) graph.Nodes { - nodes := Nodes{} - - for _, edge := range g.edges { - if edge.From().ID() == id { - nodes.Append(edge.to) - } - } - - return &nodes -} - -// HasEdgeBetween returns whether an edge exists between nodes with IDs xid and -// yid without considering direction. -func (g *SDPGraph) HasEdgeBetween(xid, yid int64) bool { - var fromID int64 - var toID int64 - - for _, edge := range g.edges { - fromID = edge.From().ID() - toID = edge.To().ID() - - if (fromID == xid && toID == yid) || (fromID == yid && toID == xid) { - return true - } - } - - return false -} - -// Edge returns the edge from u to v, with IDs uid and vid, if such an edge -// exists and nil otherwise. The node v must be directly reachable from u as -// defined by the From method. -func (g *SDPGraph) Edge(uid, vid int64) graph.Edge { - for _, edge := range g.edges { - if (edge.From().ID() == uid) && (edge.To().ID() == vid) { - return edge - } - } - - return nil -} diff --git a/sdp-go/graph/main_test.go b/sdp-go/graph/main_test.go deleted file mode 100644 index b16d14b7..00000000 --- a/sdp-go/graph/main_test.go +++ /dev/null @@ -1,300 +0,0 @@ -package graph - -import ( - "testing" - - "github.com/overmindtech/cli/sdp-go" - "gonum.org/v1/gonum/graph/network" - "google.golang.org/protobuf/types/known/structpb" -) - -func makeTestItem(name string) *sdp.Item { - return &sdp.Item{ - Type: "test", - UniqueAttribute: "name", - Scope: "test", - Attributes: &sdp.ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": { - Kind: &structpb.Value_StringValue{ - StringValue: name, - }, - }, - }, - }, - }, - } -} - -func TestNode(t *testing.T) { - node := Node{ - Item: makeTestItem("test"), - Weight: 1.5, - Id: 1, - } - - if node.ID() != 1 { - t.Errorf("expected ID to be 1, got %v", node.ID()) - } -} - -func TestNodes(t *testing.T) { - nodes := Nodes{} - - nodes.Append(&Node{ - Item: makeTestItem("a"), - Weight: 1.5, - Id: 1, - }) - nodes.Append(&Node{ - Item: makeTestItem("b"), - Weight: 1.5, - Id: 2, - }) - - if nodes.Len() != 2 { - t.Errorf("expected length to be 2, got %v", nodes.Len()) - } - - // Call node before next should return nil - if nodes.Node() != nil { - t.Errorf("expected Node to be nil") - } - - // Call next - if nodes.Next() != true { - t.Errorf("expected Next to be true") - } - - // A - if nodes.Node().ID() != 1 { - t.Errorf("expected ID to be 1, got %v", nodes.Node().ID()) - } - - if nodes.Len() != 1 { - t.Errorf("expected length to be 1, got %v", nodes.Len()) - } - - if nodes.Next() != true { - t.Errorf("expected Next to be true") - } - - // B - if nodes.Node().ID() != 2 { - t.Errorf("expected ID to be 2, got %v", nodes.Node().ID()) - } - - if nodes.Len() != 0 { - t.Errorf("expected length to be 0, got %v", nodes.Len()) - } - - if nodes.Next() != false { - t.Errorf("expected Next to be false") - } - - if nodes.Node() != nil { - t.Errorf("expected Node to be nil") - - } - - nodes.Reset() - - if nodes.Len() != 2 { - t.Errorf("expected length to be 2, got %v", nodes.Len()) - } -} - -func TestGraph(t *testing.T) { - // A list of items that form the following graph: - // - // ┌────┐ - // ┌──┤ A ├──┐ - // │ └────┘ │ - // │ │ - // ┌──▼───┐ ┌──▼─┐ - // │ B ├──►│ C │ - // └──┬───┘ └────┘ - // │ - // │ - // ┌──▼───┐ - // │ D │ - // └──────┘ - // - a := makeTestItem("a") - b := makeTestItem("b") - c := makeTestItem("c") - d := makeTestItem("d") - - // TODO(LIQs): https://github.com/overmindtech/workspace/issues/1228 - a.LinkedItems = []*sdp.LinkedItem{ - { - Item: b.Reference(), - }, - { - Item: c.Reference(), - }, - } - - b.LinkedItems = []*sdp.LinkedItem{ - { - Item: c.Reference(), - }, - { - Item: d.Reference(), - }, - } - - graph := NewSDPGraph(false) - - aID := graph.AddItem(a, 1) - bID := graph.AddItem(b, 1) - cID := graph.AddItem(c, 1) - dID := graph.AddItem(d, 1) - - t.Run("To", func(t *testing.T) { - nodes := graph.To(cID) - - if nodes.Len() != 2 { - t.Errorf("expected length to be 2, got %v", nodes.Len()) - } - }) - - t.Run("WeightedEdge", func(t *testing.T) { - t.Run("with a real edge", func(t *testing.T) { - e := graph.WeightedEdge(aID, cID) - - if e == nil { - t.Fatal("expected edge to be non-nil") - } - - if e.Weight() != 2 { - t.Errorf("expected weight to be 2, got %v", e.Weight()) - } - }) - - t.Run("with a non-existent edge", func(t *testing.T) { - e := graph.WeightedEdge(aID, dID) - - if e != nil { - t.Errorf("expected edge to be nil") - } - }) - }) - - t.Run("Weight", func(t *testing.T) { - t.Run("with a real edge", func(t *testing.T) { - w, ok := graph.Weight(aID, cID) - - if !ok { - t.Fatal("expected edge to be non-nil") - } - - if w != 2 { - t.Errorf("expected weight to be 2, got %v", w) - } - }) - - t.Run("with a non-existent edge", func(t *testing.T) { - w, ok := graph.Weight(aID, dID) - - if ok { - t.Errorf("expected edge to be nil") - } - - if w != 0 { - t.Errorf("expected weight to be 0, got %v", w) - } - }) - }) - - t.Run("Node", func(t *testing.T) { - t.Run("with a node that exists", func(t *testing.T) { - n := graph.Node(aID) - - if n == nil { - t.Fatal("expected node to be non-nil") - } - - if n.ID() != aID { - t.Errorf("expected ID to be %v, got %v", aID, n.ID()) - } - }) - - t.Run("with a node that doesn't exist", func(t *testing.T) { - n := graph.Node(999) - - if n != nil { - t.Errorf("expected node to be nil") - } - }) - }) - - t.Run("Nodes", func(t *testing.T) { - nodes := graph.Nodes() - - if nodes.Len() != 4 { - t.Errorf("expected length to be 4, got %v", nodes.Len()) - } - }) - - t.Run("From", func(t *testing.T) { - nodes := graph.From(bID) - - if nodes.Len() != 2 { - t.Errorf("expected length to be 2, got %v", nodes.Len()) - } - }) - - t.Run("HasEdgeBetween", func(t *testing.T) { - t.Run("with a real edge", func(t *testing.T) { - ok := graph.HasEdgeBetween(aID, cID) - - if !ok { - t.Fatal("expected edge to be non-nil") - } - }) - - t.Run("with a non-existent edge", func(t *testing.T) { - ok := graph.HasEdgeBetween(aID, dID) - - if ok { - t.Errorf("expected edge to be nil") - } - }) - }) - - t.Run("Edge", func(t *testing.T) { - e := graph.Edge(aID, cID) - - if e == nil { - t.Fatal("expected edge to be non-nil") - } - }) - - t.Run("PageRank", func(t *testing.T) { - ranks := network.PageRank(graph, 0.85, 0.0001) - - if len(ranks) != 4 { - t.Errorf("expected length to be 4, got %v", len(ranks)) - } - }) - - t.Run("Undirected", func(t *testing.T) { - directed := NewSDPGraph(false) - undirected := NewSDPGraph(true) - - directed.AddItem(a, 1) - directed.AddItem(b, 1) - directed.AddItem(c, 1) - directed.AddItem(d, 1) - undirected.AddItem(a, 1) - undirected.AddItem(b, 1) - undirected.AddItem(c, 1) - undirected.AddItem(d, 1) - - if len(undirected.edges) == 4 { - t.Errorf("expected undirected graph to have > 4 edges, got %v", len(undirected.edges)) - } - }) -} diff --git a/sdp-go/handler_cancelquery.go b/sdp-go/handler_cancelquery.go deleted file mode 100644 index 921a58dc..00000000 --- a/sdp-go/handler_cancelquery.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler CancelQuery"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i CancelQuery - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawCancelQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i CancelQuery - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawCancelQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i CancelQuery - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/handler_gatewayresponse.go b/sdp-go/handler_gatewayresponse.go deleted file mode 100644 index 776f22bd..00000000 --- a/sdp-go/handler_gatewayresponse.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler GatewayResponse"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i GatewayResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawGatewayResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i GatewayResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawGatewayResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i GatewayResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/handler_natsgetlogrecordsrequest.go b/sdp-go/handler_natsgetlogrecordsrequest.go deleted file mode 100644 index 64e08127..00000000 --- a/sdp-go/handler_natsgetlogrecordsrequest.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler NATSGetLogRecordsRequest"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsRequest - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsRequest - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsRequest - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/handler_natsgetlogrecordsresponse.go b/sdp-go/handler_natsgetlogrecordsresponse.go deleted file mode 100644 index 1af8bb5e..00000000 --- a/sdp-go/handler_natsgetlogrecordsresponse.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler NATSGetLogRecordsResponse"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i NATSGetLogRecordsResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/handler_query.go b/sdp-go/handler_query.go deleted file mode 100644 index 3bd8adca..00000000 --- a/sdp-go/handler_query.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler Query"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i Query - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i Query - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i Query - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/handler_queryresponse.go b/sdp-go/handler_queryresponse.go deleted file mode 100644 index 2f8c309c..00000000 --- a/sdp-go/handler_queryresponse.go +++ /dev/null @@ -1,56 +0,0 @@ -// Code generated by "genhandler QueryResponse"; DO NOT EDIT - -package sdp - -import ( - "context" - - "github.com/nats-io/nats.go" - "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 { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i QueryResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, &i) - }, - tracing.Tracer(), - ) -} - -func NewRawQueryResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i QueryResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} - -func NewAsyncRawQueryResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { - return NewAsyncOtelExtractingHandler( - spanName, - func(ctx context.Context, m *nats.Msg) { - var i QueryResponse - err := Unmarshal(ctx, m.Data, &i) - if err != nil { - return - } - h(ctx, m, &i) - }, - tracing.Tracer(), - ) -} diff --git a/sdp-go/instance_detect.go b/sdp-go/instance_detect.go deleted file mode 100644 index deb2ef56..00000000 --- a/sdp-go/instance_detect.go +++ /dev/null @@ -1,91 +0,0 @@ -package sdp - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - - "github.com/overmindtech/cli/tracing" -) - -// Information about a particular instance of Overmind. This is used to -// determine where to send requests, how to authenticate etc. -type OvermindInstance struct { - FrontendUrl *url.URL - ApiUrl *url.URL - NatsUrl *url.URL - Audience string - Auth0Domain string - CLIClientID string -} - -// GatewayUrl returns the URL for the gateway for this instance. -func (oi OvermindInstance) GatewayUrl() string { - return fmt.Sprintf("%v/api/gateway", oi.ApiUrl.String()) -} - -func (oi OvermindInstance) String() string { - return fmt.Sprintf("Frontend: %v, API: %v, Nats: %v, Audience: %v", oi.FrontendUrl, oi.ApiUrl, oi.NatsUrl, oi.Audience) -} - -type instanceData struct { - Api string `json:"api_url"` - Nats string `json:"nats_url"` - Aud string `json:"aud"` - Auth0Domain string `json:"auth0_domain"` - CLIClientID string `json:"auth0_cli_client_id"` -} - -// NewOvermindInstance creates a new OvermindInstance from the given app URL -// with all URLs filled in, or an error. The app URL should be the URL of the -// frontend of the Overmind instance. e.g. https://app.overmind.tech -func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, error) { - var instance OvermindInstance - var err error - - instance.FrontendUrl, err = url.Parse(app) - if err != nil { - return instance, fmt.Errorf("invalid app value '%v', error: %w", app, err) - } - - // Get the instance data - instanceDataUrl := fmt.Sprintf("%v/api/public/instance-data", instance.FrontendUrl) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, instanceDataUrl, nil) - if err != nil { - return OvermindInstance{}, fmt.Errorf("could not initialize instance-data fetch: %w", err) - } - - req = req.WithContext(ctx) - res, err := tracing.HTTPClient().Do(req) - if err != nil { - return OvermindInstance{}, fmt.Errorf("could not fetch instance-data: %w", err) - } - - if res.StatusCode != http.StatusOK { - return OvermindInstance{}, fmt.Errorf("instance-data fetch returned non-200 status: %v", res.StatusCode) - } - - defer res.Body.Close() - data := instanceData{} - err = json.NewDecoder(res.Body).Decode(&data) - if err != nil { - return OvermindInstance{}, fmt.Errorf("could not parse instance-data: %w", err) - } - - instance.ApiUrl, err = url.Parse(data.Api) - if err != nil { - return OvermindInstance{}, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err) - } - instance.NatsUrl, err = url.Parse(data.Nats) - if err != nil { - return OvermindInstance{}, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err) - } - - instance.Audience = data.Aud - instance.CLIClientID = data.CLIClientID - instance.Auth0Domain = data.Auth0Domain - - return instance, nil -} diff --git a/sdp-go/invites.pb.go b/sdp-go/invites.pb.go deleted file mode 100644 index 72b2ec9f..00000000 --- a/sdp-go/invites.pb.go +++ /dev/null @@ -1,546 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: invites.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - _ "google.golang.org/protobuf/types/known/structpb" - _ "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Invite_InviteStatus int32 - -const ( - Invite_INVITE_STATUS_UNSPECIFIED Invite_InviteStatus = 0 - // The user has been invited but has not yet accepted - Invite_INVITE_STATUS_INVITED Invite_InviteStatus = 1 - // The user has accepted the invitation - Invite_INVITE_STATUS_ACCEPTED Invite_InviteStatus = 2 -) - -// Enum value maps for Invite_InviteStatus. -var ( - Invite_InviteStatus_name = map[int32]string{ - 0: "INVITE_STATUS_UNSPECIFIED", - 1: "INVITE_STATUS_INVITED", - 2: "INVITE_STATUS_ACCEPTED", - } - Invite_InviteStatus_value = map[string]int32{ - "INVITE_STATUS_UNSPECIFIED": 0, - "INVITE_STATUS_INVITED": 1, - "INVITE_STATUS_ACCEPTED": 2, - } -) - -func (x Invite_InviteStatus) Enum() *Invite_InviteStatus { - p := new(Invite_InviteStatus) - *p = x - return p -} - -func (x Invite_InviteStatus) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Invite_InviteStatus) Descriptor() protoreflect.EnumDescriptor { - return file_invites_proto_enumTypes[0].Descriptor() -} - -func (Invite_InviteStatus) Type() protoreflect.EnumType { - return &file_invites_proto_enumTypes[0] -} - -func (x Invite_InviteStatus) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Invite_InviteStatus.Descriptor instead. -func (Invite_InviteStatus) EnumDescriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{2, 0} -} - -type CreateInviteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Emails []string `protobuf:"bytes,1,rep,name=emails,proto3" json:"emails,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateInviteRequest) Reset() { - *x = CreateInviteRequest{} - mi := &file_invites_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateInviteRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateInviteRequest) ProtoMessage() {} - -func (x *CreateInviteRequest) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[0] - 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 CreateInviteRequest.ProtoReflect.Descriptor instead. -func (*CreateInviteRequest) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{0} -} - -func (x *CreateInviteRequest) GetEmails() []string { - if x != nil { - return x.Emails - } - return nil -} - -type CreateInviteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateInviteResponse) Reset() { - *x = CreateInviteResponse{} - mi := &file_invites_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateInviteResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateInviteResponse) ProtoMessage() {} - -func (x *CreateInviteResponse) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[1] - 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 CreateInviteResponse.ProtoReflect.Descriptor instead. -func (*CreateInviteResponse) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{1} -} - -type Invite struct { - state protoimpl.MessageState `protogen:"open.v1"` - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - Status Invite_InviteStatus `protobuf:"varint,2,opt,name=status,proto3,enum=invites.Invite_InviteStatus" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Invite) Reset() { - *x = Invite{} - mi := &file_invites_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Invite) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Invite) ProtoMessage() {} - -func (x *Invite) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[2] - 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 Invite.ProtoReflect.Descriptor instead. -func (*Invite) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{2} -} - -func (x *Invite) GetEmail() string { - if x != nil { - return x.Email - } - return "" -} - -func (x *Invite) GetStatus() Invite_InviteStatus { - if x != nil { - return x.Status - } - return Invite_INVITE_STATUS_UNSPECIFIED -} - -type ListInvitesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInvitesRequest) Reset() { - *x = ListInvitesRequest{} - mi := &file_invites_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInvitesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInvitesRequest) ProtoMessage() {} - -func (x *ListInvitesRequest) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[3] - 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 ListInvitesRequest.ProtoReflect.Descriptor instead. -func (*ListInvitesRequest) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{3} -} - -type ListInvitesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Invites []*Invite `protobuf:"bytes,1,rep,name=invites,proto3" json:"invites,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListInvitesResponse) Reset() { - *x = ListInvitesResponse{} - mi := &file_invites_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListInvitesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListInvitesResponse) ProtoMessage() {} - -func (x *ListInvitesResponse) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[4] - 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 ListInvitesResponse.ProtoReflect.Descriptor instead. -func (*ListInvitesResponse) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{4} -} - -func (x *ListInvitesResponse) GetInvites() []*Invite { - if x != nil { - return x.Invites - } - return nil -} - -type RevokeInviteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RevokeInviteRequest) Reset() { - *x = RevokeInviteRequest{} - mi := &file_invites_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RevokeInviteRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RevokeInviteRequest) ProtoMessage() {} - -func (x *RevokeInviteRequest) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[5] - 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 RevokeInviteRequest.ProtoReflect.Descriptor instead. -func (*RevokeInviteRequest) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{5} -} - -func (x *RevokeInviteRequest) GetEmail() string { - if x != nil { - return x.Email - } - return "" -} - -type RevokeInviteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RevokeInviteResponse) Reset() { - *x = RevokeInviteResponse{} - mi := &file_invites_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RevokeInviteResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RevokeInviteResponse) ProtoMessage() {} - -func (x *RevokeInviteResponse) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[6] - 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 RevokeInviteResponse.ProtoReflect.Descriptor instead. -func (*RevokeInviteResponse) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{6} -} - -type ResendInviteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ResendInviteRequest) Reset() { - *x = ResendInviteRequest{} - mi := &file_invites_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ResendInviteRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ResendInviteRequest) ProtoMessage() {} - -func (x *ResendInviteRequest) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[7] - 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 ResendInviteRequest.ProtoReflect.Descriptor instead. -func (*ResendInviteRequest) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{7} -} - -func (x *ResendInviteRequest) GetEmail() string { - if x != nil { - return x.Email - } - return "" -} - -type ResendInviteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ResendInviteResponse) Reset() { - *x = ResendInviteResponse{} - mi := &file_invites_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ResendInviteResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ResendInviteResponse) ProtoMessage() {} - -func (x *ResendInviteResponse) ProtoReflect() protoreflect.Message { - mi := &file_invites_proto_msgTypes[8] - 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 ResendInviteResponse.ProtoReflect.Descriptor instead. -func (*ResendInviteResponse) Descriptor() ([]byte, []int) { - return file_invites_proto_rawDescGZIP(), []int{8} -} - -var File_invites_proto protoreflect.FileDescriptor - -const file_invites_proto_rawDesc = "" + - "\n" + - "\rinvites.proto\x12\ainvites\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"-\n" + - "\x13CreateInviteRequest\x12\x16\n" + - "\x06emails\x18\x01 \x03(\tR\x06emails\"\x16\n" + - "\x14CreateInviteResponse\"\xba\x01\n" + - "\x06Invite\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\x124\n" + - "\x06status\x18\x02 \x01(\x0e2\x1c.invites.Invite.InviteStatusR\x06status\"d\n" + - "\fInviteStatus\x12\x1d\n" + - "\x19INVITE_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + - "\x15INVITE_STATUS_INVITED\x10\x01\x12\x1a\n" + - "\x16INVITE_STATUS_ACCEPTED\x10\x02\"\x14\n" + - "\x12ListInvitesRequest\"@\n" + - "\x13ListInvitesResponse\x12)\n" + - "\ainvites\x18\x01 \x03(\v2\x0f.invites.InviteR\ainvites\"+\n" + - "\x13RevokeInviteRequest\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\"\x16\n" + - "\x14RevokeInviteResponse\"+\n" + - "\x13ResendInviteRequest\x12\x14\n" + - "\x05email\x18\x01 \x01(\tR\x05email\"\x16\n" + - "\x14ResendInviteResponse2\xc0\x02\n" + - "\rInviteService\x12K\n" + - "\fCreateInvite\x12\x1c.invites.CreateInviteRequest\x1a\x1d.invites.CreateInviteResponse\x12H\n" + - "\vListInvites\x12\x1b.invites.ListInvitesRequest\x1a\x1c.invites.ListInvitesResponse\x12K\n" + - "\fRevokeInvite\x12\x1c.invites.RevokeInviteRequest\x1a\x1d.invites.RevokeInviteResponse\x12K\n" + - "\fResendInvite\x12\x1c.invites.ResendInviteRequest\x1a\x1d.invites.ResendInviteResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_invites_proto_rawDescOnce sync.Once - file_invites_proto_rawDescData []byte -) - -func file_invites_proto_rawDescGZIP() []byte { - file_invites_proto_rawDescOnce.Do(func() { - file_invites_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_invites_proto_rawDesc), len(file_invites_proto_rawDesc))) - }) - return file_invites_proto_rawDescData -} - -var file_invites_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_invites_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_invites_proto_goTypes = []any{ - (Invite_InviteStatus)(0), // 0: invites.Invite.InviteStatus - (*CreateInviteRequest)(nil), // 1: invites.CreateInviteRequest - (*CreateInviteResponse)(nil), // 2: invites.CreateInviteResponse - (*Invite)(nil), // 3: invites.Invite - (*ListInvitesRequest)(nil), // 4: invites.ListInvitesRequest - (*ListInvitesResponse)(nil), // 5: invites.ListInvitesResponse - (*RevokeInviteRequest)(nil), // 6: invites.RevokeInviteRequest - (*RevokeInviteResponse)(nil), // 7: invites.RevokeInviteResponse - (*ResendInviteRequest)(nil), // 8: invites.ResendInviteRequest - (*ResendInviteResponse)(nil), // 9: invites.ResendInviteResponse -} -var file_invites_proto_depIdxs = []int32{ - 0, // 0: invites.Invite.status:type_name -> invites.Invite.InviteStatus - 3, // 1: invites.ListInvitesResponse.invites:type_name -> invites.Invite - 1, // 2: invites.InviteService.CreateInvite:input_type -> invites.CreateInviteRequest - 4, // 3: invites.InviteService.ListInvites:input_type -> invites.ListInvitesRequest - 6, // 4: invites.InviteService.RevokeInvite:input_type -> invites.RevokeInviteRequest - 8, // 5: invites.InviteService.ResendInvite:input_type -> invites.ResendInviteRequest - 2, // 6: invites.InviteService.CreateInvite:output_type -> invites.CreateInviteResponse - 5, // 7: invites.InviteService.ListInvites:output_type -> invites.ListInvitesResponse - 7, // 8: invites.InviteService.RevokeInvite:output_type -> invites.RevokeInviteResponse - 9, // 9: invites.InviteService.ResendInvite:output_type -> invites.ResendInviteResponse - 6, // [6:10] is the sub-list for method output_type - 2, // [2:6] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name -} - -func init() { file_invites_proto_init() } -func file_invites_proto_init() { - if File_invites_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_invites_proto_rawDesc), len(file_invites_proto_rawDesc)), - NumEnums: 1, - NumMessages: 9, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_invites_proto_goTypes, - DependencyIndexes: file_invites_proto_depIdxs, - EnumInfos: file_invites_proto_enumTypes, - MessageInfos: file_invites_proto_msgTypes, - }.Build() - File_invites_proto = out.File - file_invites_proto_goTypes = nil - file_invites_proto_depIdxs = nil -} diff --git a/sdp-go/items.go b/sdp-go/items.go deleted file mode 100644 index 8f09d3c9..00000000 --- a/sdp-go/items.go +++ /dev/null @@ -1,659 +0,0 @@ -package sdp - -import ( - "context" - "crypto/sha256" - "encoding/base32" - "encoding/json" - "errors" - "fmt" - "reflect" - "sort" - "strings" - "time" - - "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/types/known/structpb" -) - -const WILDCARD = "*" - -func (bp *BlastPropagation) IsEqual(other *BlastPropagation) bool { - return bp.GetIn() == other.GetIn() && bp.GetOut() == other.GetOut() -} - -// UniqueAttributeValue returns the value of whatever the Unique Attribute is -// for this item. This will then be converted to a string and returned -func (i *Item) UniqueAttributeValue() string { - var value interface{} - var err error - - value, err = i.GetAttributes().Get(i.GetUniqueAttribute()) - - if err == nil { - return fmt.Sprint(value) - } - - return "" -} - -// Reference returns an SDP reference for the item -func (i *Item) Reference() *Reference { - return &Reference{ - Scope: i.GetScope(), - Type: i.GetType(), - UniqueAttributeValue: i.UniqueAttributeValue(), - } -} - -// GloballyUniqueName Returns a string that defines the Item globally. This a -// combination of the following values: -// -// - scope -// - type -// - uniqueAttributeValue -// -// They are concatenated with dots (.) -func (i *Item) GloballyUniqueName() string { - return strings.Join([]string{ - i.GetScope(), - i.GetType(), - i.UniqueAttributeValue(), - }, - ".", - ) -} - -// Hash Returns a 12 character hash for the item. This is likely but not -// guaranteed to be unique. The hash is calculated using the GloballyUniqueName -func (i *Item) Hash() string { - return HashSum(([]byte(fmt.Sprint(i.GloballyUniqueName())))) -} - -func (e *Edge) IsEqual(other *Edge) bool { - return e.GetFrom().IsEqual(other.GetFrom()) && - e.GetTo().IsEqual(other.GetTo()) && - e.GetBlastPropagation().IsEqual(other.GetBlastPropagation()) -} - -// Hash Returns a 12 character hash for the item. This is likely but not -// guaranteed to be unique. The hash is calculated using the GloballyUniqueName -func (r *Reference) Hash() string { - return HashSum(([]byte(fmt.Sprint(r.GloballyUniqueName())))) -} - -// GloballyUniqueName Returns a string that defines the Item globally. This a -// combination of the following values: -// -// - scope -// - type -// - uniqueAttributeValue -// -// They are concatenated with dots (.) -func (r *Reference) GloballyUniqueName() string { - if r == nil { - // in the llm templates nil references are processed, and after spending - // half an hour on trying to figure out what was happening in the - // reflect code, I decided to just return an empty string here. DS, - // 2025-02-26 - return "" - } - if r.GetIsQuery() { - if r.GetMethod() == QueryMethod_GET { - // GET queries are single items - return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetQuery()) - } - panic(fmt.Sprintf("cannot get globally unique name for query reference: %v", r)) - } - return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetUniqueAttributeValue()) -} - -// Key returns a globally unique string for this reference, even if it is a GET query -func (r *Reference) Key() string { - if r == nil { - panic("cannot get key for nil reference") - } - if r.GetIsQuery() { - if r.IsSingle() { - // GET queries without wildcards are single items - return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetQuery()) - } - return fmt.Sprintf("%v: %v.%v.%v", r.GetMethod(), r.GetScope(), r.GetType(), r.GetQuery()) - } - return r.GloballyUniqueName() -} - -// IsSingle returns true if this references a single item, false if it is a LIST -// or SEARCH query, or a GET query with scope and/or type wildcards. -func (r *Reference) IsSingle() bool { - // nil reference is never good - if r == nil { - return false - } - // if it is a query, then it is only a single item if it is a GET query with no wildcards - if r.GetIsQuery() { - return r.GetMethod() == QueryMethod_GET && r.GetScope() != "*" && r.GetType() != "*" - } - // if it is not a query, then it is always single item - return true -} - -func (r *Reference) IsEqual(other *Reference) bool { - return r.GetScope() == other.GetScope() && - r.GetType() == other.GetType() && - r.GetUniqueAttributeValue() == other.GetUniqueAttributeValue() && - r.GetIsQuery() == other.GetIsQuery() && - r.GetMethod() == other.GetMethod() && - r.GetQuery() == other.GetQuery() -} - -func (r *Reference) ToQuery() *Query { - if !r.GetIsQuery() { - return &Query{ - Scope: r.GetScope(), - Type: r.GetType(), - Method: QueryMethod_GET, - Query: r.GetUniqueAttributeValue(), - } - } - - return &Query{ - Scope: r.GetScope(), - Type: r.GetType(), - Method: r.GetMethod(), - Query: r.GetQuery(), - } -} - -// Get Returns the value of a given attribute by name. If the attribute is -// a nested hash, nested values can be referenced using dot notation e.g. -// location.country -func (a *ItemAttributes) Get(name string) (interface{}, error) { - var result interface{} - - // Start at the beginning of the map, we will then traverse down as required - result = a.GetAttrStruct().AsMap() - - for _, section := range strings.Split(name, ".") { - // Check that the data we're using is in the supported format - var m map[string]interface{} - - m, isMap := result.(map[string]interface{}) - - if !isMap { - return nil, fmt.Errorf("attribute %v not found", name) - } - - v, keyExists := m[section] - - if keyExists { - result = v - } else { - return nil, fmt.Errorf("attribute %v not found", name) - } - } - - return result, nil -} - -// Set sets an attribute. Values are converted to structpb versions and an error -// will be returned if this fails. Note that this does *not* yet support -// dot notation e.g. location.country -func (a *ItemAttributes) Set(name string, value interface{}) error { - // Check to make sure that the pointer is not nil - if a == nil { - return errors.New("Set called on nil pointer") - } - - // Ensure that this interface will be able to be converted to a struct value - sanitizedValue := sanitizeInterface(value, false, DefaultTransforms) - structValue, err := structpb.NewValue(sanitizedValue) - if err != nil { - return err - } - - fields := a.GetAttrStruct().GetFields() - - fields[name] = structValue - - return nil -} - -// IsSingle returns true if this query can only return a single item. -func (q *Query) IsSingle() bool { - return q.GetMethod() == QueryMethod_GET && q.GetScope() != "*" && q.GetType() != "*" -} - -// Reference returns an SDP reference equivalent to this Query -func (q *Query) Reference() *Reference { - if q.IsSingle() { - return &Reference{ - Scope: q.GetScope(), - Type: q.GetType(), - UniqueAttributeValue: q.GetQuery(), - } - } - return &Reference{ - Scope: q.GetScope(), - Type: q.GetType(), - IsQuery: true, - Query: q.GetQuery(), - Method: q.GetMethod(), - } -} - -// Subject returns a NATS subject for all traffic relating to this query -func (q *Query) Subject() string { - return fmt.Sprintf("query.%v", q.GetUUIDParsed()) -} - -// TimeoutContext returns a context and cancel function representing the timeout -// for this request -func (q *Query) TimeoutContext(ctx context.Context) (context.Context, context.CancelFunc) { - // If there is no deadline, treat that as infinite - if q == nil || !q.GetDeadline().IsValid() { - return context.WithCancel(ctx) - } - - return context.WithDeadline(ctx, q.GetDeadline().AsTime()) -} - -// GetUUIDParsed returns this request's UUID. If there's an error parsing it, -// generates and stores a fresh one -func (r *Query) GetUUIDParsed() uuid.UUID { - if r == nil { - return uuid.UUID{} - } - // Extract and parse the UUID - reqUUID, uuidErr := uuid.FromBytes(r.GetUUID()) - if uuidErr != nil { - reqUUID = uuid.New() - r.UUID = reqUUID[:] - } - 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{ - NewItem: item, - }, - } -} - -func NewQueryResponseFromEdge(edge *Edge) *QueryResponse { - return &QueryResponse{ - ResponseType: &QueryResponse_Edge{ - Edge: edge, - }, - } -} - -func NewQueryResponseFromError(qe *QueryError) *QueryResponse { - return &QueryResponse{ - ResponseType: &QueryResponse_Error{ - Error: qe, - }, - } -} - -func NewQueryResponseFromResponse(r *Response) *QueryResponse { - return &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: r, - }, - } -} - -func (qr *QueryResponse) ToGatewayResponse() *GatewayResponse { - switch qr.GetResponseType().(type) { - case *QueryResponse_NewItem: - return &GatewayResponse{ - ResponseType: &GatewayResponse_NewItem{ - NewItem: qr.GetNewItem(), - }, - } - case *QueryResponse_Edge: - return &GatewayResponse{ - ResponseType: &GatewayResponse_NewEdge{ - NewEdge: qr.GetEdge(), - }, - } - case *QueryResponse_Error: - return &GatewayResponse{ - ResponseType: &GatewayResponse_QueryError{ - QueryError: qr.GetError(), - }, - } - case *QueryResponse_Response: - return &GatewayResponse{ - ResponseType: &GatewayResponse_QueryStatus{ - QueryStatus: qr.GetResponse().ToQueryStatus(), - }, - } - default: - panic(fmt.Sprintf("encountered unknown QueryResponse type: %T", qr)) - } -} - -func (x *CancelQuery) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (x *Expand) GetUUIDParsed() *uuid.UUID { - u, err := uuid.FromBytes(x.GetUUID()) - if err != nil { - return nil - } - return &u -} - -// AddDefaultTransforms adds the default transforms to a TransformMap -func AddDefaultTransforms(customTransforms TransformMap) TransformMap { - for k, v := range DefaultTransforms { - if _, ok := customTransforms[k]; !ok { - customTransforms[k] = v - } - } - return customTransforms -} - -// Converts to attributes using an additional set of custom transformers. These -// can be used to change the transform behaviour of known types to do things -// like redaction of sensitive data or simplification of complex types. -// -// For example this could be used to completely remove anything of type -// `Secret`: -// -// ```go -// -// TransformMap{ -// reflect.TypeOf(Secret{}): func(i interface{}) interface{} { -// // Remove it -// return "REDACTED" -// }, -// } -// -// ``` -// -// Note that you need to use `AddDefaultTransforms(TransformMap) TransformMap` -// to get sensible default transformations. -func ToAttributesCustom(m map[string]interface{}, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { - return toAttributes(m, sort, customTransforms) -} - -// Converts a map[string]interface{} to an ItemAttributes object, sorting all -// slices alphabetically.This should be used when the item doesn't contain array -// attributes that are explicitly sorted, especially if these are sometimes -// returned in a different order -func ToAttributesSorted(m map[string]interface{}) (*ItemAttributes, error) { - return toAttributes(m, true, DefaultTransforms) -} - -// ToAttributes Converts a map[string]interface{} to an ItemAttributes object -func ToAttributes(m map[string]interface{}) (*ItemAttributes, error) { - return toAttributes(m, false, DefaultTransforms) -} - -func toAttributes(m map[string]interface{}, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { - if m == nil { - return nil, nil - } - - var s map[string]*structpb.Value - var err error - - s = make(map[string]*structpb.Value) - - // Loop over the map - for k, v := range m { - sanitizedValue := sanitizeInterface(v, sort, customTransforms) - structValue, err := structpb.NewValue(sanitizedValue) - if err != nil { - return nil, err - } - - s[k] = structValue - } - - return &ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: s, - }, - }, err -} - -// ToAttributesViaJson Converts any struct to a set of attributes by marshalling -// to JSON and then back again. This is less performant than ToAttributes() but -// does save work when copying large structs to attributes in their entirety -func ToAttributesViaJson(v interface{}) (*ItemAttributes, error) { - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - - var m map[string]interface{} - - err = json.Unmarshal(b, &m) - if err != nil { - return nil, err - } - - return ToAttributes(m) -} - -// A function that transforms one data type into another that is compatible with -// protobuf. This is used to convert things like time.Time into a string -type TransformFunc func(interface{}) interface{} - -// A map of types to transform functions -type TransformMap map[reflect.Type]TransformFunc - -// The default transforms that are used when converting to attributes -var DefaultTransforms = TransformMap{ - // Time should be in RFC3339Nano format i.e. 2006-01-02T15:04:05.999999999Z07:00 - reflect.TypeOf(time.Time{}): func(i interface{}) interface{} { - return i.(time.Time).Format(time.RFC3339Nano) - }, - // Duration should be in string format - reflect.TypeOf(time.Duration(0)): func(i interface{}) interface{} { - return i.(time.Duration).String() - }, -} - -// sanitizeInterface Ensures that en interface is in a format that can be -// converted to a protobuf value. The structpb.ToValue() function expects things -// to be in one of the following formats: -// -// ╔════════════════════════╤════════════════════════════════════════════╗ -// ║ Go type │ Conversion ║ -// ╠════════════════════════╪════════════════════════════════════════════╣ -// ║ nil │ stored as NullValue ║ -// ║ bool │ stored as BoolValue ║ -// ║ int, int32, int64 │ stored as NumberValue ║ -// ║ uint, uint32, uint64 │ stored as NumberValue ║ -// ║ float32, float64 │ stored as NumberValue ║ -// ║ string │ stored as StringValue; must be valid UTF-8 ║ -// ║ []byte │ stored as StringValue; base64-encoded ║ -// ║ map[string]interface{} │ stored as StructValue ║ -// ║ []interface{} │ stored as ListValue ║ -// ╚════════════════════════╧════════════════════════════════════════════╝ -// -// However this means that a data type like []string won't work, despite the -// function being perfectly able to represent it in a protobuf struct. This -// function does its best to example the available data type to ensure that as -// long as the data can in theory be represented by a protobuf struct, the -// conversion will work. -func sanitizeInterface(i interface{}, sortArrays bool, customTransforms TransformMap) interface{} { - if i == nil { - return nil - } - - v := reflect.ValueOf(i) - t := v.Type() - - // Use the transform for this specific type if it exists - if tFunc, ok := customTransforms[t]; ok { - // Reset the value and type to the transformed value. This means that - // even if the function returns something bad, we will then transform it - i = tFunc(i) - - if i == nil { - return nil - } - - v = reflect.ValueOf(i) - t = v.Type() - } - - switch v.Kind() { //nolint:exhaustive // we fall through to the default case - case reflect.Bool: - return v.Bool() - case reflect.Int: - return v.Int() - case reflect.Int8: - return v.Int() - case reflect.Int16: - return v.Int() - case reflect.Int32: - return v.Int() - case reflect.Int64: - return v.Int() - case reflect.Uint: - return v.Uint() - case reflect.Uint8: - return v.Uint() - case reflect.Uint16: - return v.Uint() - case reflect.Uint32: - return v.Uint() - case reflect.Uint64: - return v.Uint() - case reflect.Float32: - return v.Float() - case reflect.Float64: - return v.Float() - case reflect.String: - return fmt.Sprint(v) - case reflect.Array, reflect.Slice: - // We need to check the type of each element in the array and do - // conversion on that - - // returnSlice Returns the array in the format that protobuf can deal with - var returnSlice []interface{} - - returnSlice = make([]interface{}, v.Len()) - - for i := range v.Len() { - returnSlice[i] = sanitizeInterface(v.Index(i).Interface(), sortArrays, customTransforms) - } - - if sortArrays { - sortInterfaceArray(returnSlice) - } - - return returnSlice - case reflect.Map: - var returnMap map[string]interface{} - - returnMap = make(map[string]interface{}) - - for _, mapKey := range v.MapKeys() { - // Convert the key to a string - stringKey := fmt.Sprint(mapKey.Interface()) - - // Convert the value to a compatible interface - value := sanitizeInterface(v.MapIndex(mapKey).Interface(), sortArrays, customTransforms) - returnMap[stringKey] = value - } - - return returnMap - case reflect.Struct: - // In the case of a struct we basically want to turn it into a - // map[string]interface{} - var returnMap map[string]interface{} - - returnMap = make(map[string]interface{}) - - // Range over fields - n := t.NumField() - for i := range n { - field := t.Field(i) - - if field.PkgPath != "" { - // If this has a PkgPath then it is an un-exported fiend and - // should be ignored - continue - } - - // Get the zero value for this field - zeroValue := reflect.Zero(field.Type).Interface() - fieldValue := v.Field(i).Interface() - - // Check if the field is it's nil value - // Check if there actually was a field with that name - if !reflect.DeepEqual(fieldValue, zeroValue) { - returnMap[field.Name] = fieldValue - } - } - - return sanitizeInterface(returnMap, sortArrays, customTransforms) - case reflect.Ptr: - // Get the zero value for this field - zero := reflect.Zero(t) - - // Check if the field is it's nil value - if reflect.DeepEqual(v, zero) { - return nil - } - - return sanitizeInterface(v.Elem().Interface(), sortArrays, customTransforms) - default: - // If we don't recognize the type then we need to see what the - // underlying type is and see if we can convert that - return i - } -} - -// Sorts an interface slice by converting each item to a string and sorting -// these strings -func sortInterfaceArray(input []interface{}) { - sort.Slice(input, func(i, j int) bool { - return fmt.Sprint(input[i]) < fmt.Sprint(input[j]) - }) -} - -// HashSum is a function that takes a byte array and returns a 12 character hash for use in neo4j -func HashSum(b []byte) string { - var paddedEncoding *base32.Encoding - var unpaddedEncoding *base32.Encoding - - shaSum := sha256.Sum256(b) - - // We need to specify a custom encoding here since dGraph has fairly strict - // requirements about what name a variable can have - paddedEncoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyzABCDEF") - - // We also can't have padding since "=" is not allowed in variable names - unpaddedEncoding = paddedEncoding.WithPadding(base32.NoPadding) - - return unpaddedEncoding.EncodeToString(shaSum[:11]) -} diff --git a/sdp-go/items.pb.go b/sdp-go/items.pb.go deleted file mode 100644 index ca8305fd..00000000 --- a/sdp-go/items.pb.go +++ /dev/null @@ -1,1840 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: items.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - structpb "google.golang.org/protobuf/types/known/structpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Represents the health of something, the meaning of each state may depend on -// the context in which it is used but should be reasonably obvious -type Health int32 - -const ( - Health_HEALTH_UNKNOWN Health = 0 // The health could not be determined - Health_HEALTH_OK Health = 1 // Functioning normally - Health_HEALTH_WARNING Health = 2 // Functioning, but degraded - Health_HEALTH_ERROR Health = 3 // Not functioning - Health_HEALTH_PENDING Health = 4 // Health state is transitioning, such as when something is first provisioned -) - -// Enum value maps for Health. -var ( - Health_name = map[int32]string{ - 0: "HEALTH_UNKNOWN", - 1: "HEALTH_OK", - 2: "HEALTH_WARNING", - 3: "HEALTH_ERROR", - 4: "HEALTH_PENDING", - } - Health_value = map[string]int32{ - "HEALTH_UNKNOWN": 0, - "HEALTH_OK": 1, - "HEALTH_WARNING": 2, - "HEALTH_ERROR": 3, - "HEALTH_PENDING": 4, - } -) - -func (x Health) Enum() *Health { - p := new(Health) - *p = x - return p -} - -func (x Health) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Health) Descriptor() protoreflect.EnumDescriptor { - return file_items_proto_enumTypes[0].Descriptor() -} - -func (Health) Type() protoreflect.EnumType { - return &file_items_proto_enumTypes[0] -} - -func (x Health) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Health.Descriptor instead. -func (Health) EnumDescriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{0} -} - -// QueryMethod represents the available query methods. The details of these -// methods are: -// -// GET: This takes a single unique query and should only return a single item. -// -// If an item matching the parameter passed doesn't exist the server should -// fail -// -// LIST: This takes no query (or ignores it) and should return all items that it -// -// can find -// -// SEARCH: This takes a non-unique query which is designed to be used as a -// -// search term. It should return some number of items (or zero) which -// match the query -type QueryMethod int32 - -const ( - QueryMethod_GET QueryMethod = 0 - QueryMethod_LIST QueryMethod = 1 - QueryMethod_SEARCH QueryMethod = 2 -) - -// Enum value maps for QueryMethod. -var ( - QueryMethod_name = map[int32]string{ - 0: "GET", - 1: "LIST", - 2: "SEARCH", - } - QueryMethod_value = map[string]int32{ - "GET": 0, - "LIST": 1, - "SEARCH": 2, - } -) - -func (x QueryMethod) Enum() *QueryMethod { - p := new(QueryMethod) - *p = x - return p -} - -func (x QueryMethod) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (QueryMethod) Descriptor() protoreflect.EnumDescriptor { - return file_items_proto_enumTypes[1].Descriptor() -} - -func (QueryMethod) Type() protoreflect.EnumType { - return &file_items_proto_enumTypes[1] -} - -func (x QueryMethod) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use QueryMethod.Descriptor instead. -func (QueryMethod) EnumDescriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{1} -} - -// The error type. Any types in here will be gracefully handled unless the -// type os "OTHER" -type QueryStatus_Status int32 - -const ( - // the status has not been specified - QueryStatus_UNSPECIFIED QueryStatus_Status = 0 - // the query has been started - QueryStatus_STARTED QueryStatus_Status = 1 - // the query has been cancelled. - // This is a final state. - QueryStatus_CANCELLED QueryStatus_Status = 3 - // the query has finished with an error status. expect a separate QueryError describing that. - // This is a final state. - // TODO: fold the error details into this message - QueryStatus_ERRORED QueryStatus_Status = 4 - // The query has finished and all results have been sent over the wire - // This is a final state. - QueryStatus_FINISHED QueryStatus_Status = 5 -) - -// Enum value maps for QueryStatus_Status. -var ( - QueryStatus_Status_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "STARTED", - 3: "CANCELLED", - 4: "ERRORED", - 5: "FINISHED", - } - QueryStatus_Status_value = map[string]int32{ - "UNSPECIFIED": 0, - "STARTED": 1, - "CANCELLED": 3, - "ERRORED": 4, - "FINISHED": 5, - } -) - -func (x QueryStatus_Status) Enum() *QueryStatus_Status { - p := new(QueryStatus_Status) - *p = x - return p -} - -func (x QueryStatus_Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (QueryStatus_Status) Descriptor() protoreflect.EnumDescriptor { - return file_items_proto_enumTypes[2].Descriptor() -} - -func (QueryStatus_Status) Type() protoreflect.EnumType { - return &file_items_proto_enumTypes[2] -} - -func (x QueryStatus_Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use QueryStatus_Status.Descriptor instead. -func (QueryStatus_Status) EnumDescriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{10, 0} -} - -// The error type. Any types in here will be gracefully handled unless the -// type os "OTHER" -type QueryError_ErrorType int32 - -const ( - // This should be used of all other failure modes, such as timeouts, - // unexpected failures when querying state, permissions errors etc. Errors - // that return this type should not be cached as the error may be transient. - QueryError_OTHER QueryError_ErrorType = 0 - // NOTFOUND means that the item was not found. This is only returned as the - // result of a GET query since all other queries would return an empty - // list instead - QueryError_NOTFOUND QueryError_ErrorType = 1 - // NOSCOPE means that the item was not found because we don't have - // access to the requested scope. This should not be interpreted as "The - // item doesn't exist" (as with a NOTFOUND error) but rather as "We can't - // tell you whether or not the item exists" - QueryError_NOSCOPE QueryError_ErrorType = 2 - // TIMEOUT means that the source times out when trying to query the item. - // The timeout is provided in the original query - QueryError_TIMEOUT QueryError_ErrorType = 3 -) - -// Enum value maps for QueryError_ErrorType. -var ( - QueryError_ErrorType_name = map[int32]string{ - 0: "OTHER", - 1: "NOTFOUND", - 2: "NOSCOPE", - 3: "TIMEOUT", - } - QueryError_ErrorType_value = map[string]int32{ - "OTHER": 0, - "NOTFOUND": 1, - "NOSCOPE": 2, - "TIMEOUT": 3, - } -) - -func (x QueryError_ErrorType) Enum() *QueryError_ErrorType { - p := new(QueryError_ErrorType) - *p = x - return p -} - -func (x QueryError_ErrorType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (QueryError_ErrorType) Descriptor() protoreflect.EnumDescriptor { - return file_items_proto_enumTypes[3].Descriptor() -} - -func (QueryError_ErrorType) Type() protoreflect.EnumType { - return &file_items_proto_enumTypes[3] -} - -func (x QueryError_ErrorType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use QueryError_ErrorType.Descriptor instead. -func (QueryError_ErrorType) EnumDescriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{11, 0} -} - -// This message stores additional information on Edges (and edge-like constructs) to determine how configuration changes can impact -// the linked items. -// -// Blast Propagation options: -// -// |-------|-------|---------------------- -// | in | out | result -// |-------|-------|---------------------- -// | false | false | no change in any item can affect the other -// | false | true | a change to this item can affect its linked items -// | | | example: a change to an EC2 instance can affect its DNS name (in the sense that other items depending on that DNS name will see the impact) -// | true | false | a change to linked items can affect this item -// | | | example: changing the KMS key used by a DynamoDB table can impact the table, but no change to the table can impact the key -// | true | true | changes on both sides of the link can affect the other -// | | | example: changes to both EC2 Instances and their volumes can affect the other side of the relation. -type BlastPropagation struct { - state protoimpl.MessageState `protogen:"open.v1"` - // is true if changes on linked items can affect this item - In bool `protobuf:"varint,1,opt,name=in,proto3" json:"in,omitempty"` - // is true if changes on this item can affect linked items - Out bool `protobuf:"varint,2,opt,name=out,proto3" json:"out,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BlastPropagation) Reset() { - *x = BlastPropagation{} - mi := &file_items_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BlastPropagation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BlastPropagation) ProtoMessage() {} - -func (x *BlastPropagation) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[0] - 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 BlastPropagation.ProtoReflect.Descriptor instead. -func (*BlastPropagation) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{0} -} - -func (x *BlastPropagation) GetIn() bool { - if x != nil { - return x.In - } - return false -} - -func (x *BlastPropagation) GetOut() bool { - if x != nil { - return x.Out - } - return false -} - -// An annotated query to indicate potential linked items. -type LinkedItemQuery struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the query that would find linked items - Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LinkedItemQuery) Reset() { - *x = LinkedItemQuery{} - mi := &file_items_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LinkedItemQuery) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LinkedItemQuery) ProtoMessage() {} - -func (x *LinkedItemQuery) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[1] - 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 LinkedItemQuery.ProtoReflect.Descriptor instead. -func (*LinkedItemQuery) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{1} -} - -func (x *LinkedItemQuery) GetQuery() *Query { - if x != nil { - return x.Query - } - return nil -} - -func (x *LinkedItemQuery) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - -// An annotated reference to list linked items. -type LinkedItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the linked item - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LinkedItem) Reset() { - *x = LinkedItem{} - mi := &file_items_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LinkedItem) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LinkedItem) ProtoMessage() {} - -func (x *LinkedItem) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[2] - 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 LinkedItem.ProtoReflect.Descriptor instead. -func (*LinkedItem) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{2} -} - -func (x *LinkedItem) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *LinkedItem) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - -// This is the same as Item within the package with a couple of exceptions, no -// real reason why this whole thing couldn't be modelled in protobuf though if -// required. Just need to decide what if anything should remain private -type Item struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - UniqueAttribute string `protobuf:"bytes,2,opt,name=uniqueAttribute,proto3" json:"uniqueAttribute,omitempty"` - Attributes *ItemAttributes `protobuf:"bytes,3,opt,name=attributes,proto3" json:"attributes,omitempty"` - Metadata *Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` - // The scope within which the item is unique. Item uniqueness is determined - // by the combination of type and uniqueAttribute value. However it is - // possible for the same item to exist in many scopes. There is not formal - // definition for what a scope should be other than the fact that it should - // be somewhat descriptive and should ensure item uniqueness - Scope string `protobuf:"bytes,5,opt,name=scope,proto3" json:"scope,omitempty"` - // Not all items will have relatedItems we are are using a two byte - // integer to save one byte integers for more common things - LinkedItemQueries []*LinkedItemQuery `protobuf:"bytes,16,rep,name=linkedItemQueries,proto3" json:"linkedItemQueries,omitempty"` - // Linked items - LinkedItems []*LinkedItem `protobuf:"bytes,17,rep,name=linkedItems,proto3" json:"linkedItems,omitempty"` - // (optional) Represents the health of the item. Only items that have a - // clearly relevant health attribute should return a value for health - Health *Health `protobuf:"varint,18,opt,name=health,proto3,enum=Health,oneof" json:"health,omitempty"` - // Arbitrary key-value pairs that can be used to store additional information. - // These tags are retrieved from the source and map to the target's definition - // of a tag (e.g. AWS tags, Kubernetes labels, etc.) - Tags map[string]string `protobuf:"bytes,19,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // The available log streams for this item, if any. Use the Logs service to - // access the actual contents. - LogStreams []*LogStreamDetails `protobuf:"bytes,20,rep,name=logStreams,proto3" json:"logStreams,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Item) Reset() { - *x = Item{} - mi := &file_items_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Item) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Item) ProtoMessage() {} - -func (x *Item) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[3] - 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 Item.ProtoReflect.Descriptor instead. -func (*Item) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{3} -} - -func (x *Item) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *Item) GetUniqueAttribute() string { - if x != nil { - return x.UniqueAttribute - } - return "" -} - -func (x *Item) GetAttributes() *ItemAttributes { - if x != nil { - return x.Attributes - } - return nil -} - -func (x *Item) GetMetadata() *Metadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Item) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *Item) GetLinkedItemQueries() []*LinkedItemQuery { - if x != nil { - return x.LinkedItemQueries - } - return nil -} - -func (x *Item) GetLinkedItems() []*LinkedItem { - if x != nil { - return x.LinkedItems - } - return nil -} - -func (x *Item) GetHealth() Health { - if x != nil && x.Health != nil { - return *x.Health - } - return Health_HEALTH_UNKNOWN -} - -func (x *Item) GetTags() map[string]string { - if x != nil { - return x.Tags - } - return nil -} - -func (x *Item) GetLogStreams() []*LogStreamDetails { - if x != nil { - return x.LogStreams - } - return nil -} - -// ItemAttributes represents the known attributes for an item. These are likely -// to be common to a given type, but even this is not guaranteed. All items must -// have at least one attribute however as it needs something to uniquely -// identify it -type ItemAttributes struct { - state protoimpl.MessageState `protogen:"open.v1"` - AttrStruct *structpb.Struct `protobuf:"bytes,1,opt,name=attrStruct,proto3" json:"attrStruct,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ItemAttributes) Reset() { - *x = ItemAttributes{} - mi := &file_items_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ItemAttributes) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ItemAttributes) ProtoMessage() {} - -func (x *ItemAttributes) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[4] - 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 ItemAttributes.ProtoReflect.Descriptor instead. -func (*ItemAttributes) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{4} -} - -func (x *ItemAttributes) GetAttrStruct() *structpb.Struct { - if x != nil { - return x.AttrStruct - } - return nil -} - -// Metadata about the item. Where it came from, how long it took, etc. -type Metadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // This is the name of the source that was used to find the item. - SourceName string `protobuf:"bytes,2,opt,name=sourceName,proto3" json:"sourceName,omitempty"` - // The query that caused this item to be found. This is for gateway-internal use and will not be exposed to the frontend. - SourceQuery *Query `protobuf:"bytes,3,opt,name=sourceQuery,proto3" json:"sourceQuery,omitempty"` - // The time that the item was found - Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // How long the source took to execute in total when processing the Query. - // - // (deprecated) This is no longer sent as streaming responses make this metric - // impossible to calculate on a per-item basis - // - // Deprecated: Marked as deprecated in items.proto. - SourceDuration *durationpb.Duration `protobuf:"bytes,5,opt,name=sourceDuration,proto3" json:"sourceDuration,omitempty"` - // How long the source took to execute per item when processing the - // Query - // - // (deprecated) This is no longer sent - // - // Deprecated: Marked as deprecated in items.proto. - SourceDurationPerItem *durationpb.Duration `protobuf:"bytes,6,opt,name=sourceDurationPerItem,proto3" json:"sourceDurationPerItem,omitempty"` - // Whether the item should be hidden/ignored by user-facing things such as - // GUIs and databases. - // - // Some types of items are only relevant in calculating higher-layer - // abstractions and are therefore always hidden. A good example of this would - // be the output of a command. This could be used by a remote source to gather - // information, but we don't actually want to show the user all the commands - // that were run, just the final item returned by the source - Hidden bool `protobuf:"varint,7,opt,name=hidden,proto3" json:"hidden,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Metadata) Reset() { - *x = Metadata{} - mi := &file_items_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Metadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Metadata) ProtoMessage() {} - -func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[5] - 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 Metadata.ProtoReflect.Descriptor instead. -func (*Metadata) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{5} -} - -func (x *Metadata) GetSourceName() string { - if x != nil { - return x.SourceName - } - return "" -} - -func (x *Metadata) GetSourceQuery() *Query { - if x != nil { - return x.SourceQuery - } - return nil -} - -func (x *Metadata) GetTimestamp() *timestamppb.Timestamp { - if x != nil { - return x.Timestamp - } - return nil -} - -// Deprecated: Marked as deprecated in items.proto. -func (x *Metadata) GetSourceDuration() *durationpb.Duration { - if x != nil { - return x.SourceDuration - } - return nil -} - -// Deprecated: Marked as deprecated in items.proto. -func (x *Metadata) GetSourceDurationPerItem() *durationpb.Duration { - if x != nil { - return x.SourceDurationPerItem - } - return nil -} - -func (x *Metadata) GetHidden() bool { - if x != nil { - return x.Hidden - } - return false -} - -// This is a list of items, like a List() would return -type Items struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Items) Reset() { - *x = Items{} - mi := &file_items_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Items) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Items) ProtoMessage() {} - -func (x *Items) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[6] - 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 Items.ProtoReflect.Descriptor instead. -func (*Items) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{6} -} - -func (x *Items) GetItems() []*Item { - if x != nil { - return x.Items - } - return nil -} - -// describes the details of a Log Stream for an item -type LogStreamDetails struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The descriptive name for display purposes - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // the source scope for this log stream. Has to be a specific scope, not - // wildcarded. - Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` - // The query that should pe passed back to the upstream - // API to get log lines from this stream - Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LogStreamDetails) Reset() { - *x = LogStreamDetails{} - mi := &file_items_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LogStreamDetails) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LogStreamDetails) ProtoMessage() {} - -func (x *LogStreamDetails) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[7] - 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 LogStreamDetails.ProtoReflect.Descriptor instead. -func (*LogStreamDetails) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{7} -} - -func (x *LogStreamDetails) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *LogStreamDetails) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *LogStreamDetails) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -// Query represents a query for an item or a list of items. -type Query struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The type of item to search for. "*" means all types - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - // Which method to use when looking for it - Method QueryMethod `protobuf:"varint,2,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` - // What query should be passed to that method - Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` - // Defines how this query should behave when finding new items - RecursionBehaviour *Query_RecursionBehaviour `protobuf:"bytes,4,opt,name=recursionBehaviour,proto3" json:"recursionBehaviour,omitempty"` - // The scope for which we are requesting. To query all scopes use the the - // wildcard '*' - Scope string `protobuf:"bytes,5,opt,name=scope,proto3" json:"scope,omitempty"` - // Whether to ignore the cache and execute the query regardless. - // - // By default sources will implement some level of caching, this is - // particularly important for linked items as a single query with a large link - // depth may result in the same item being queried many times as links are - // resolved and more and more items link to each other. However if required - // this caching can be turned off using this parameter - IgnoreCache bool `protobuf:"varint,6,opt,name=ignoreCache,proto3" json:"ignoreCache,omitempty"` - // A UUID to uniquely identify the query. This should be stored by the - // requester as it will be needed later if the requester wants to cancel a - // query. It should be stored as 128 bytes, as opposed to the textual - // representation - UUID []byte `protobuf:"bytes,7,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The deadline for this query. When the deadline elapses, results become - // irrelevant for the sender and any processing can stop. The deadline gets - // propagated to all related queries (e.g. for linked items) and processes. - // Note: there is currently a migration going on from timeouts to durations, - // so depending on which service is hit, either one is evaluated. - Deadline *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=deadline,proto3" json:"deadline,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Query) Reset() { - *x = Query{} - mi := &file_items_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Query) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Query) ProtoMessage() {} - -func (x *Query) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[8] - 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 Query.ProtoReflect.Descriptor instead. -func (*Query) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{8} -} - -func (x *Query) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *Query) GetMethod() QueryMethod { - if x != nil { - return x.Method - } - return QueryMethod_GET -} - -func (x *Query) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *Query) GetRecursionBehaviour() *Query_RecursionBehaviour { - if x != nil { - return x.RecursionBehaviour - } - return nil -} - -func (x *Query) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *Query) GetIgnoreCache() bool { - if x != nil { - return x.IgnoreCache - } - return false -} - -func (x *Query) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *Query) GetDeadline() *timestamppb.Timestamp { - if x != nil { - return x.Deadline - } - return nil -} - -type QueryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to ResponseType: - // - // *QueryResponse_NewItem - // *QueryResponse_Response - // *QueryResponse_Error - // *QueryResponse_Edge - ResponseType isQueryResponse_ResponseType `protobuf_oneof:"response_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *QueryResponse) Reset() { - *x = QueryResponse{} - mi := &file_items_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *QueryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryResponse) ProtoMessage() {} - -func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[9] - 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 QueryResponse.ProtoReflect.Descriptor instead. -func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{9} -} - -func (x *QueryResponse) GetResponseType() isQueryResponse_ResponseType { - if x != nil { - return x.ResponseType - } - return nil -} - -func (x *QueryResponse) GetNewItem() *Item { - if x != nil { - if x, ok := x.ResponseType.(*QueryResponse_NewItem); ok { - return x.NewItem - } - } - return nil -} - -func (x *QueryResponse) GetResponse() *Response { - if x != nil { - if x, ok := x.ResponseType.(*QueryResponse_Response); ok { - return x.Response - } - } - return nil -} - -func (x *QueryResponse) GetError() *QueryError { - if x != nil { - if x, ok := x.ResponseType.(*QueryResponse_Error); ok { - return x.Error - } - } - return nil -} - -func (x *QueryResponse) GetEdge() *Edge { - if x != nil { - if x, ok := x.ResponseType.(*QueryResponse_Edge); ok { - return x.Edge - } - } - return nil -} - -type isQueryResponse_ResponseType interface { - isQueryResponse_ResponseType() -} - -type QueryResponse_NewItem struct { - NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered -} - -type QueryResponse_Response struct { - Response *Response `protobuf:"bytes,3,opt,name=response,proto3,oneof"` // Status update -} - -type QueryResponse_Error struct { - Error *QueryError `protobuf:"bytes,4,opt,name=error,proto3,oneof"` // An error has been encountered -} - -type QueryResponse_Edge struct { - Edge *Edge `protobuf:"bytes,5,opt,name=edge,proto3,oneof"` // a link between items/queries -} - -func (*QueryResponse_NewItem) isQueryResponse_ResponseType() {} - -func (*QueryResponse_Response) isQueryResponse_ResponseType() {} - -func (*QueryResponse_Error) isQueryResponse_ResponseType() {} - -func (*QueryResponse_Edge) isQueryResponse_ResponseType() {} - -// QueryStatus informs the client of status updates of all queries running in this session. -type QueryStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - // UUID of the query - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - Status QueryStatus_Status `protobuf:"varint,2,opt,name=status,proto3,enum=QueryStatus_Status" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *QueryStatus) Reset() { - *x = QueryStatus{} - mi := &file_items_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *QueryStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryStatus) ProtoMessage() {} - -func (x *QueryStatus) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[10] - 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 QueryStatus.ProtoReflect.Descriptor instead. -func (*QueryStatus) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{10} -} - -func (x *QueryStatus) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *QueryStatus) GetStatus() QueryStatus_Status { - if x != nil { - return x.Status - } - return QueryStatus_UNSPECIFIED -} - -// QueryError is sent back when an item query fails -type QueryError struct { - state protoimpl.MessageState `protogen:"open.v1"` - // UUID of the item query that this response is in relation to (in binary - // format) - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - ErrorType QueryError_ErrorType `protobuf:"varint,2,opt,name=errorType,proto3,enum=QueryError_ErrorType" json:"errorType,omitempty"` - // The string contents of the error - ErrorString string `protobuf:"bytes,3,opt,name=errorString,proto3" json:"errorString,omitempty"` - // The scope from which the error was raised - Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"` - // The name of the source which raised the error (if relevant) - SourceName string `protobuf:"bytes,5,opt,name=sourceName,proto3" json:"sourceName,omitempty"` - // The type of item that we were looking for at the time of the error - ItemType string `protobuf:"bytes,6,opt,name=itemType,proto3" json:"itemType,omitempty"` - // The name of the responder that this error was raised from - ResponderName string `protobuf:"bytes,7,opt,name=responderName,proto3" json:"responderName,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *QueryError) Reset() { - *x = QueryError{} - mi := &file_items_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *QueryError) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*QueryError) ProtoMessage() {} - -func (x *QueryError) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[11] - 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 QueryError.ProtoReflect.Descriptor instead. -func (*QueryError) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{11} -} - -func (x *QueryError) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *QueryError) GetErrorType() QueryError_ErrorType { - if x != nil { - return x.ErrorType - } - return QueryError_OTHER -} - -func (x *QueryError) GetErrorString() string { - if x != nil { - return x.ErrorString - } - return "" -} - -func (x *QueryError) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *QueryError) GetSourceName() string { - if x != nil { - return x.SourceName - } - return "" -} - -func (x *QueryError) GetItemType() string { - if x != nil { - return x.ItemType - } - return "" -} - -func (x *QueryError) GetResponderName() string { - if x != nil { - return x.ResponderName - } - return "" -} - -// The message signals that the Query with the corresponding UUID should -// be cancelled. Work should stop immediately, and a final response should be -// sent with a state of CANCELLED to acknowledge that the query has ended due -// to a cancellation -type CancelQuery struct { - state protoimpl.MessageState `protogen:"open.v1"` - // UUID of the Query to cancel - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CancelQuery) Reset() { - *x = CancelQuery{} - mi := &file_items_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CancelQuery) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CancelQuery) ProtoMessage() {} - -func (x *CancelQuery) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[12] - 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 CancelQuery.ProtoReflect.Descriptor instead. -func (*CancelQuery) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{12} -} - -func (x *CancelQuery) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -// This requests that the gateway "expands" an item. This involves executing all -// linked item queries within the session and sending the results to the -// client. It is recommended that this be used rather than simply sending each -// linked item request. Using this request type allows the Gateway to save the -// session more intelligently so that it can be bookmarked and used later. -// "Expanding" an item will mean an item always acts the same, even if its -// linked item queries have changed -type Expand struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The item that should be expanded - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // How many levels of expansion should be run - LinkDepth uint32 `protobuf:"varint,2,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` - // A UUID to uniquely identify the request. This should be stored by the - // requester as it will be needed later if the requester wants to cancel a - // request. It should be stored as 128 bytes, as opposed to the textual - // representation - UUID []byte `protobuf:"bytes,3,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The time at which the gateway should stop processing the queries spawned by this request - Deadline *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deadline,proto3" json:"deadline,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Expand) Reset() { - *x = Expand{} - mi := &file_items_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Expand) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Expand) ProtoMessage() {} - -func (x *Expand) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Expand.ProtoReflect.Descriptor instead. -func (*Expand) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{13} -} - -func (x *Expand) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *Expand) GetLinkDepth() uint32 { - if x != nil { - return x.LinkDepth - } - return 0 -} - -func (x *Expand) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *Expand) GetDeadline() *timestamppb.Timestamp { - if x != nil { - return x.Deadline - } - return nil -} - -// Reference to an item -// -// The uniqueness of an item is determined by the combination of: -// -// - Type -// - UniqueAttributeValue -// - Scope -type Reference struct { - state protoimpl.MessageState `protogen:"open.v1"` - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` - Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` - IsQuery bool `protobuf:"varint,4,opt,name=isQuery,proto3" json:"isQuery,omitempty"` - Query string `protobuf:"bytes,5,opt,name=query,proto3" json:"query,omitempty"` - Method QueryMethod `protobuf:"varint,6,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Reference) Reset() { - *x = Reference{} - mi := &file_items_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Reference) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Reference) ProtoMessage() {} - -func (x *Reference) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Reference.ProtoReflect.Descriptor instead. -func (*Reference) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{14} -} - -func (x *Reference) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *Reference) GetUniqueAttributeValue() string { - if x != nil { - return x.UniqueAttributeValue - } - return "" -} - -func (x *Reference) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *Reference) GetIsQuery() bool { - if x != nil { - return x.IsQuery - } - return false -} - -func (x *Reference) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *Reference) GetMethod() QueryMethod { - if x != nil { - return x.Method - } - return QueryMethod_GET -} - -// Edge represents a link between two items. The `to` Reference can be a query -// that will be unrolled by the gateway during query processing. Clients are -// guaranteed that edges are only sent after the referenced items. -type Edge struct { - state protoimpl.MessageState `protogen:"open.v1"` - From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` - To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` - BlastPropagation *BlastPropagation `protobuf:"bytes,3,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Edge) Reset() { - *x = Edge{} - mi := &file_items_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Edge) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Edge) ProtoMessage() {} - -func (x *Edge) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[15] - 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 Edge.ProtoReflect.Descriptor instead. -func (*Edge) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{15} -} - -func (x *Edge) GetFrom() *Reference { - if x != nil { - return x.From - } - return nil -} - -func (x *Edge) GetTo() *Reference { - if x != nil { - return x.To - } - return nil -} - -func (x *Edge) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - -// Defines how this query should behave when finding new items -type Query_RecursionBehaviour struct { - state protoimpl.MessageState `protogen:"open.v1"` - // How deeply to link items. A value of 0 will mean that items are not linked. - // To resolve linked items "infinitely" simply set this to a high number, with - // the highest being 4,294,967,295. While this isn't truly *infinite*, chances - // are that it is effectively the same, think six degrees of separation etc. - LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` - // set to true to only follow links that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,2,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Query_RecursionBehaviour) Reset() { - *x = Query_RecursionBehaviour{} - mi := &file_items_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Query_RecursionBehaviour) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Query_RecursionBehaviour) ProtoMessage() {} - -func (x *Query_RecursionBehaviour) ProtoReflect() protoreflect.Message { - mi := &file_items_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Query_RecursionBehaviour.ProtoReflect.Descriptor instead. -func (*Query_RecursionBehaviour) Descriptor() ([]byte, []int) { - return file_items_proto_rawDescGZIP(), []int{8, 0} -} - -func (x *Query_RecursionBehaviour) GetLinkDepth() uint32 { - if x != nil { - return x.LinkDepth - } - return 0 -} - -func (x *Query_RecursionBehaviour) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - -var File_items_proto protoreflect.FileDescriptor - -const file_items_proto_rawDesc = "" + - "\n" + - "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"4\n" + - "\x10BlastPropagation\x12\x0e\n" + - "\x02in\x18\x01 \x01(\bR\x02in\x12\x10\n" + - "\x03out\x18\x02 \x01(\bR\x03out\"n\n" + - "\x0fLinkedItemQuery\x12\x1c\n" + - "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05query\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"k\n" + - "\n" + - "LinkedItem\x12\x1e\n" + - "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"\xe3\x03\n" + - "\x04Item\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x12(\n" + - "\x0funiqueAttribute\x18\x02 \x01(\tR\x0funiqueAttribute\x12/\n" + - "\n" + - "attributes\x18\x03 \x01(\v2\x0f.ItemAttributesR\n" + - "attributes\x12%\n" + - "\bmetadata\x18\x04 \x01(\v2\t.MetadataR\bmetadata\x12\x14\n" + - "\x05scope\x18\x05 \x01(\tR\x05scope\x12>\n" + - "\x11linkedItemQueries\x18\x10 \x03(\v2\x10.LinkedItemQueryR\x11linkedItemQueries\x12-\n" + - "\vlinkedItems\x18\x11 \x03(\v2\v.LinkedItemR\vlinkedItems\x12$\n" + - "\x06health\x18\x12 \x01(\x0e2\a.HealthH\x00R\x06health\x88\x01\x01\x12#\n" + - "\x04tags\x18\x13 \x03(\v2\x0f.Item.TagsEntryR\x04tags\x121\n" + - "\n" + - "logStreams\x18\x14 \x03(\v2\x11.LogStreamDetailsR\n" + - "logStreams\x1a7\n" + - "\tTagsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + - "\a_health\"I\n" + - "\x0eItemAttributes\x127\n" + - "\n" + - "attrStruct\x18\x01 \x01(\v2\x17.google.protobuf.StructR\n" + - "attrStruct\"\xc2\x02\n" + - "\bMetadata\x12\x1e\n" + - "\n" + - "sourceName\x18\x02 \x01(\tR\n" + - "sourceName\x12(\n" + - "\vsourceQuery\x18\x03 \x01(\v2\x06.QueryR\vsourceQuery\x128\n" + - "\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12E\n" + - "\x0esourceDuration\x18\x05 \x01(\v2\x19.google.protobuf.DurationB\x02\x18\x01R\x0esourceDuration\x12S\n" + - "\x15sourceDurationPerItem\x18\x06 \x01(\v2\x19.google.protobuf.DurationB\x02\x18\x01R\x15sourceDurationPerItem\x12\x16\n" + - "\x06hidden\x18\a \x01(\bR\x06hidden\"$\n" + - "\x05Items\x12\x1b\n" + - "\x05items\x18\x01 \x03(\v2\x05.ItemR\x05items\"R\n" + - "\x10LogStreamDetails\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05scope\x18\x02 \x01(\tR\x05scope\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\"\xa0\x03\n" + - "\x05Query\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + - "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\x12I\n" + - "\x12recursionBehaviour\x18\x04 \x01(\v2\x19.Query.RecursionBehaviourR\x12recursionBehaviour\x12\x14\n" + - "\x05scope\x18\x05 \x01(\tR\x05scope\x12 \n" + - "\vignoreCache\x18\x06 \x01(\bR\vignoreCache\x12\x12\n" + - "\x04UUID\x18\a \x01(\fR\x04UUID\x126\n" + - "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1ar\n" + - "\x12RecursionBehaviour\x12\x1c\n" + - "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepth\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x02 \x01(\bR\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + - "\rQueryResponse\x12!\n" + - "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12'\n" + - "\bresponse\x18\x03 \x01(\v2\t.ResponseH\x00R\bresponse\x12#\n" + - "\x05error\x18\x04 \x01(\v2\v.QueryErrorH\x00R\x05error\x12\x1b\n" + - "\x04edge\x18\x05 \x01(\v2\x05.EdgeH\x00R\x04edgeB\x0f\n" + - "\rresponse_type\"\xa6\x01\n" + - "\vQueryStatus\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12+\n" + - "\x06status\x18\x02 \x01(\x0e2\x13.QueryStatus.StatusR\x06status\"V\n" + - "\x06Status\x12\x0f\n" + - "\vUNSPECIFIED\x10\x00\x12\v\n" + - "\aSTARTED\x10\x01\x12\r\n" + - "\tCANCELLED\x10\x03\x12\v\n" + - "\aERRORED\x10\x04\x12\f\n" + - "\bFINISHED\x10\x05\"\x04\b\x02\x10\x02\"\xaf\x02\n" + - "\n" + - "QueryError\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x123\n" + - "\terrorType\x18\x02 \x01(\x0e2\x15.QueryError.ErrorTypeR\terrorType\x12 \n" + - "\verrorString\x18\x03 \x01(\tR\verrorString\x12\x14\n" + - "\x05scope\x18\x04 \x01(\tR\x05scope\x12\x1e\n" + - "\n" + - "sourceName\x18\x05 \x01(\tR\n" + - "sourceName\x12\x1a\n" + - "\bitemType\x18\x06 \x01(\tR\bitemType\x12$\n" + - "\rresponderName\x18\a \x01(\tR\rresponderName\">\n" + - "\tErrorType\x12\t\n" + - "\x05OTHER\x10\x00\x12\f\n" + - "\bNOTFOUND\x10\x01\x12\v\n" + - "\aNOSCOPE\x10\x02\x12\v\n" + - "\aTIMEOUT\x10\x03\"!\n" + - "\vCancelQuery\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x92\x01\n" + - "\x06Expand\x12\x1e\n" + - "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12\x1c\n" + - "\tlinkDepth\x18\x02 \x01(\rR\tlinkDepth\x12\x12\n" + - "\x04UUID\x18\x03 \x01(\fR\x04UUID\x126\n" + - "\bdeadline\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\"\xbf\x01\n" + - "\tReference\x12\x12\n" + - "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + - "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + - "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + - "\aisQuery\x18\x04 \x01(\bR\aisQuery\x12\x14\n" + - "\x05query\x18\x05 \x01(\tR\x05query\x12$\n" + - "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"\x81\x01\n" + - "\x04Edge\x12\x1e\n" + - "\x04from\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04from\x12\x1a\n" + - "\x02to\x18\x02 \x01(\v2\n" + - ".ReferenceR\x02to\x12=\n" + - "\x10blastPropagation\x18\x03 \x01(\v2\x11.BlastPropagationR\x10blastPropagation*e\n" + - "\x06Health\x12\x12\n" + - "\x0eHEALTH_UNKNOWN\x10\x00\x12\r\n" + - "\tHEALTH_OK\x10\x01\x12\x12\n" + - "\x0eHEALTH_WARNING\x10\x02\x12\x10\n" + - "\fHEALTH_ERROR\x10\x03\x12\x12\n" + - "\x0eHEALTH_PENDING\x10\x04*,\n" + - "\vQueryMethod\x12\a\n" + - "\x03GET\x10\x00\x12\b\n" + - "\x04LIST\x10\x01\x12\n" + - "\n" + - "\x06SEARCH\x10\x02B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_items_proto_rawDescOnce sync.Once - file_items_proto_rawDescData []byte -) - -func file_items_proto_rawDescGZIP() []byte { - file_items_proto_rawDescOnce.Do(func() { - file_items_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_items_proto_rawDesc), len(file_items_proto_rawDesc))) - }) - return file_items_proto_rawDescData -} - -var file_items_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_items_proto_msgTypes = make([]protoimpl.MessageInfo, 18) -var file_items_proto_goTypes = []any{ - (Health)(0), // 0: Health - (QueryMethod)(0), // 1: QueryMethod - (QueryStatus_Status)(0), // 2: QueryStatus.Status - (QueryError_ErrorType)(0), // 3: QueryError.ErrorType - (*BlastPropagation)(nil), // 4: BlastPropagation - (*LinkedItemQuery)(nil), // 5: LinkedItemQuery - (*LinkedItem)(nil), // 6: LinkedItem - (*Item)(nil), // 7: Item - (*ItemAttributes)(nil), // 8: ItemAttributes - (*Metadata)(nil), // 9: Metadata - (*Items)(nil), // 10: Items - (*LogStreamDetails)(nil), // 11: LogStreamDetails - (*Query)(nil), // 12: Query - (*QueryResponse)(nil), // 13: QueryResponse - (*QueryStatus)(nil), // 14: QueryStatus - (*QueryError)(nil), // 15: QueryError - (*CancelQuery)(nil), // 16: CancelQuery - (*Expand)(nil), // 17: Expand - (*Reference)(nil), // 18: Reference - (*Edge)(nil), // 19: Edge - nil, // 20: Item.TagsEntry - (*Query_RecursionBehaviour)(nil), // 21: Query.RecursionBehaviour - (*structpb.Struct)(nil), // 22: google.protobuf.Struct - (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 24: google.protobuf.Duration - (*Response)(nil), // 25: Response -} -var file_items_proto_depIdxs = []int32{ - 12, // 0: LinkedItemQuery.query:type_name -> Query - 4, // 1: LinkedItemQuery.blastPropagation:type_name -> BlastPropagation - 18, // 2: LinkedItem.item:type_name -> Reference - 4, // 3: LinkedItem.blastPropagation:type_name -> BlastPropagation - 8, // 4: Item.attributes:type_name -> ItemAttributes - 9, // 5: Item.metadata:type_name -> Metadata - 5, // 6: Item.linkedItemQueries:type_name -> LinkedItemQuery - 6, // 7: Item.linkedItems:type_name -> LinkedItem - 0, // 8: Item.health:type_name -> Health - 20, // 9: Item.tags:type_name -> Item.TagsEntry - 11, // 10: Item.logStreams:type_name -> LogStreamDetails - 22, // 11: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct - 12, // 12: Metadata.sourceQuery:type_name -> Query - 23, // 13: Metadata.timestamp:type_name -> google.protobuf.Timestamp - 24, // 14: Metadata.sourceDuration:type_name -> google.protobuf.Duration - 24, // 15: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration - 7, // 16: Items.items:type_name -> Item - 1, // 17: Query.method:type_name -> QueryMethod - 21, // 18: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour - 23, // 19: Query.deadline:type_name -> google.protobuf.Timestamp - 7, // 20: QueryResponse.newItem:type_name -> Item - 25, // 21: QueryResponse.response:type_name -> Response - 15, // 22: QueryResponse.error:type_name -> QueryError - 19, // 23: QueryResponse.edge:type_name -> Edge - 2, // 24: QueryStatus.status:type_name -> QueryStatus.Status - 3, // 25: QueryError.errorType:type_name -> QueryError.ErrorType - 18, // 26: Expand.item:type_name -> Reference - 23, // 27: Expand.deadline:type_name -> google.protobuf.Timestamp - 1, // 28: Reference.method:type_name -> QueryMethod - 18, // 29: Edge.from:type_name -> Reference - 18, // 30: Edge.to:type_name -> Reference - 4, // 31: Edge.blastPropagation:type_name -> BlastPropagation - 32, // [32:32] is the sub-list for method output_type - 32, // [32:32] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name -} - -func init() { file_items_proto_init() } -func file_items_proto_init() { - if File_items_proto != nil { - return - } - file_responses_proto_init() - file_items_proto_msgTypes[3].OneofWrappers = []any{} - file_items_proto_msgTypes[9].OneofWrappers = []any{ - (*QueryResponse_NewItem)(nil), - (*QueryResponse_Response)(nil), - (*QueryResponse_Error)(nil), - (*QueryResponse_Edge)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_items_proto_rawDesc), len(file_items_proto_rawDesc)), - NumEnums: 4, - NumMessages: 18, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_items_proto_goTypes, - DependencyIndexes: file_items_proto_depIdxs, - EnumInfos: file_items_proto_enumTypes, - MessageInfos: file_items_proto_msgTypes, - }.Build() - File_items_proto = out.File - file_items_proto_goTypes = nil - file_items_proto_depIdxs = nil -} diff --git a/sdp-go/items_test.go b/sdp-go/items_test.go deleted file mode 100644 index 666f3c69..00000000 --- a/sdp-go/items_test.go +++ /dev/null @@ -1,695 +0,0 @@ -package sdp - -import ( - "bytes" - "context" - "encoding/json" - "reflect" - "testing" - "time" - - "github.com/google/uuid" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -type ToAttributesTest struct { - Name string - Input map[string]interface{} -} - -type CustomString string - -var Dylan CustomString = "Dylan" - -type CustomBool bool - -var Bool1 CustomBool = false -var NilPointerBool *bool - -type CustomStruct struct { - Foo string `json:",omitempty"` - Bar string `json:",omitempty"` - Baz string `json:",omitempty"` - Time time.Time `json:",omitempty"` - Duration time.Duration `json:",omitempty"` -} - -var ToAttributesTests = []ToAttributesTest{ - { - Name: "Basic strings map", - Input: map[string]interface{}{ - "firstName": "Dylan", - "lastName": "Ratcliffe", - }, - }, - { - Name: "Arrays map", - Input: map[string]interface{}{ - "empty": []string{}, - "single-level": []string{ - "one", - "two", - }, - "multi-level": [][]string{ - { - "one-one", - "one-two", - }, - { - "two-one", - "two-two", - }, - }, - }, - }, - { - Name: "Nested strings maps", - Input: map[string]interface{}{ - "strings map": map[string]string{ - "foo": "bar", - }, - }, - }, - { - Name: "Nested integer map", - Input: map[string]interface{}{ - "numbers map": map[string]int{ - "one": 1, - "two": 2, - }, - }, - }, - { - Name: "Nested string-array map", - Input: map[string]interface{}{ - "arrays map": map[string][]string{ - "dogs": { - "pug", - "also pug", - }, - }, - }, - }, - { - Name: "Nested non-string keys map", - Input: map[string]interface{}{ - "non-string keys": map[int]string{ - 1: "one", - 2: "two", - 3: "three", - }, - }, - }, - { - Name: "Composite types", - Input: map[string]interface{}{ - "underlying string": Dylan, - "underlying bool": Bool1, - }, - }, - { - Name: "Pointers", - Input: map[string]interface{}{ - "pointer bool": &Bool1, - "pointer string": &Dylan, - }, - }, - { - Name: "structs", - Input: map[string]interface{}{ - "named struct": CustomStruct{ - Foo: "foo", - Bar: "bar", - Baz: "baz", - Time: time.Now(), - }, - "anon struct": struct { - Yes bool - }{ - Yes: true, - }, - }, - }, - { - Name: "Zero-value structs", - Input: map[string]interface{}{ - "something": CustomStruct{ - Foo: "yes", - Time: time.Now(), - }, - }, - }, -} - -func TestToAttributes(t *testing.T) { - for _, tat := range ToAttributesTests { - t.Run(tat.Name, func(t *testing.T) { - var inputBytes []byte - var attributesBytes []byte - var inputJSON string - var attributesJSON string - var attributes *ItemAttributes - var err error - - // Convert the input to Attributes - attributes, err = ToAttributes(tat.Input) - - if err != nil { - t.Fatal(err) - } - - // In order to compare these reliably I'm going to do the following: - // - // 1. Convert to JSON - // 2. Convert back again - // 3. Compare with reflect.DeepEqual() - - // Convert the input to JSON - inputBytes, err = json.MarshalIndent(tat.Input, "", " ") - - if err != nil { - t.Fatal(err) - } - - // Convert the attributes to JSON - attributesBytes, err = json.MarshalIndent(attributes.GetAttrStruct().AsMap(), "", " ") - - if err != nil { - t.Fatal(err) - } - - var input map[string]interface{} - var output map[string]interface{} - - err = json.Unmarshal(inputBytes, &input) - - if err != nil { - t.Fatal(err) - } - - err = json.Unmarshal(attributesBytes, &output) - - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(input, output) { - // Convert to strings for printing - inputJSON = string(inputBytes) - attributesJSON = string(attributesBytes) - - t.Errorf("JSON did not match (note that order of map keys doesn't matter)\nInput: %v\nAttributes: %v", inputJSON, attributesJSON) - } - }) - - } -} - -func TestDefaultTransformMap(t *testing.T) { - input := map[string]interface{}{ - // Use a duration - "hour": 1 * time.Hour, - } - - attributes, err := ToAttributes(input) - - if err != nil { - t.Fatal(err) - } - - hour, err := attributes.Get("hour") - - if err != nil { - t.Fatal(err) - } - - if hour != "1h0m0s" { - t.Errorf("Expected hour to be 1h0m0s, got %v", hour) - } -} - -func TestCustomTransforms(t *testing.T) { - t.Run("redaction", func(t *testing.T) { - type Secret struct { - Value string - } - - data := map[string]interface{}{ - "user": map[string]interface{}{ - "name": "Hunter", - "password": Secret{ - Value: "hunter2", - }, - }, - } - - attributes, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Secret{}): func(i interface{}) interface{} { - // Remove it - return "REDACTED" - }, - }) - - if err != nil { - t.Fatal(err) - } - - user, err := attributes.Get("user") - - if err != nil { - t.Fatal(err) - } - - userMap, ok := user.(map[string]interface{}) - - if !ok { - t.Fatalf("Expected user to be a map, got %T", user) - } - - pass := userMap["password"] - if pass != "REDACTED" { - t.Errorf("Expected password to be REDACTED, got %v", pass) - } - }) - - t.Run("map response", func(t *testing.T) { - type Something struct { - Foo string - Bar string - } - - data := map[string]interface{}{ - "something": Something{ - Foo: "foo", - Bar: "bar", - }, - } - - attributes, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Something{}): func(i interface{}) interface{} { - something := i.(Something) - - return map[string]string{ - "foo": something.Foo, - "bar": something.Bar, - } - }, - }) - - if err != nil { - t.Fatal(err) - } - - something, err := attributes.Get("something") - - if err != nil { - t.Fatal(err) - } - - somethingMap, ok := something.(map[string]interface{}) - - if !ok { - t.Fatalf("Expected something to be a map, got %T", something) - } - - if somethingMap["foo"] != "foo" { - t.Errorf("Expected foo to be foo, got %v", somethingMap["foo"]) - } - - if somethingMap["bar"] != "bar" { - t.Errorf("Expected bar to be bar, got %v", somethingMap["bar"]) - } - }) - t.Run("returns nil", func(t *testing.T) { - type Something struct { - Foo string - Bar string - } - - data := map[string]interface{}{ - "something": Something{ - Foo: "foo", - Bar: "bar", - }, - "else": nil, - } - - _, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Something{}): func(i interface{}) interface{} { - return nil - }, - }) - - if err != nil { - t.Fatal(err) - } - }) -} - -func TestCopy(t *testing.T) { - exampleAttributes, err := ToAttributes(map[string]interface{}{ - "name": "Dylan", - "friend": "Mike", - "age": 27, - }) - - if err != nil { - t.Fatalf("Could not convert to attributes: %v", err) - } - - t.Run("With a complete item", func(t *testing.T) { - u := uuid.New() - - itemA := Item{ - Type: "user", - UniqueAttribute: "name", - Scope: "test", - Attributes: exampleAttributes, - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - LinkedItemQueries: []*LinkedItemQuery{ - { - Query: &Query{ - Type: "user", - Method: QueryMethod_GET, - Query: "Mike", - }, - }, - }, - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - LinkedItems: []*LinkedItem{}, - Metadata: &Metadata{ - SourceName: "test", - SourceQuery: &Query{ - Type: "user", - Method: QueryMethod_GET, - Query: "Dylan", - Scope: "testScope", - UUID: u[:], - }, - Timestamp: timestamppb.Now(), - SourceDuration: durationpb.New(100 * time.Millisecond), - SourceDurationPerItem: durationpb.New(10 * time.Millisecond), - }, - Health: Health_HEALTH_ERROR.Enum(), - Tags: map[string]string{ - "foo": "bar", - }, - } - - t.Run("Copying an item", func(t *testing.T) { - itemB := proto.Clone(&itemA).(*Item) - - AssertItemsEqual(&itemA, itemB, t) - }) - }) - - t.Run("With a party-filled item", func(t *testing.T) { - itemA := Item{ - Type: "user", - UniqueAttribute: "name", - Scope: "test", - Attributes: exampleAttributes, - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - LinkedItemQueries: []*LinkedItemQuery{ - { - Query: &Query{ - Type: "user", - Method: QueryMethod_GET, - Query: "Mike", - }, - }, - }, - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - LinkedItems: []*LinkedItem{}, - Metadata: &Metadata{ - Hidden: true, - SourceName: "test", - Timestamp: timestamppb.Now(), - SourceDuration: durationpb.New(100 * time.Millisecond), - SourceDurationPerItem: durationpb.New(10 * time.Millisecond), - }, - } - - t.Run("Copying an item", func(t *testing.T) { - itemB := proto.Clone(&itemA).(*Item) - - AssertItemsEqual(&itemA, itemB, t) - }) - }) - - t.Run("With a minimal item", func(t *testing.T) { - itemA := Item{ - Type: "user", - UniqueAttribute: "name", - Scope: "test", - Attributes: exampleAttributes, - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - LinkedItemQueries: []*LinkedItemQuery{}, - LinkedItems: []*LinkedItem{}, - } - - t.Run("Copying an item", func(t *testing.T) { - itemB := proto.Clone(&itemA).(*Item) - - AssertItemsEqual(&itemA, itemB, t) - }) - }) - -} - -func AssertItemsEqual(itemA *Item, itemB *Item, t *testing.T) { - if itemA.GetScope() != itemB.GetScope() { - t.Error("Scope did not match") - } - - if itemA.GetType() != itemB.GetType() { - t.Error("Type did not match") - } - - if itemA.GetUniqueAttribute() != itemB.GetUniqueAttribute() { - t.Error("UniqueAttribute did not match") - } - - var nameA interface{} - var nameB interface{} - var err error - - nameA, err = itemA.GetAttributes().Get("name") - - if err != nil { - t.Error(err) - } - - nameB, err = itemB.GetAttributes().Get("name") - - if err != nil { - t.Error(err) - } - - if nameA != nameB { - t.Error("Attributes.nam did not match") - - } - - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - if len(itemA.GetLinkedItemQueries()) != len(itemB.GetLinkedItemQueries()) { - t.Error("LinkedItemQueries length did not match") - } - - if len(itemA.GetLinkedItemQueries()) > 0 { - if itemA.GetLinkedItemQueries()[0].GetQuery().GetType() != itemB.GetLinkedItemQueries()[0].GetQuery().GetType() { - t.Error("LinkedItemQueries[0].Type did not match") - } - } - - // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore - if len(itemA.GetLinkedItems()) != len(itemB.GetLinkedItems()) { - t.Error("LinkedItems length did not match") - } - - if len(itemA.GetLinkedItems()) > 0 { - if itemA.GetLinkedItems()[0].GetItem().GetType() != itemB.GetLinkedItems()[0].GetItem().GetType() { - t.Error("LinkedItemQueries[0].Type did not match") - } - } - - for k, v := range itemA.GetTags() { - if itemB.GetTags()[k] != v { - t.Errorf("Tags[%v] did not match", k) - } - } - - if itemA.Health == nil { - if itemB.Health != nil { - t.Errorf("mismatched health nil and %v", itemB.GetHealth()) - } - } else { - if itemB.Health == nil { - t.Errorf("mismatched health %v and nil", itemA.GetHealth()) - - } else { - if itemA.GetHealth() != itemB.GetHealth() { - t.Errorf("mismatched health %v and %v", itemA.GetHealth(), itemB.GetHealth()) - } - } - } - - if itemA.GetMetadata() != nil { - if itemA.GetMetadata().GetSourceDuration().String() != itemB.GetMetadata().GetSourceDuration().String() { - t.Error("SourceDuration did not match") - } - - if itemA.GetMetadata().GetSourceDurationPerItem().String() != itemB.GetMetadata().GetSourceDurationPerItem().String() { - t.Error("SourceDurationPerItem did not match") - } - - if itemA.GetMetadata().GetSourceName() != itemB.GetMetadata().GetSourceName() { - t.Error("SourceName did not match") - } - - if itemA.GetMetadata().GetTimestamp().String() != itemB.GetMetadata().GetTimestamp().String() { - t.Error("Timestamp did not match") - } - - if itemA.GetMetadata().GetHidden() != itemB.GetMetadata().GetHidden() { - t.Error("Metadata.Hidden does not match") - } - - if itemA.GetMetadata().GetSourceQuery() != nil { - if itemA.GetMetadata().GetSourceQuery().GetScope() != itemB.GetMetadata().GetSourceQuery().GetScope() { - t.Error("Metadata.SourceQuery.Scope does not match") - } - - if itemA.GetMetadata().GetSourceQuery().GetMethod() != itemB.GetMetadata().GetSourceQuery().GetMethod() { - t.Error("Metadata.SourceQuery.Method does not match") - } - - if itemA.GetMetadata().GetSourceQuery().GetQuery() != itemB.GetMetadata().GetSourceQuery().GetQuery() { - t.Error("Metadata.SourceQuery.Query does not match") - } - - if itemA.GetMetadata().GetSourceQuery().GetType() != itemB.GetMetadata().GetSourceQuery().GetType() { - t.Error("Metadata.SourceQuery.Type does not match") - } - - if !bytes.Equal(itemA.GetMetadata().GetSourceQuery().GetUUID(), itemB.GetMetadata().GetSourceQuery().GetUUID()) { - t.Error("Metadata.SourceQuery.UUID does not match") - } - } - } -} - -func TestTimeoutContext(t *testing.T) { - q := Query{ - Type: "person", - Method: QueryMethod_GET, - Query: "foo", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 2, - }, - IgnoreCache: false, - Deadline: timestamppb.New(time.Now().Add(10 * time.Millisecond)), - } - - ctx, cancel := q.TimeoutContext(context.Background()) - defer cancel() - - select { - case <-time.After(20 * time.Millisecond): - t.Error("Context did not time out after 10ms") - case <-ctx.Done(): - // This is good - } -} - -func TestToAttributesViaJson(t *testing.T) { - // Create a random struct - test1 := struct { - Foo string - Bar bool - Blip []string - Baz struct { - Zap string - Bam int - } - }{ - Foo: "foo", - Bar: false, - Blip: []string{ - "yes", - "I", - "blip", - }, - Baz: struct { - Zap string - Bam int - }{ - Zap: "negative", - Bam: 42, - }, - } - - attributes, err := ToAttributesViaJson(test1) - - if err != nil { - t.Fatal(err) - } - - if foo, err := attributes.Get("Foo"); err != nil || foo != "foo" { - t.Errorf("Expected Foo to be 'foo', got %v, err: %v", foo, err) - } -} - -func TestAttributesGet(t *testing.T) { - mapData := map[string]interface{}{ - "foo": "bar", - "nest": map[string]interface{}{ - "nest2": map[string]string{ - "nest3": "nestValue", - }, - }, - } - - attr, err := ToAttributes(mapData) - - if err != nil { - t.Fatal(err) - } - - if v, err := attr.Get("foo"); err != nil || v != "bar" { - t.Errorf("expected Get(\"foo\") to be bar, got %v", v) - } - - if v, err := attr.Get("nest.nest2.nest3"); err != nil || v != "nestValue" { - t.Errorf("expected Get(\"nest.nest2.nest3\") to be nestValue, got %v", v) - } -} - -func TestAttributesSet(t *testing.T) { - mapData := map[string]interface{}{ - "foo": "bar", - "nest": map[string]interface{}{ - "nest2": map[string]string{ - "nest3": "nestValue", - }, - }, - } - - attr, err := ToAttributes(mapData) - - if err != nil { - t.Fatal(err) - } - - err = attr.Set("foo", "baz") - - if err != nil { - t.Error(err) - } - - if v, err := attr.Get("foo"); err != nil || v != "baz" { - t.Errorf("expected Get(\"foo\") to be baz, got %v", v) - } -} diff --git a/sdp-go/link_extract.go b/sdp-go/link_extract.go deleted file mode 100644 index d0e6c383..00000000 --- a/sdp-go/link_extract.go +++ /dev/null @@ -1,247 +0,0 @@ -package sdp - -import ( - "net" - "net/url" - "regexp" - "strings" - - "google.golang.org/protobuf/types/known/structpb" -) - -// This function tries to extract linked item queries from the attributes of an -// item. It should be on items that we know are likely to contain references -// that we can discover, but are in an unstructured format which we can't -// construct the linked item queries from directly. A good example of this would -// be the env vars for a kubernetes pod, or a config map -// -// This supports extracting the following formats: -// -// - IP addresses -// - HTTP/HTTPS URLs -// - DNS names -func ExtractLinksFromAttributes(attributes *ItemAttributes) []*LinkedItemQuery { - return extractLinksFromStructValue(attributes.GetAttrStruct()) -} - -// The same as `ExtractLinksFromAttributes`, but takes any input format and -// converts it to a set of ItemAttributes via the `ToAttributes` function. This -// uses reflection. `ExtractLinksFromAttributes` is more efficient if you have -// the attributes already in the correct format. -func ExtractLinksFrom(anything interface{}) ([]*LinkedItemQuery, error) { - attributes, err := ToAttributes(map[string]interface{}{ - "": anything, - }) - if err != nil { - return nil, err - } - - return ExtractLinksFromAttributes(attributes), nil -} - -func extractLinksFromValue(value *structpb.Value) []*LinkedItemQuery { - switch value.GetKind().(type) { - case *structpb.Value_NullValue: - return nil - case *structpb.Value_NumberValue: - return nil - case *structpb.Value_StringValue: - return extractLinksFromStringValue(value.GetStringValue()) - case *structpb.Value_BoolValue: - return nil - case *structpb.Value_StructValue: - return extractLinksFromStructValue(value.GetStructValue()) - case *structpb.Value_ListValue: - return extractLinksFromListValue(value.GetListValue()) - } - - return nil -} - -func extractLinksFromStructValue(structValue *structpb.Struct) []*LinkedItemQuery { - queries := make([]*LinkedItemQuery, 0) - - for _, value := range structValue.GetFields() { - queries = append(queries, extractLinksFromValue(value)...) - } - - return queries -} - -func extractLinksFromListValue(list *structpb.ListValue) []*LinkedItemQuery { - queries := make([]*LinkedItemQuery, 0) - - for _, value := range list.GetValues() { - queries = append(queries, extractLinksFromValue(value)...) - } - - return queries -} - -// A regex that matches the ARN format and extracts the service, region, account -// id and resource. Uses a capture group for the full resource portion after -// the account-id (which may include slashes for resource types). -var awsARNRegex = regexp.MustCompile(`^arn:[\w-]+:([\w-]+):([\w-]*):([\w-]*):(.+)`) - -// This function does all the heavy lifting for extracting linked item queries -// from strings. It will be called once for every string value in the item so -// needs to be very performant -func extractLinksFromStringValue(val string) []*LinkedItemQuery { - if ip := net.ParseIP(val); ip != nil { - return []*LinkedItemQuery{ - { - Query: &Query{ - Type: "ip", - Method: QueryMethod_GET, - Query: ip.String(), - Scope: "global", - }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: true, - }, - }, - } - } - - // This is pretty overzealous when it comes to what it considers a URL, so - // we need ot do out own validation to make sure that it has actually found - // what we expected - if parsed, err := url.Parse(val); err == nil && parsed.Scheme != "" && parsed.Host != "" { - // If it's a HTTP/HTTPS URL, we can use a HTTP query - if parsed.Scheme == "http" || parsed.Scheme == "https" { - return []*LinkedItemQuery{ - { - Query: &Query{ - Type: "http", - Method: QueryMethod_SEARCH, - Query: val, - Scope: "global", - }, - BlastPropagation: &BlastPropagation{ - // If we are referencing a HTTP URL, I think it's safe - // to assume that this is something that the current - // resource depends on and therefore that the blast - // radius should propagate inwards. This is a bit of a - // guess though... - In: true, - Out: false, - }, - }, - } - } - - // If it's not a HTTP/HTTPS URL, it'll be an IP or DNS name, so pass - // back to the main function - return extractLinksFromStringValue(parsed.Hostname()) - } - - if isLikelyDNSName(val) { - return []*LinkedItemQuery{ - { - Query: &Query{ - Type: "dns", - Method: QueryMethod_SEARCH, - Query: val, - Scope: "global", - }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, - }, - } - } - - // ARNs can't be shorter than 12 characters - if len(val) >= 12 { - if matches := awsARNRegex.FindStringSubmatch(val); matches != nil { - // If it looks like an ARN then we can construct a SEARCH query to try - // and find it. We can rely on the conventions in the AWS source here - - // Basic validation - if len(matches) != 5 || matches[1] == "" { - return nil - } - - // Parsed ARN parts - service := matches[1] // e.g. "ec2", "iam", "s3" - region := matches[2] // may be empty for global services (iam, cloudfront) - accountID := matches[3] // may be empty (e.g. s3, route53) - resource := matches[4] // full resource segment (may contain ":" or "/") - - // Extract resource type from the resource field (everything before first "/" or ":" if present) - resourceType := resource - if idx := strings.IndexAny(resource, "/:"); idx != -1 { - resourceType = resource[:idx] - } - - // Determine scope using a simple rule: - // - No account → wildcard scope - // - Account, no region → account-only - // - Account and region → account.region - var scope string - if accountID == "" { - scope = WILDCARD - } else if region == "" { - scope = accountID - } else { - scope = accountID + "." + region - } - - // Determine type using a consistent rule. Default to service-resourceType if available. - queryType := service - if resourceType != "" { - queryType = service + "-" + resourceType - } - // Special-case S3 ARNs that omit account and region → treat as bucket references - if service == "s3" && accountID == "" && region == "" { - queryType = "s3-bucket" - - // If this is an S3 object ARN (contains /), extract just the bucket - if strings.Contains(resource, "/") { - bucketName := strings.SplitN(resource, "/", 2)[0] - // Construct a bucket-only ARN for the query - val = "arn:aws:s3:::" + bucketName - } - } - - return []*LinkedItemQuery{ - { - Query: &Query{ - Type: queryType, - Method: QueryMethod_SEARCH, - Query: val, - Scope: scope, - }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, - }, - } - } - } - - return nil -} - -// Compile a regex pattern to match the general structure of a DNS name. Limits -// each label to 1-63 characters and matches only allowed characters and ensure -// that the name has at least three sections i.e. two dots. -var dnsNameRegex = regexp.MustCompile(`^(?i)([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.){2,}[a-z]{2,}$`) - -// This function returns true if the given string is a valid DNS name with at -// least three labels (sections) -func isLikelyDNSName(name string) bool { - // Quick length check before the regex. The less than 6 is because we're - // only matching names that have three sections or more, and the shortest - // three section name is a.b.cd (6 characters, there are no single letter - // top-level domains) - if len(name) < 6 || len(name) > 253 { - return false - } - - // Check if the name matches the regex pattern. - return dnsNameRegex.MatchString(name) -} diff --git a/sdp-go/link_extract_test.go b/sdp-go/link_extract_test.go deleted file mode 100644 index 0314b0f4..00000000 --- a/sdp-go/link_extract_test.go +++ /dev/null @@ -1,795 +0,0 @@ -package sdp - -import ( - "testing" - - "gopkg.in/yaml.v3" -) - -// Create a very large set of attributes for the benchmark -func createTestData() (*ItemAttributes, interface{}) { - yamlString := `--- -creationTimestamp: 2024-07-09T11:16:31Z -data: - AUTH0_AUDIENCE: https://api.example.com - AUTH0_DOMAIN: domain.eu.auth0.com - AUTH_COOKIE_NAME: overmind_app_access_token - GATEWAY_CLIENT_ID: 1234567890 - GATEWAY_CORS_ALLOW_ORIGINS: https://app.example.com https://*.app.example.com - GATEWAY_OVERMIND_AUTH_URL: https://domain.eu.auth0.com/oauth/token - GATEWAY_OVERMIND_TOKEN_API: http://service:8080/api - GATEWAY_PGDBNAME: user - GATEWAY_PGHOST: name.cluster-id.eu-west-2.rds.amazonaws.com - GATEWAY_PGPORT: "5432" - GATEWAY_PGUSER: user - GATEWAY_RUN_MODE: release - GATEWAY_SERVICE_PORT: "8080" - LOG: info -immutable: false -name: foo-config -namespace: default -resourceVersion: "167230088" -uid: c1c1be5e-e11e-46da-8ef4-ce243fe7056e -generateName: 49731160-e407-4148-bd4d-e00b8eb56cd2-5b76f5987b- -labels: - app: test - config-hash: 2be88ca42 - pod-template-hash: 5b76f5987b - source: 49731160-e407-4148-bd4d-e00b8eb56cd2 -spec: - containers: - - env: - - name: NATS_SERVICE_HOST - value: fdb4:5627:96ee::bfa3 - - name: NATS_SERVICE_PORT - value: "4222" - - name: NATS_NAME_PREFIX - value: source.default - - name: SERVICE_PORT - value: "8080" - - name: NATS_JWT - valueFrom: - secretKeyRef: - key: jwt - name: 49731160-e407-4148-bd4d-e00b8eb56cd2-nats-auth - - name: NATS_NKEY_SEED - valueFrom: - secretKeyRef: - key: nkeySeed - name: 49731160-e407-4148-bd4d-e00b8eb56cd2-nats-auth - - name: NATS_CA_FILE - value: /etc/srcman/certs/ca.pem - - name: S3_BUCKET_ARN - value: arn:aws:s3:::example-bucket-name - - name: S3_OBJECT_ARN - value: arn:aws:s3:::my-test-bucket/data/key - - name: IAM_ROLE_ARN - value: arn:aws:iam::123456789012:role/example-role - - name: CLOUDFRONT_ARN - value: arn:aws:cloudfront::123456789012:distribution/EDFDVBDEXAMPLE - envFrom: - - secretRef: - name: prod-tracing-secrets - image: ghcr.io/example/example:main - imagePullPolicy: Always - name: 49731160-e407-4148-bd4d-e00b8eb56cd2 - readinessProbe: - failureThreshold: 3 - httpGet: - path: healthz - port: 8080 - scheme: HTTP - periodSeconds: 10 - successThreshold: 1 - timeoutSeconds: 1 - resources: {} - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /etc/srcman/config - name: source-config - readOnly: true - - mountPath: /etc/srcman/certs - name: nats-certs - readOnly: true - - mountPath: /var/run/secrets/kubernetes.io/serviceaccount - name: kube-api-access-vjgp7 - readOnly: true - dnsPolicy: ClusterFirst - enableServiceLinks: true - nodeName: ip-10-0-4-118.eu-west-2.compute.internal - preemptionPolicy: PreemptLowerPriority - priority: 0 - restartPolicy: Always - schedulerName: default-scheduler - securityContext: {} - serviceAccount: default - serviceAccountName: default - terminationGracePeriodSeconds: 30 - tolerations: - - effect: NoExecute - key: node.kubernetes.io/not-ready - operator: Exists - tolerationSeconds: 300 - - effect: NoExecute - key: node.kubernetes.io/unreachable - operator: Exists - tolerationSeconds: 300 - volumes: - - configMap: - defaultMode: 420 - name: 49731160-e407-4148-bd4d-e00b8eb56cd2 - name: source-config - - configMap: - defaultMode: 420 - name: prod-ca - name: nats-certs - - name: kube-api-access-vjgp7 - projected: - defaultMode: 420 - sources: - - serviceAccountToken: - expirationSeconds: 3607 - path: token - - configMap: - items: - - key: ca.crt - path: ca.crt - name: kube-root-ca.crt - - downwardAPI: - items: - - fieldRef: - apiVersion: v1 - fieldPath: metadata.namespace - path: namespace -status: - conditions: - - lastTransitionTime: 2024-08-22T13:42:26Z - status: "True" - type: Initialized - - lastTransitionTime: 2024-08-22T13:43:17Z - status: "True" - type: Ready - - lastTransitionTime: 2024-08-22T13:43:17Z - status: "True" - type: ContainersReady - - lastTransitionTime: 2024-08-22T13:42:26Z - status: "True" - type: PodScheduled - containerStatuses: - - containerID: containerd://6274579a84ea3bee8cb9bd68092f4ccd6fff13852c1e5c09672c8b3489f3c082 - image: ghcr.io/example/example:main - imageID: ghcr.io/example/example@sha256:c3fd0767e82105e9127267bda3bdb77f51a9e6fbeb79d20c4d25ae0a71876719 - lastState: {} - name: 49731160-e407-4148-bd4d-e00b8eb56cd2 - ready: true - restartCount: 0 - started: true - state: - running: - startedAt: 2024-08-22T13:42:32Z - hostIP: 2a05:d01c:40:7600::6c81 - phase: Running - podIP: 2a05:d01c:40:7600:fbac::4 - podIPs: - - ip: 2a05:d01c:40:7600:fbac::3 - qosClass: BestEffort - startTime: 2024-08-22T13:42:26Z -code: - location: https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/123456789/ingress_log-dd9f17f1-0694-4e79-9855-300a7f9b7300?versionId=sxmueRdM4S.HXwlebzlb7rOpD2ZHmS3Z&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMiJGMEQCIBWNIrhNoAqNUG%2BoZLmKNSxY9ncDogcyFTGeJFef0zVMAiBhkAW9JWxVna%2FoCXe4u9S3364dCavXEvZP%2FXcD6iwfISq7BQiR%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDQ3MjAzODg2NDE4OC - repositoryType: S3 -configuration: - architectures: - - x86_64 - codeSha256: JxWQc4FaGuW8503fcWt5S2Ua+HHpIX2z2SMhyo/gzBU= - codeSize: 7586073 - description: Parses LB access logs from S3, sending them to Honeycomb as structured events - environment: - variables: - APIHOST: https://api.honeycomb.io - DATASET: ingress - ENVIRONMENT: "" - FILTERFIELDS: "" - FORCEGUNZIP: "true" - HONEYCOMBWRITEKEY: foobar - KMSKEYID: "" - PARSERTYPE: alb - RENAMEFIELDS: "" - SAMPLERATE: "1" - SAMPLERATERULES: "[]" - ephemeralStorage: - size: 512 - functionArn: arn:aws:lambda:eu-west-2:123456789:function:ingress_log - functionName: ingress_log - handler: s3-handler - lastModified: 2024-05-10T14:33:45.279+0000 - lastUpdateStatus: Successful - lastUpdateStatusReasonCode: "" - loggingConfig: - applicationLogLevel: "" - logFormat: Text - logGroup: /aws/lambda/ingress_log - systemLogLevel: "" - memorySize: 192 - packageType: Zip - revisionId: 876d6948-2e4c-41e0-9a62-d9be8a6a59f5 - role: arn:aws:iam::123456789:role/ingress_log - runtime: provided.al2 - runtimeVersionConfig: - runtimeVersionArn: arn:aws:lambda:eu-west-2::runtime:f4d7a18770044f40f09a49471782a2a42431d746fcfb30bf1cadeda985858aa0 - snapStart: - applyOn: None - optimizationStatus: Off - state: Active - stateReasonCode: "" - timeout: 600 - tracingConfig: - mode: PassThrough - version: $LATEST -tags: - honeycombAgentless: "true" - terraform: "true" -capacityProviderStrategy: - - base: 0 - capacityProvider: FARGATE - weight: 100 -clusterArn: arn:aws:ecs:eu-west-2:123456789:cluster/example-tfc -createdAt: 2024-08-01T16:06:18.906Z -createdBy: arn:aws:iam::123456789:role/terraform-example -deploymentConfiguration: - deploymentCircuitBreaker: - enable: false - rollback: false - maximumPercent: 200 - minimumHealthyPercent: 100 -deploymentController: - type: ECS -deployments: - - capacityProviderStrategy: - - base: 0 - capacityProvider: FARGATE - weight: 100 - createdAt: 2024-08-01T16:42:08.6Z - desiredCount: 1 - failedTasks: 0 - id: ecs-svc/5699741454300708027 - launchType: "" - networkConfiguration: - awsvpcConfiguration: - assignPublicIp: DISABLED - securityGroups: - - sg-0826c8494b61cac1f - subnets: - - subnet-0a393cf4c844bf32d - - subnet-0fafe900a3dc4ba78 - pendingCount: 0 - platformFamily: Linux - platformVersion: 1.4.0 - rolloutState: COMPLETED - rolloutStateReason: ECS deployment ecs-svc/5699741454300708027 completed. - runningCount: 1 - status: PRIMARY - taskDefinition: arn:aws:ecs:eu-west-2:123456789:task-definition/facial-recognition-tfc:1 - updatedAt: 2024-08-01T17:20:11.853Z -desiredCount: 1 -enableECSManagedTags: false -enableExecuteCommand: false -events: - - createdAt: 2024-08-01T16:37:45.222Z - id: f8240f68-73d0-497f-bf8e-4cb5185bd76c - message: "(service facial-recognition) has started 1 tasks: (task - d0fd4b687ebf4c968482a9814e1de455)." - - createdAt: 2024-08-22T15:50:56.905Z - id: 769e21aa-7a70-4270-88b9-55f902ddb727 - message: (service facial-recognition) has reached a steady state. -healthCheckGracePeriodSeconds: 0 -launchType: "" -loadBalancers: - - containerName: facial-recognition - containerPort: 1234 - targetGroupArn: arn:aws:elasticloadbalancing:eu-west-2:123456789:targetgroup/facerec-tfc/0b6a17c7de07be40 -networkConfiguration: - awsvpcConfiguration: - assignPublicIp: DISABLED - securityGroups: - - sg-0826c8494b61cac1f - subnets: - - subnet-0a393cf4c844bf32d - - subnet-0fafe900a3dc4ba78 -pendingCount: 0 -placementConstraints: [] -placementStrategy: [] -platformFamily: Linux -platformVersion: LATEST -propagateTags: NONE -roleArn: arn:aws:iam::123456789:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS -runningCount: 1 -schedulingStrategy: REPLICA -serviceArn: arn:aws:ecs:eu-west-2:123456789:service/example-tfc/facial-recognition -serviceFullName: service/example-tfc/facial-recognition -serviceName: facial-recognition -serviceRegistries: [] -taskSets: [] -compatibilities: - - EC2 - - FARGATE -containerDefinitions: - - cpu: 1024 - environment: [] - essential: true - healthCheck: - command: - - CMD-SHELL - - wget -q --spider localhost:1234 - interval: 30 - retries: 3 - timeout: 5 - image: harshmanvar/face-detection-tensorjs:slim-amd - memory: 2048 - mountPoints: [] - name: facial-recognition - portMappings: - - appProtocol: http - containerPort: 1234 - hostPort: 1234 - protocol: tcp - systemControls: [] - volumesFrom: [] -cpu: "1024" -family: facial-recognition-tfc -ipcMode: "" -memory: "2048" -networkMode: awsvpc -pidMode: "" -registeredAt: 2024-08-01T15:27:30.781Z -registeredBy: arn:aws:sts::123456789:assumed-role/terraform-example/terraform-run-nKsGeEsVUcxfxEuY -requiresAttributes: - - name: com.amazonaws.ecs.capability.docker-remote-api.1.18 - targetType: "" - - name: com.amazonaws.ecs.capability.docker-remote-api.1.24 - targetType: "" - - name: ecs.capability.container-health-check - targetType: "" - - name: ecs.capability.task-eni - targetType: "" -requiresCompatibilities: - - FARGATE -revision: 1 -volumes: [] -attachments: - - details: - - name: macAddress - value: 0a:98:2a:a1:8c:cd - - name: networkInterfaceId - value: eni-0c99da7dff9025194 - - name: privateDnsName - value: ip-10-0-2-101.eu-west-2.compute.internal - - name: privateIPv4Address - value: 10.0.2.101 - - name: subnetId - value: subnet-0fafe900a3dc4ba78 - id: f2dc881c-d3c5-49ca-904e-30358f1675d8 - status: ATTACHED - type: ElasticNetworkInterface -attributes: - - name: ecs.cpu-architecture - targetType: "" - value: x86_64 -availabilityZone: eu-west-2b -capacityProviderName: FARGATE -connectivity: CONNECTED -connectivityAt: 2024-08-01T17:16:34.995Z -containers: - - containerArn: arn:aws:ecs:eu-west-2:123456789:container/example-tfc/ded4f8eebe4144ddb9a93a27b5661008/778778dd-3a31-44f0-a84f-42a2e75403d9 - cpu: "1024" - healthStatus: HEALTHY - image: harshmanvar/face-detection-tensorjs:slim-amd - imageDigest: sha256:a12d885a6d05efa01735e5dd60b2580eece2f21d962e38b9bbdf8cfeb81c6894 - lastStatus: RUNNING - memory: "2048" - name: facial-recognition - networkBindings: [] - networkInterfaces: - - attachmentId: f2dc881c-d3c5-49ca-904e-30358f1675d8 - runtimeId: ded4f8eebe4144ddb9a93a27b5661008-4091029319 -desiredStatus: RUNNING -ephemeralStorage: - sizeInGiB: 20 -fargateEphemeralStorage: - sizeInGiB: 20 -group: service:facial-recognition -healthStatus: HEALTHY -id: example-tfc/ded4f8eebe4144ddb9a93a27b5661008 -lastStatus: RUNNING -overrides: - containerOverrides: - - name: facial-recognition - inferenceAcceleratorOverrides: [] -pullStartedAt: 2024-08-01T17:18:22.901Z -pullStoppedAt: 2024-08-01T17:18:34.827Z -startedAt: 2024-08-01T17:18:48.139Z -startedBy: ecs-svc/5699741454300708027 -stopCode: "" -taskArn: arn:aws:ecs:eu-west-2:123456789:task/example-tfc/ded4f8eebe4144ddb9a93a27b5661008 -version: 5 -` - - mapData := make(map[string]interface{}) - _ = yaml.Unmarshal([]byte(yamlString), &mapData) - - attrs, _ := ToAttributes(mapData) - - return attrs, mapData -} - -// Current performance: -// BenchmarkExtractLinksFromAttributes-10 5676 193114 ns/op 58868 B/op 721 allocs/op -func BenchmarkExtractLinksFromAttributes(b *testing.B) { - attrs, _ := createTestData() - - for range b.N { - _ = ExtractLinksFromAttributes(attrs) - } -} - -// Current performance: -// BenchmarkExtractLinksFrom-10 2671 451209 ns/op 231509 B/op 4241 allocs/op -func BenchmarkExtractLinksFrom(b *testing.B) { - _, data := createTestData() - - for range b.N { - _, _ = ExtractLinksFrom(data) - } -} - -func TestExtractLinksFromAttributes(t *testing.T) { - attrs, _ := createTestData() - - queries := ExtractLinksFromAttributes(attrs) - - tests := []struct { - ExpectedType string - ExpectedQuery string - ExpectedScope string - }{ - // ARN edge cases - these should work after the fix - { - ExpectedType: "s3-bucket", - ExpectedQuery: "arn:aws:s3:::example-bucket-name", - ExpectedScope: "*", // S3 buckets don't have region/account in ARN, use wildcard - }, - { - ExpectedType: "s3-bucket", - ExpectedQuery: "arn:aws:s3:::my-test-bucket", - ExpectedScope: "*", - }, - { - ExpectedType: "iam-role", - ExpectedQuery: "arn:aws:iam::123456789012:role/example-role", - ExpectedScope: "123456789012", // IAM is account-scoped, no region - }, - { - ExpectedType: "cloudfront-distribution", - ExpectedQuery: "arn:aws:cloudfront::123456789012:distribution/EDFDVBDEXAMPLE", - ExpectedScope: "123456789012", // CloudFront is account-scoped, no region - }, - { - ExpectedType: "ip", - ExpectedQuery: "2a05:d01c:40:7600::6c81", - }, - { - ExpectedType: "ip", - ExpectedQuery: "2a05:d01c:40:7600:fbac::3", - }, - { - ExpectedType: "ip", - ExpectedQuery: "2a05:d01c:40:7600:fbac::4", - }, - { - ExpectedType: "ip", - ExpectedQuery: "10.0.2.101", - }, - { - ExpectedType: "ip", - ExpectedQuery: "fdb4:5627:96ee::bfa3", - }, - { - ExpectedType: "http", - ExpectedQuery: "https://api.example.com", - }, - { - ExpectedType: "http", - ExpectedQuery: "https://domain.eu.auth0.com/oauth/token", - }, - { - ExpectedType: "dns", - ExpectedQuery: "domain.eu.auth0.com", - }, - { - ExpectedType: "http", - ExpectedQuery: "http://service:8080/api", - }, - { - ExpectedType: "http", - ExpectedQuery: "https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/123456789/ingress_log-dd9f17f1-0694-4e79-9855-300a7f9b7300?versionId=sxmueRdM4S.HXwlebzlb7rOpD2ZHmS3Z&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMiJGMEQCIBWNIrhNoAqNUG%2BoZLmKNSxY9ncDogcyFTGeJFef0zVMAiBhkAW9JWxVna%2FoCXe4u9S3364dCavXEvZP%2FXcD6iwfISq7BQiR%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDQ3MjAzODg2NDE4OC", - }, - { - ExpectedType: "http", - ExpectedQuery: "https://api.honeycomb.io", - }, - { - ExpectedType: "dns", - ExpectedQuery: "ip-10-0-2-101.eu-west-2.compute.internal", - }, - { - ExpectedType: "dns", - ExpectedQuery: "ip-10-0-4-118.eu-west-2.compute.internal", - }, - { - ExpectedType: "dns", - ExpectedQuery: "name.cluster-id.eu-west-2.rds.amazonaws.com", - }, - { - ExpectedType: "lambda-function", - ExpectedQuery: "arn:aws:lambda:eu-west-2:123456789:function:ingress_log", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "ecs-cluster", - ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:cluster/example-tfc", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "ecs-container", - ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:container/example-tfc/ded4f8eebe4144ddb9a93a27b5661008/778778dd-3a31-44f0-a84f-42a2e75403d9", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "ecs-service", - ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:service/example-tfc/facial-recognition", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "ecs-task-definition", - ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:task-definition/facial-recognition-tfc:1", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "ecs-task", - ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:task/example-tfc/ded4f8eebe4144ddb9a93a27b5661008", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "elasticloadbalancing-targetgroup", - ExpectedQuery: "arn:aws:elasticloadbalancing:eu-west-2:123456789:targetgroup/facerec-tfc/0b6a17c7de07be40", - ExpectedScope: "123456789.eu-west-2", - }, - { - ExpectedType: "iam-role", - ExpectedQuery: "arn:aws:iam::123456789:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS", - ExpectedScope: "123456789", - }, - { - ExpectedType: "iam-role", - ExpectedQuery: "arn:aws:iam::123456789:role/ingress_log", - ExpectedScope: "123456789", - }, - { - ExpectedType: "iam-role", - ExpectedQuery: "arn:aws:iam::123456789:role/terraform-example", - ExpectedScope: "123456789", - }, - { - ExpectedType: "sts-assumed-role", - ExpectedQuery: "arn:aws:sts::123456789:assumed-role/terraform-example/terraform-run-nKsGeEsVUcxfxEuY", - ExpectedScope: "123456789", - }, - } - - // Note: We don't check length anymore since we added new test cases - // that may result in more extracted queries than we have tests for - - for _, test := range tests { - found := false - for _, query := range queries { - if query.GetQuery().GetQuery() == test.ExpectedQuery && query.GetQuery().GetType() == test.ExpectedType { - if test.ExpectedScope == "" { - // If we don't care about the scope then it's a match - found = true - break - } else { - // If we do care about the scope then check that it matches - if query.GetQuery().GetScope() == test.ExpectedScope { - found = true - break - } - } - } - } - - if !found { - t.Errorf("expected query not found: %s %s", test.ExpectedType, test.ExpectedQuery) - } - } -} - -func TestExtractLinksFrom(t *testing.T) { - tests := []struct { - Name string - Object interface{} - ExpectedQueries []string - }{ - { - Name: "Env var structure array", - Object: []struct { - Name string - Value string - }{ - { - Name: "example", - Value: "https://example.com", - }, - }, - ExpectedQueries: []string{"https://example.com"}, - }, - { - Name: "Just a raw string", - Object: "https://example.com", - ExpectedQueries: []string{"https://example.com"}, - }, - { - Name: "Nil", - Object: nil, - ExpectedQueries: []string{}, - }, - { - Name: "Struct", - Object: struct { - Name string - Value string - }{ - Name: "example", - Value: "https://example.com", - }, - ExpectedQueries: []string{"https://example.com"}, - }, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - queries, err := ExtractLinksFrom(test.Object) - if err != nil { - t.Fatal(err) - } - - if len(queries) != len(test.ExpectedQueries) { - t.Errorf("expected %d queries, got %d", len(test.ExpectedQueries), len(queries)) - } - - for i, query := range queries { - if query.GetQuery().GetQuery() != test.ExpectedQueries[i] { - t.Errorf("expected query %s, got %s", test.ExpectedQueries[i], query.GetQuery().GetQuery()) - } - } - }) - } -} - -func TestExtractLinksFromConfigMapData(t *testing.T) { - // Test ConfigMap data with S3 bucket ARN - configMapData := map[string]interface{}{ - "data": map[string]interface{}{ - "S3_BUCKET_ARN": "arn:aws:s3:::example-bucket-name", - "S3_BUCKET_NAME": "example-bucket-name", - }, - } - - queries, err := ExtractLinksFrom(configMapData) - if err != nil { - t.Fatal(err) - } - - // Find the S3 bucket query - found := false - for _, query := range queries { - if query.GetQuery().GetType() == "s3-bucket" && - query.GetQuery().GetQuery() == "arn:aws:s3:::example-bucket-name" { - found = true - if query.GetQuery().GetScope() != WILDCARD { - t.Errorf("expected scope to be WILDCARD (%s), got %s", WILDCARD, query.GetQuery().GetScope()) - } - if query.GetQuery().GetMethod() != QueryMethod_SEARCH { - t.Errorf("expected method to be SEARCH, got %v", query.GetQuery().GetMethod()) - } - break - } - } - - if !found { - t.Errorf("expected to find s3-bucket query for ARN arn:aws:s3:::example-bucket-name") - t.Logf("Found %d queries:", len(queries)) - for _, q := range queries { - t.Logf(" Type: %s, Query: %s, Scope: %s", q.GetQuery().GetType(), q.GetQuery().GetQuery(), q.GetQuery().GetScope()) - } - } -} - -func TestS3BucketARNTypeDetection(t *testing.T) { - tests := []struct { - name string - arn string - expectedType string - expectedQuery string - expectedScope string - }{ - { - name: "S3 bucket ARN without account/region", - arn: "arn:aws:s3:::example-bucket-name", - expectedType: "s3-bucket", - expectedQuery: "arn:aws:s3:::example-bucket-name", - expectedScope: WILDCARD, - }, - { - name: "S3 bucket ARN with short name", - arn: "arn:aws:s3:::my-bucket", - expectedType: "s3-bucket", - expectedQuery: "arn:aws:s3:::my-bucket", - expectedScope: WILDCARD, - }, - { - name: "S3 object ARN (should extract bucket)", - arn: "arn:aws:s3:::my-bucket/path/to/object", - expectedType: "s3-bucket", - expectedQuery: "arn:aws:s3:::my-bucket", - expectedScope: WILDCARD, - }, - { - name: "S3 object ARN with nested path", - arn: "arn:aws:s3:::my-bucket/folder/subfolder/file.txt", - expectedType: "s3-bucket", - expectedQuery: "arn:aws:s3:::my-bucket", - expectedScope: WILDCARD, - }, - { - name: "S3 bucket ARN with hyphens in name", - arn: "arn:aws:s3:::my-test-bucket-name", - expectedType: "s3-bucket", - expectedQuery: "arn:aws:s3:::my-test-bucket-name", - expectedScope: WILDCARD, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - queries, err := ExtractLinksFrom(map[string]interface{}{ - "arn": tt.arn, - }) - if err != nil { - t.Fatal(err) - } - - found := false - for _, query := range queries { - if query.GetQuery().GetType() == tt.expectedType && - query.GetQuery().GetQuery() == tt.expectedQuery { - found = true - if query.GetQuery().GetScope() != tt.expectedScope { - t.Errorf("expected scope %s, got %s", tt.expectedScope, query.GetQuery().GetScope()) - } - if query.GetQuery().GetMethod() != QueryMethod_SEARCH { - t.Errorf("expected method SEARCH, got %v", query.GetQuery().GetMethod()) - } - break - } - } - - if !found { - t.Errorf("expected to find query with type %s and query %s", tt.expectedType, tt.expectedQuery) - t.Logf("Found %d queries:", len(queries)) - for _, q := range queries { - t.Logf(" Type: %s, Query: %s, Scope: %s", q.GetQuery().GetType(), q.GetQuery().GetQuery(), q.GetQuery().GetScope()) - } - } - }) - } -} diff --git a/sdp-go/logs.go b/sdp-go/logs.go deleted file mode 100644 index aa012b4d..00000000 --- a/sdp-go/logs.go +++ /dev/null @@ -1,77 +0,0 @@ -package sdp - -import ( - "errors" - "fmt" - "strings" - - "connectrpc.com/connect" -) - -// Validate ensures that GetLogRecordsRequest is valid -func (req *GetLogRecordsRequest) Validate() error { - if req == nil { - return errors.New("GetLogRecordsRequest is nil") - } - - // scope has to be non-nil, non-empty string - if strings.TrimSpace(req.GetScope()) == "" { - return errors.New("scope has to be non-empty") - } - - // query has to be non-nil, non-empty string - if strings.TrimSpace(req.GetQuery()) == "" { - return errors.New("query has to be non-empty") - } - - // from and to have to be valid timestamps - if req.GetFrom() == nil { - return errors.New("from timestamp is required") - } - - if req.GetTo() == nil { - return errors.New("to timestamp is required") - } - - // from has to be before or equal to to - fromTime := req.GetFrom().AsTime() - toTime := req.GetTo().AsTime() - if fromTime.After(toTime) { - return fmt.Errorf("from timestamp (%v) must be before or equal to to timestamp (%v)", fromTime, toTime) - } - - if req.GetMaxRecords() < 0 { - return errors.New("maxRecords must be greater than or equal to zero") - } - - return nil -} - -// NewUpstreamSourceError creates a new SourceError with the given message and error -func NewUpstreamSourceError(code connect.Code, message string) *SourceError { - return &SourceError{ - Code: SourceError_Code(code), //nolint:gosec - Message: message, - Upstream: true, - } -} - -// NewLocalSourceError creates a new SourceError with the given message and error, indicating a local (non-upstream) error -func NewLocalSourceError(code connect.Code, message string) *SourceError { - return &SourceError{ - Code: SourceError_Code(code), //nolint:gosec - Message: message, - Upstream: false, - } -} - -// assert interface implementation -var _ error = (*SourceError)(nil) - -// Error implements the error interface for SourceError -func (e *SourceError) Error() string { - if e.GetUpstream() { - return fmt.Sprintf("Upstream Error: %s", e.GetMessage()) - } - return fmt.Sprintf("Source Error: %s", e.GetMessage()) -} diff --git a/sdp-go/logs.pb.go b/sdp-go/logs.pb.go deleted file mode 100644 index 2ae943fc..00000000 --- a/sdp-go/logs.pb.go +++ /dev/null @@ -1,1083 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: logs.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - structpb "google.golang.org/protobuf/types/known/structpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// These mirror the OpenTelemetry log severity levels -// https://opentelemetry.io/docs/specs/otel/logs/data-model/#displaying-severity -// Refer to the OpenTelemetry documentation for information on how these should -// be mapped -type LogSeverity int32 - -const ( - LogSeverity_UNSPECIFIED LogSeverity = 0 - LogSeverity_TRACE LogSeverity = 1 - LogSeverity_TRACE2 LogSeverity = 2 - LogSeverity_TRACE3 LogSeverity = 3 - LogSeverity_TRACE4 LogSeverity = 4 - LogSeverity_DEBUG LogSeverity = 5 - LogSeverity_DEBUG2 LogSeverity = 6 - LogSeverity_DEBUG3 LogSeverity = 7 - LogSeverity_DEBUG4 LogSeverity = 8 - LogSeverity_INFO LogSeverity = 9 - LogSeverity_INFO2 LogSeverity = 10 - LogSeverity_INFO3 LogSeverity = 11 - LogSeverity_INFO4 LogSeverity = 12 - LogSeverity_WARN LogSeverity = 13 - LogSeverity_WARN2 LogSeverity = 14 - LogSeverity_WARN3 LogSeverity = 15 - LogSeverity_WARN4 LogSeverity = 16 - LogSeverity_ERROR LogSeverity = 17 - LogSeverity_ERROR2 LogSeverity = 18 - LogSeverity_ERROR3 LogSeverity = 19 - LogSeverity_ERROR4 LogSeverity = 20 - LogSeverity_FATAL LogSeverity = 21 - LogSeverity_FATAL2 LogSeverity = 22 - LogSeverity_FATAL3 LogSeverity = 23 - LogSeverity_FATAL4 LogSeverity = 24 -) - -// Enum value maps for LogSeverity. -var ( - LogSeverity_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "TRACE", - 2: "TRACE2", - 3: "TRACE3", - 4: "TRACE4", - 5: "DEBUG", - 6: "DEBUG2", - 7: "DEBUG3", - 8: "DEBUG4", - 9: "INFO", - 10: "INFO2", - 11: "INFO3", - 12: "INFO4", - 13: "WARN", - 14: "WARN2", - 15: "WARN3", - 16: "WARN4", - 17: "ERROR", - 18: "ERROR2", - 19: "ERROR3", - 20: "ERROR4", - 21: "FATAL", - 22: "FATAL2", - 23: "FATAL3", - 24: "FATAL4", - } - LogSeverity_value = map[string]int32{ - "UNSPECIFIED": 0, - "TRACE": 1, - "TRACE2": 2, - "TRACE3": 3, - "TRACE4": 4, - "DEBUG": 5, - "DEBUG2": 6, - "DEBUG3": 7, - "DEBUG4": 8, - "INFO": 9, - "INFO2": 10, - "INFO3": 11, - "INFO4": 12, - "WARN": 13, - "WARN2": 14, - "WARN3": 15, - "WARN4": 16, - "ERROR": 17, - "ERROR2": 18, - "ERROR3": 19, - "ERROR4": 20, - "FATAL": 21, - "FATAL2": 22, - "FATAL3": 23, - "FATAL4": 24, - } -) - -func (x LogSeverity) Enum() *LogSeverity { - p := new(LogSeverity) - *p = x - return p -} - -func (x LogSeverity) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (LogSeverity) Descriptor() protoreflect.EnumDescriptor { - return file_logs_proto_enumTypes[0].Descriptor() -} - -func (LogSeverity) Type() protoreflect.EnumType { - return &file_logs_proto_enumTypes[0] -} - -func (x LogSeverity) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use LogSeverity.Descriptor instead. -func (LogSeverity) EnumDescriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{0} -} - -type NATSGetLogRecordsResponseStatus_Status int32 - -const ( - NATSGetLogRecordsResponseStatus_UNSPECIFIED NATSGetLogRecordsResponseStatus_Status = 0 - // The source has started processing the request. - NATSGetLogRecordsResponseStatus_STARTED NATSGetLogRecordsResponseStatus_Status = 1 - // The source has finished processing the request. No further messages will - // be sent after this. - NATSGetLogRecordsResponseStatus_FINISHED NATSGetLogRecordsResponseStatus_Status = 2 - // The source encountered an error while processing the request. No further - // messages will be sent after this. - NATSGetLogRecordsResponseStatus_ERRORED NATSGetLogRecordsResponseStatus_Status = 3 -) - -// Enum value maps for NATSGetLogRecordsResponseStatus_Status. -var ( - NATSGetLogRecordsResponseStatus_Status_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "STARTED", - 2: "FINISHED", - 3: "ERRORED", - } - NATSGetLogRecordsResponseStatus_Status_value = map[string]int32{ - "UNSPECIFIED": 0, - "STARTED": 1, - "FINISHED": 2, - "ERRORED": 3, - } -) - -func (x NATSGetLogRecordsResponseStatus_Status) Enum() *NATSGetLogRecordsResponseStatus_Status { - p := new(NATSGetLogRecordsResponseStatus_Status) - *p = x - return p -} - -func (x NATSGetLogRecordsResponseStatus_Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (NATSGetLogRecordsResponseStatus_Status) Descriptor() protoreflect.EnumDescriptor { - return file_logs_proto_enumTypes[1].Descriptor() -} - -func (NATSGetLogRecordsResponseStatus_Status) Type() protoreflect.EnumType { - return &file_logs_proto_enumTypes[1] -} - -func (x NATSGetLogRecordsResponseStatus_Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use NATSGetLogRecordsResponseStatus_Status.Descriptor instead. -func (NATSGetLogRecordsResponseStatus_Status) EnumDescriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{5, 0} -} - -// This directly mirrors the Connect RPC codes that can be found here: -// https://connectrpc.com/docs/protocol/#error-codes -type SourceError_Code int32 - -const ( - // This is the default value and should not be used. In connectrpc, this - // is "OK", but is not actually written out because _go_. - SourceError_UNSPECIFIED SourceError_Code = 0 - // CodeCanceled indicates that the operation was canceled, typically by the - // caller. - // - // HTTP Code: 499 Client Closed Request - SourceError_CANCELED SourceError_Code = 1 - // CodeUnknown indicates that the operation failed for an unknown reason. - // - // HTTP Code: 500 Internal Server Error - SourceError_UNKNOWN SourceError_Code = 2 - // CodeInvalidArgument indicates that client supplied an invalid argument. - // - // HTTP Code: 400 Bad Request - SourceError_INVALID_ARGUMENT SourceError_Code = 3 - // CodeDeadlineExceeded indicates that deadline expired before the operation - // could complete. - // - // HTTP Code: 504 Gateway Timeout - SourceError_DEADLINE_EXCEEDED SourceError_Code = 4 - // CodeNotFound indicates that some requested entity (for example, a file or - // directory) was not found. - // - // HTTP Code: 404 Not Found - SourceError_NOT_FOUND SourceError_Code = 5 - // CodeAlreadyExists indicates that client attempted to create an entity (for - // example, a file or directory) that already exists. - // - // HTTP Code: 409 Conflict - SourceError_ALREADY_EXISTS SourceError_Code = 6 - // CodePermissionDenied indicates that the caller doesn't have permission to - // execute the specified operation. - // - // HTTP Code: 403 Forbidden - SourceError_PERMISSION_DENIED SourceError_Code = 7 - // CodeResourceExhausted indicates that some resource has been exhausted. For - // example, a per-user quota may be exhausted or the entire file system may - // be full. - // - // HTTP Code: 429 Too Many Requests - SourceError_RESOURCE_EXHAUSTED SourceError_Code = 8 - // CodeFailedPrecondition indicates that the system is not in a state - // required for the operation's execution. - // - // HTTP Code: 400 Bad Request - SourceError_FAILED_PRECONDITION SourceError_Code = 9 - // CodeAborted indicates that operation was aborted by the system, usually - // because of a concurrency issue such as a sequencer check failure or - // transaction abort. - // - // HTTP Code: 409 Conflict - SourceError_ABORTED SourceError_Code = 10 - // CodeOutOfRange indicates that the operation was attempted past the valid - // range (for example, seeking past end-of-file). - // - // HTTP Code: 400 Bad Request - SourceError_OUT_OF_RANGE SourceError_Code = 11 - // CodeUnimplemented indicates that the operation isn't implemented, - // supported, or enabled in this service. - // - // HTTP Code: 501 Not Implemented - SourceError_UNIMPLEMENTED SourceError_Code = 12 - // CodeInternal indicates that some invariants expected by the underlying - // system have been broken. This code is reserved for serious errors. - // - // HTTP Code: 500 Internal Server Error - SourceError_INTERNAL SourceError_Code = 13 - // CodeUnavailable indicates that the service is currently unavailable. This - // is usually temporary, so clients can back off and retry idempotent - // operations. - // - // HTTP Code: 503 Service Unavailable - SourceError_UNAVAILABLE SourceError_Code = 14 - // CodeDataLoss indicates that the operation has resulted in unrecoverable - // data loss or corruption. - // - // HTTP Code: 500 Internal Server Error - SourceError_DATA_LOSS SourceError_Code = 15 - // CodeUnauthenticated indicates that the request does not have valid - // authentication credentials for the operation. - // - // HTTP Code: 401 Unauthorized - SourceError_UNAUTHENTICATED SourceError_Code = 16 -) - -// Enum value maps for SourceError_Code. -var ( - SourceError_Code_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "CANCELED", - 2: "UNKNOWN", - 3: "INVALID_ARGUMENT", - 4: "DEADLINE_EXCEEDED", - 5: "NOT_FOUND", - 6: "ALREADY_EXISTS", - 7: "PERMISSION_DENIED", - 8: "RESOURCE_EXHAUSTED", - 9: "FAILED_PRECONDITION", - 10: "ABORTED", - 11: "OUT_OF_RANGE", - 12: "UNIMPLEMENTED", - 13: "INTERNAL", - 14: "UNAVAILABLE", - 15: "DATA_LOSS", - 16: "UNAUTHENTICATED", - } - SourceError_Code_value = map[string]int32{ - "UNSPECIFIED": 0, - "CANCELED": 1, - "UNKNOWN": 2, - "INVALID_ARGUMENT": 3, - "DEADLINE_EXCEEDED": 4, - "NOT_FOUND": 5, - "ALREADY_EXISTS": 6, - "PERMISSION_DENIED": 7, - "RESOURCE_EXHAUSTED": 8, - "FAILED_PRECONDITION": 9, - "ABORTED": 10, - "OUT_OF_RANGE": 11, - "UNIMPLEMENTED": 12, - "INTERNAL": 13, - "UNAVAILABLE": 14, - "DATA_LOSS": 15, - "UNAUTHENTICATED": 16, - } -) - -func (x SourceError_Code) Enum() *SourceError_Code { - p := new(SourceError_Code) - *p = x - return p -} - -func (x SourceError_Code) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SourceError_Code) Descriptor() protoreflect.EnumDescriptor { - return file_logs_proto_enumTypes[2].Descriptor() -} - -func (SourceError_Code) Type() protoreflect.EnumType { - return &file_logs_proto_enumTypes[2] -} - -func (x SourceError_Code) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SourceError_Code.Descriptor instead. -func (SourceError_Code) EnumDescriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{6, 0} -} - -// The request to get log records from the upstream API. -type GetLogRecordsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The scope of the logs to get. This comes from the item that the LogStream - // was attached to and ensures that the `NATSGetLogRecordsRequest` is - // received by the same source that sent the item in the first place - Scope string `protobuf:"bytes,1,opt,name=scope,proto3" json:"scope,omitempty"` - // The query that was provided in the `LogStreamDetails` . The format of this - // is determined by the source, and will contain enough information for the - // source to successfully query the upstream API that contains the logs - Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` - // The start point for the logs - From *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"` - // The end point for the logs - To *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"` - // The maximum number of records to return. Set to zero (`0`) to return all. - MaxRecords int32 `protobuf:"varint,5,opt,name=maxRecords,proto3" json:"maxRecords,omitempty"` - // If the value is true, the earliest log events are returned first. If the - // value is false, the latest log events are returned first. The default - // value is false. - StartFromOldest bool `protobuf:"varint,6,opt,name=startFromOldest,proto3" json:"startFromOldest,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetLogRecordsRequest) Reset() { - *x = GetLogRecordsRequest{} - mi := &file_logs_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetLogRecordsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetLogRecordsRequest) ProtoMessage() {} - -func (x *GetLogRecordsRequest) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[0] - 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 GetLogRecordsRequest.ProtoReflect.Descriptor instead. -func (*GetLogRecordsRequest) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{0} -} - -func (x *GetLogRecordsRequest) GetScope() string { - if x != nil { - return x.Scope - } - return "" -} - -func (x *GetLogRecordsRequest) GetQuery() string { - if x != nil { - return x.Query - } - return "" -} - -func (x *GetLogRecordsRequest) GetFrom() *timestamppb.Timestamp { - if x != nil { - return x.From - } - return nil -} - -func (x *GetLogRecordsRequest) GetTo() *timestamppb.Timestamp { - if x != nil { - return x.To - } - return nil -} - -func (x *GetLogRecordsRequest) GetMaxRecords() int32 { - if x != nil { - return x.MaxRecords - } - return 0 -} - -func (x *GetLogRecordsRequest) GetStartFromOldest() bool { - if x != nil { - return x.StartFromOldest - } - return false -} - -// Each chunk is gonna be a page of the underlying APIs pagination. -// The source is expected to use sane defaults within the limits of the -// underlying API and SDP capabilities (message size, etc). -// -// This chunking can also be re-used for live streaming in the future. -// -// Note that the results are expected to be returned in ascending (oldest -// to newest) order. -type GetLogRecordsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Records []*LogRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetLogRecordsResponse) Reset() { - *x = GetLogRecordsResponse{} - mi := &file_logs_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetLogRecordsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetLogRecordsResponse) ProtoMessage() {} - -func (x *GetLogRecordsResponse) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[1] - 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 GetLogRecordsResponse.ProtoReflect.Descriptor instead. -func (*GetLogRecordsResponse) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{1} -} - -func (x *GetLogRecordsResponse) GetRecords() []*LogRecord { - if x != nil { - return x.Records - } - return nil -} - -// Represents a single entry in a LogStream. Roughly a "line" in traditional -// terms, but nowadays often with more details, additional structure, etc. -// -// This is chiefly modelled on the OpenTelemetry log data model: -// https://opentelemetry.io/docs/specs/otel/logs/data-model/ -type LogRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - // "Time when the event occurred measured by the origin clock, i.e. the time - // at the source." - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-timestamp - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=createdAt,proto3,oneof" json:"createdAt,omitempty"` - // "Time when the event was observed by the collection system." - // This can be used if no `createdAt` timestamp is available. - // Client libraries are encouraged to provide a singular getter that returns - // our best guess for ease of use: createdAt if available, else observedAt. - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-observedtimestamp - ObservedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=observedAt,proto3,oneof" json:"observedAt,omitempty"` - // See the definitions in - // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber - // Each source should document its mapping to this standard, e.g. following - // the examples in - // https://opentelemetry.io/docs/specs/otel/logs/data-model-appendix/#appendix-b-severitynumber-example-mappings - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber - Severity LogSeverity `protobuf:"varint,3,opt,name=severity,proto3,enum=logs.LogSeverity" json:"severity,omitempty"` - // the string form of the `body`. Can be empty if the upstream API only - // provides structured records. A Source can decide in this case to render a - // default field here if available. - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-body - Body string `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` - // "Describes the source of the log", as provided by the upstream API. - // This is arbitrary metadata from the upstream API as interpreted by the - // source. May be empty. Should use standard OTel attribute names where - // applicable. - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-resource - Resource *structpb.Struct `protobuf:"bytes,5,opt,name=resource,proto3,oneof" json:"resource,omitempty"` - // "Additional information about the specific event occurrence." This is - // arbitrary metadata from the upstream API as interpreted by the source. - // May be empty, may contain error and exception attributes. - // - // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-attributes - Attributes *structpb.Struct `protobuf:"bytes,6,opt,name=attributes,proto3,oneof" json:"attributes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *LogRecord) Reset() { - *x = LogRecord{} - mi := &file_logs_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *LogRecord) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LogRecord) ProtoMessage() {} - -func (x *LogRecord) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[2] - 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 LogRecord.ProtoReflect.Descriptor instead. -func (*LogRecord) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{2} -} - -func (x *LogRecord) GetCreatedAt() *timestamppb.Timestamp { - if x != nil { - return x.CreatedAt - } - return nil -} - -func (x *LogRecord) GetObservedAt() *timestamppb.Timestamp { - if x != nil { - return x.ObservedAt - } - return nil -} - -func (x *LogRecord) GetSeverity() LogSeverity { - if x != nil { - return x.Severity - } - return LogSeverity_UNSPECIFIED -} - -func (x *LogRecord) GetBody() string { - if x != nil { - return x.Body - } - return "" -} - -func (x *LogRecord) GetResource() *structpb.Struct { - if x != nil { - return x.Resource - } - return nil -} - -func (x *LogRecord) GetAttributes() *structpb.Struct { - if x != nil { - return x.Attributes - } - return nil -} - -// A quick passthrough to keep the NATS message format consistent. -type NATSGetLogRecordsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Request *GetLogRecordsRequest `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *NATSGetLogRecordsRequest) Reset() { - *x = NATSGetLogRecordsRequest{} - mi := &file_logs_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *NATSGetLogRecordsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*NATSGetLogRecordsRequest) ProtoMessage() {} - -func (x *NATSGetLogRecordsRequest) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[3] - 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 NATSGetLogRecordsRequest.ProtoReflect.Descriptor instead. -func (*NATSGetLogRecordsRequest) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{3} -} - -func (x *NATSGetLogRecordsRequest) GetRequest() *GetLogRecordsRequest { - if x != nil { - return x.Request - } - return nil -} - -type NATSGetLogRecordsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Content: - // - // *NATSGetLogRecordsResponse_Status - // *NATSGetLogRecordsResponse_Response - Content isNATSGetLogRecordsResponse_Content `protobuf_oneof:"content"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *NATSGetLogRecordsResponse) Reset() { - *x = NATSGetLogRecordsResponse{} - mi := &file_logs_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *NATSGetLogRecordsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*NATSGetLogRecordsResponse) ProtoMessage() {} - -func (x *NATSGetLogRecordsResponse) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[4] - 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 NATSGetLogRecordsResponse.ProtoReflect.Descriptor instead. -func (*NATSGetLogRecordsResponse) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{4} -} - -func (x *NATSGetLogRecordsResponse) GetContent() isNATSGetLogRecordsResponse_Content { - if x != nil { - return x.Content - } - return nil -} - -func (x *NATSGetLogRecordsResponse) GetStatus() *NATSGetLogRecordsResponseStatus { - if x != nil { - if x, ok := x.Content.(*NATSGetLogRecordsResponse_Status); ok { - return x.Status - } - } - return nil -} - -func (x *NATSGetLogRecordsResponse) GetResponse() *GetLogRecordsResponse { - if x != nil { - if x, ok := x.Content.(*NATSGetLogRecordsResponse_Response); ok { - return x.Response - } - } - return nil -} - -type isNATSGetLogRecordsResponse_Content interface { - isNATSGetLogRecordsResponse_Content() -} - -type NATSGetLogRecordsResponse_Status struct { - // The status of the request. This is sent before any log records are - // sent, and then if an error occurs, or the request is finished. This - // provides signalling of the "method call" over NATS. - Status *NATSGetLogRecordsResponseStatus `protobuf:"bytes,1,opt,name=status,proto3,oneof"` -} - -type NATSGetLogRecordsResponse_Response struct { - // A set of log records (lines). These should be batched in whatever way - // that the upstream provider batches them. For example if the API that - // you are pulling the logs from returns them in pages of 50, then you - // should return 50 log records in each response, and send the response - // on before requesting the next page from the API. - Response *GetLogRecordsResponse `protobuf:"bytes,2,opt,name=response,proto3,oneof"` -} - -func (*NATSGetLogRecordsResponse_Status) isNATSGetLogRecordsResponse_Content() {} - -func (*NATSGetLogRecordsResponse_Response) isNATSGetLogRecordsResponse_Content() {} - -type NATSGetLogRecordsResponseStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status NATSGetLogRecordsResponseStatus_Status `protobuf:"varint,1,opt,name=status,proto3,enum=logs.NATSGetLogRecordsResponseStatus_Status" json:"status,omitempty"` - // Only populated when the status is ERRORED - Error *SourceError `protobuf:"bytes,2,opt,name=error,proto3,oneof" json:"error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *NATSGetLogRecordsResponseStatus) Reset() { - *x = NATSGetLogRecordsResponseStatus{} - mi := &file_logs_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *NATSGetLogRecordsResponseStatus) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*NATSGetLogRecordsResponseStatus) ProtoMessage() {} - -func (x *NATSGetLogRecordsResponseStatus) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[5] - 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 NATSGetLogRecordsResponseStatus.ProtoReflect.Descriptor instead. -func (*NATSGetLogRecordsResponseStatus) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{5} -} - -func (x *NATSGetLogRecordsResponseStatus) GetStatus() NATSGetLogRecordsResponseStatus_Status { - if x != nil { - return x.Status - } - return NATSGetLogRecordsResponseStatus_UNSPECIFIED -} - -func (x *NATSGetLogRecordsResponseStatus) GetError() *SourceError { - if x != nil { - return x.Error - } - return nil -} - -type SourceError struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The error code - Code SourceError_Code `protobuf:"varint,1,opt,name=code,proto3,enum=logs.SourceError_Code" json:"code,omitempty"` - // The error message - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - // Whether this error comes from the upstream API or not. Errors that come - // from the upstream API will result in the user-facing RPC returning a - // `code.Aborted` error, with the `NatsError` embedded in the `Detail` - // field. This differentiates between errors that were part of Overmind - // (like the source panicking) and errors that come from the upstream (like - // Datadog having an outage) - Upstream bool `protobuf:"varint,3,opt,name=upstream,proto3" json:"upstream,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SourceError) Reset() { - *x = SourceError{} - mi := &file_logs_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SourceError) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SourceError) ProtoMessage() {} - -func (x *SourceError) ProtoReflect() protoreflect.Message { - mi := &file_logs_proto_msgTypes[6] - 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 SourceError.ProtoReflect.Descriptor instead. -func (*SourceError) Descriptor() ([]byte, []int) { - return file_logs_proto_rawDescGZIP(), []int{6} -} - -func (x *SourceError) GetCode() SourceError_Code { - if x != nil { - return x.Code - } - return SourceError_UNSPECIFIED -} - -func (x *SourceError) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *SourceError) GetUpstream() bool { - if x != nil { - return x.Upstream - } - return false -} - -var File_logs_proto protoreflect.FileDescriptor - -const file_logs_proto_rawDesc = "" + - "\n" + - "\n" + - "logs.proto\x12\x04logs\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe8\x01\n" + - "\x14GetLogRecordsRequest\x12\x14\n" + - "\x05scope\x18\x01 \x01(\tR\x05scope\x12\x14\n" + - "\x05query\x18\x02 \x01(\tR\x05query\x12.\n" + - "\x04from\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04from\x12*\n" + - "\x02to\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x02to\x12\x1e\n" + - "\n" + - "maxRecords\x18\x05 \x01(\x05R\n" + - "maxRecords\x12(\n" + - "\x0fstartFromOldest\x18\x06 \x01(\bR\x0fstartFromOldest\"B\n" + - "\x15GetLogRecordsResponse\x12)\n" + - "\arecords\x18\x01 \x03(\v2\x0f.logs.LogRecordR\arecords\"\xff\x02\n" + - "\tLogRecord\x12=\n" + - "\tcreatedAt\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\tcreatedAt\x88\x01\x01\x12?\n" + - "\n" + - "observedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + - "observedAt\x88\x01\x01\x12-\n" + - "\bseverity\x18\x03 \x01(\x0e2\x11.logs.LogSeverityR\bseverity\x12\x12\n" + - "\x04body\x18\x04 \x01(\tR\x04body\x128\n" + - "\bresource\x18\x05 \x01(\v2\x17.google.protobuf.StructH\x02R\bresource\x88\x01\x01\x12<\n" + - "\n" + - "attributes\x18\x06 \x01(\v2\x17.google.protobuf.StructH\x03R\n" + - "attributes\x88\x01\x01B\f\n" + - "\n" + - "_createdAtB\r\n" + - "\v_observedAtB\v\n" + - "\t_resourceB\r\n" + - "\v_attributes\"P\n" + - "\x18NATSGetLogRecordsRequest\x124\n" + - "\arequest\x18\x01 \x01(\v2\x1a.logs.GetLogRecordsRequestR\arequest\"\xa2\x01\n" + - "\x19NATSGetLogRecordsResponse\x12?\n" + - "\x06status\x18\x01 \x01(\v2%.logs.NATSGetLogRecordsResponseStatusH\x00R\x06status\x129\n" + - "\bresponse\x18\x02 \x01(\v2\x1b.logs.GetLogRecordsResponseH\x00R\bresponseB\t\n" + - "\acontent\"\xe2\x01\n" + - "\x1fNATSGetLogRecordsResponseStatus\x12D\n" + - "\x06status\x18\x01 \x01(\x0e2,.logs.NATSGetLogRecordsResponseStatus.StatusR\x06status\x12,\n" + - "\x05error\x18\x02 \x01(\v2\x11.logs.SourceErrorH\x00R\x05error\x88\x01\x01\"A\n" + - "\x06Status\x12\x0f\n" + - "\vUNSPECIFIED\x10\x00\x12\v\n" + - "\aSTARTED\x10\x01\x12\f\n" + - "\bFINISHED\x10\x02\x12\v\n" + - "\aERRORED\x10\x03B\b\n" + - "\x06_error\"\xb1\x03\n" + - "\vSourceError\x12*\n" + - "\x04code\x18\x01 \x01(\x0e2\x16.logs.SourceError.CodeR\x04code\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12\x1a\n" + - "\bupstream\x18\x03 \x01(\bR\bupstream\"\xbf\x02\n" + - "\x04Code\x12\x0f\n" + - "\vUNSPECIFIED\x10\x00\x12\f\n" + - "\bCANCELED\x10\x01\x12\v\n" + - "\aUNKNOWN\x10\x02\x12\x14\n" + - "\x10INVALID_ARGUMENT\x10\x03\x12\x15\n" + - "\x11DEADLINE_EXCEEDED\x10\x04\x12\r\n" + - "\tNOT_FOUND\x10\x05\x12\x12\n" + - "\x0eALREADY_EXISTS\x10\x06\x12\x15\n" + - "\x11PERMISSION_DENIED\x10\a\x12\x16\n" + - "\x12RESOURCE_EXHAUSTED\x10\b\x12\x17\n" + - "\x13FAILED_PRECONDITION\x10\t\x12\v\n" + - "\aABORTED\x10\n" + - "\x12\x10\n" + - "\fOUT_OF_RANGE\x10\v\x12\x11\n" + - "\rUNIMPLEMENTED\x10\f\x12\f\n" + - "\bINTERNAL\x10\r\x12\x0f\n" + - "\vUNAVAILABLE\x10\x0e\x12\r\n" + - "\tDATA_LOSS\x10\x0f\x12\x13\n" + - "\x0fUNAUTHENTICATED\x10\x10*\xb0\x02\n" + - "\vLogSeverity\x12\x0f\n" + - "\vUNSPECIFIED\x10\x00\x12\t\n" + - "\x05TRACE\x10\x01\x12\n" + - "\n" + - "\x06TRACE2\x10\x02\x12\n" + - "\n" + - "\x06TRACE3\x10\x03\x12\n" + - "\n" + - "\x06TRACE4\x10\x04\x12\t\n" + - "\x05DEBUG\x10\x05\x12\n" + - "\n" + - "\x06DEBUG2\x10\x06\x12\n" + - "\n" + - "\x06DEBUG3\x10\a\x12\n" + - "\n" + - "\x06DEBUG4\x10\b\x12\b\n" + - "\x04INFO\x10\t\x12\t\n" + - "\x05INFO2\x10\n" + - "\x12\t\n" + - "\x05INFO3\x10\v\x12\t\n" + - "\x05INFO4\x10\f\x12\b\n" + - "\x04WARN\x10\r\x12\t\n" + - "\x05WARN2\x10\x0e\x12\t\n" + - "\x05WARN3\x10\x0f\x12\t\n" + - "\x05WARN4\x10\x10\x12\t\n" + - "\x05ERROR\x10\x11\x12\n" + - "\n" + - "\x06ERROR2\x10\x12\x12\n" + - "\n" + - "\x06ERROR3\x10\x13\x12\n" + - "\n" + - "\x06ERROR4\x10\x14\x12\t\n" + - "\x05FATAL\x10\x15\x12\n" + - "\n" + - "\x06FATAL2\x10\x16\x12\n" + - "\n" + - "\x06FATAL3\x10\x17\x12\n" + - "\n" + - "\x06FATAL4\x10\x182Y\n" + - "\vLogsService\x12J\n" + - "\rGetLogRecords\x12\x1a.logs.GetLogRecordsRequest\x1a\x1b.logs.GetLogRecordsResponse0\x01B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_logs_proto_rawDescOnce sync.Once - file_logs_proto_rawDescData []byte -) - -func file_logs_proto_rawDescGZIP() []byte { - file_logs_proto_rawDescOnce.Do(func() { - file_logs_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_logs_proto_rawDesc), len(file_logs_proto_rawDesc))) - }) - return file_logs_proto_rawDescData -} - -var file_logs_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_logs_proto_goTypes = []any{ - (LogSeverity)(0), // 0: logs.LogSeverity - (NATSGetLogRecordsResponseStatus_Status)(0), // 1: logs.NATSGetLogRecordsResponseStatus.Status - (SourceError_Code)(0), // 2: logs.SourceError.Code - (*GetLogRecordsRequest)(nil), // 3: logs.GetLogRecordsRequest - (*GetLogRecordsResponse)(nil), // 4: logs.GetLogRecordsResponse - (*LogRecord)(nil), // 5: logs.LogRecord - (*NATSGetLogRecordsRequest)(nil), // 6: logs.NATSGetLogRecordsRequest - (*NATSGetLogRecordsResponse)(nil), // 7: logs.NATSGetLogRecordsResponse - (*NATSGetLogRecordsResponseStatus)(nil), // 8: logs.NATSGetLogRecordsResponseStatus - (*SourceError)(nil), // 9: logs.SourceError - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 11: google.protobuf.Struct -} -var file_logs_proto_depIdxs = []int32{ - 10, // 0: logs.GetLogRecordsRequest.from:type_name -> google.protobuf.Timestamp - 10, // 1: logs.GetLogRecordsRequest.to:type_name -> google.protobuf.Timestamp - 5, // 2: logs.GetLogRecordsResponse.records:type_name -> logs.LogRecord - 10, // 3: logs.LogRecord.createdAt:type_name -> google.protobuf.Timestamp - 10, // 4: logs.LogRecord.observedAt:type_name -> google.protobuf.Timestamp - 0, // 5: logs.LogRecord.severity:type_name -> logs.LogSeverity - 11, // 6: logs.LogRecord.resource:type_name -> google.protobuf.Struct - 11, // 7: logs.LogRecord.attributes:type_name -> google.protobuf.Struct - 3, // 8: logs.NATSGetLogRecordsRequest.request:type_name -> logs.GetLogRecordsRequest - 8, // 9: logs.NATSGetLogRecordsResponse.status:type_name -> logs.NATSGetLogRecordsResponseStatus - 4, // 10: logs.NATSGetLogRecordsResponse.response:type_name -> logs.GetLogRecordsResponse - 1, // 11: logs.NATSGetLogRecordsResponseStatus.status:type_name -> logs.NATSGetLogRecordsResponseStatus.Status - 9, // 12: logs.NATSGetLogRecordsResponseStatus.error:type_name -> logs.SourceError - 2, // 13: logs.SourceError.code:type_name -> logs.SourceError.Code - 3, // 14: logs.LogsService.GetLogRecords:input_type -> logs.GetLogRecordsRequest - 4, // 15: logs.LogsService.GetLogRecords:output_type -> logs.GetLogRecordsResponse - 15, // [15:16] is the sub-list for method output_type - 14, // [14:15] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name -} - -func init() { file_logs_proto_init() } -func file_logs_proto_init() { - if File_logs_proto != nil { - return - } - file_logs_proto_msgTypes[2].OneofWrappers = []any{} - file_logs_proto_msgTypes[4].OneofWrappers = []any{ - (*NATSGetLogRecordsResponse_Status)(nil), - (*NATSGetLogRecordsResponse_Response)(nil), - } - file_logs_proto_msgTypes[5].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_logs_proto_rawDesc), len(file_logs_proto_rawDesc)), - NumEnums: 3, - NumMessages: 7, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_logs_proto_goTypes, - DependencyIndexes: file_logs_proto_depIdxs, - EnumInfos: file_logs_proto_enumTypes, - MessageInfos: file_logs_proto_msgTypes, - }.Build() - File_logs_proto = out.File - file_logs_proto_goTypes = nil - file_logs_proto_depIdxs = nil -} diff --git a/sdp-go/logs_test.go b/sdp-go/logs_test.go deleted file mode 100644 index 64994832..00000000 --- a/sdp-go/logs_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package sdp - -import ( - "testing" - "time" - - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestGetLogRecordsRequest_Validate(t *testing.T) { - now := time.Now() - pastTime := now.Add(-1 * time.Hour) - futureTime := now.Add(1 * time.Hour) - - tests := []struct { - name string - req *GetLogRecordsRequest - wantErr bool - }{ - { - name: "Nil request", - req: nil, - wantErr: true, - }, - { - name: "Empty scope", - req: &GetLogRecordsRequest{ - Scope: "", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "Empty query", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "Missing from timestamp", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: nil, - To: timestamppb.New(now), - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "Missing to timestamp", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: nil, - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "From after to", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(futureTime), - To: timestamppb.New(pastTime), - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "MaxRecords zero", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: 0, - StartFromOldest: false, - }, - wantErr: false, - }, - { - name: "MaxRecords negative", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: -10, - StartFromOldest: false, - }, - wantErr: true, - }, - { - name: "Valid request with MaxRecords", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: 100, - StartFromOldest: false, - }, - wantErr: false, - }, - { - name: "Valid request without MaxRecords", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(pastTime), - To: timestamppb.New(now), - MaxRecords: 0, - StartFromOldest: false, - }, - wantErr: false, - }, - { - name: "Valid request with equal timestamps", - req: &GetLogRecordsRequest{ - Scope: "valid-scope", - Query: "valid-query", - From: timestamppb.New(now), - To: timestamppb.New(now), - MaxRecords: 100, - StartFromOldest: true, - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.req.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("GetLogRecordsRequest.Validate() error = %v, wantErr %v\nrequest = %v", err, tt.wantErr, tt.req) - } - }) - } -} diff --git a/sdp-go/progress.go b/sdp-go/progress.go deleted file mode 100644 index e009fcd8..00000000 --- a/sdp-go/progress.go +++ /dev/null @@ -1,721 +0,0 @@ -package sdp - -import ( - "context" - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/getsentry/sentry-go" - "github.com/google/uuid" - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/types/known/durationpb" -) - -// DefaultResponseInterval is the default period of time within which responses -// are sent (5 seconds) -const DefaultResponseInterval = (5 * time.Second) - -// DefaultStartTimeout is the default period of time to wait for the first -// response on a query. If no response is received in this time, the query will -// be marked as complete. -const DefaultStartTimeout = 2000 * time.Millisecond - -// ResponseSender is a struct responsible for sending responses out on behalf of -// agents that are working on that request. Think of it as the agent side -// component of Responder -type ResponseSender struct { - // How often to send responses. The expected next update will be 230% of - // this value, allowing for one-and-a-bit missed responses before it is - // marked as stalled - ResponseInterval time.Duration - ResponseSubject string - monitorRunning sync.WaitGroup - monitorKill chan *Response // Sending to this channel will kill the response sender goroutine and publish the sent message as last msg on the subject - responderName string - responderId uuid.UUID - connection EncodedConnection - responseCtx context.Context -} - -// Start sends the first response on the given subject and connection to say -// that the request is being worked on. It also starts a go routine to continue -// sending responses. -// -// The user should make sure to call Done(), Error() or Cancel() once the query -// has finished to make sure this process stops sending responses. The sender -// will also be stopped if the context is cancelled -func (rs *ResponseSender) Start(ctx context.Context, ec EncodedConnection, responderName string, responderId uuid.UUID) { - rs.monitorKill = make(chan *Response, 1) - rs.responseCtx = ctx - - // Set the default if it's not set - if rs.ResponseInterval == 0 { - rs.ResponseInterval = DefaultResponseInterval - } - - // Tell it to expect the next update in 230% of the expected time. This - // allows for a response getting lost, plus some delay - nextUpdateIn := durationpb.New(time.Duration((float64(rs.ResponseInterval) * 2.3))) - - // Set struct values - rs.responderName = responderName - rs.responderId = responderId - rs.connection = ec - - // Create the response before starting the goroutine since it only needs to - // be done once - resp := Response{ - Responder: rs.responderName, - ResponderUUID: rs.responderId[:], - State: ResponderState_WORKING, - NextUpdateIn: nextUpdateIn, - } - - if rs.connection != nil { - // Send the initial response - err := rs.connection.Publish( - ctx, - rs.ResponseSubject, - &QueryResponse{ResponseType: &QueryResponse_Response{Response: &resp}}, - ) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Error publishing initial response") - } - } - - rs.monitorRunning.Add(1) - - // Start a goroutine to send further responses - go func() { - defer tracing.LogRecoverToReturn(ctx, "ResponseSender ticker") - // confirm closure on exit - defer rs.monitorRunning.Done() - - if ec == nil { - return - } - tick := time.NewTicker(rs.ResponseInterval) - defer tick.Stop() - - for { - var err error - - select { - case <-rs.monitorKill: - return - case <-ctx.Done(): - return - case <-tick.C: - err = rs.connection.Publish( - ctx, - rs.ResponseSubject, - &QueryResponse{ResponseType: &QueryResponse_Response{Response: &resp}}, - ) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Error publishing response") - } - } - } - }() -} - -// Kill Kills the response sender immediately. This should be used if something -// has failed and you don't want to send a completed response -// -// Deprecated: Use KillWithContext(ctx) instead -func (rs *ResponseSender) Kill() { - rs.killWithResponse(context.Background(), nil) -} - -// KillWithContext Kills the response sender immediately. This should be used if something -// has failed and you don't want to send a completed response -func (rs *ResponseSender) KillWithContext(ctx context.Context) { - rs.killWithResponse(ctx, nil) -} - -func (rs *ResponseSender) killWithResponse(ctx context.Context, r *Response) { - // close the channel to kill the sender - close(rs.monitorKill) - - // wait for the sender to be actually done - rs.monitorRunning.Wait() - - if rs.connection != nil { - if r != nil { - // Send the final response - err := rs.connection.Publish(ctx, rs.ResponseSubject, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: r, - }, - }) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Error publishing final response") - } - } - } -} - -// Done kills the responder but sends a final completion message -// -// Deprecated: Use DoneWithContext(ctx) instead -func (rs *ResponseSender) Done() { - rs.DoneWithContext(context.Background()) -} - -// DoneWithContext kills the responder but sends a final completion message -func (rs *ResponseSender) DoneWithContext(ctx context.Context) { - resp := Response{ - Responder: rs.responderName, - ResponderUUID: rs.responderId[:], - State: ResponderState_COMPLETE, - } - rs.killWithResponse(ctx, &resp) -} - -// Error marks the request and completed with error, and sends the final error -// response -// -// Deprecated: Use ErrorWithContext(ctx) instead -func (rs *ResponseSender) Error() { - rs.ErrorWithContext(context.Background()) -} - -// ErrorWithContext marks the request and completed with error, and sends the final error -// response -func (rs *ResponseSender) ErrorWithContext(ctx context.Context) { - resp := Response{ - Responder: rs.responderName, - ResponderUUID: rs.responderId[:], - State: ResponderState_ERROR, - } - rs.killWithResponse(ctx, &resp) -} - -// Cancel Marks the request as CANCELLED and sends the final response -// -// Deprecated: Use CancelWithContext(ctx) instead -func (rs *ResponseSender) Cancel() { - rs.CancelWithContext(context.Background()) -} - -// CancelWithContext Marks the request as CANCELLED and sends the final response -func (rs *ResponseSender) CancelWithContext(ctx context.Context) { - resp := Response{ - Responder: rs.responderName, - ResponderUUID: rs.responderId[:], - State: ResponderState_CANCELLED, - } - rs.killWithResponse(ctx, &resp) -} - -type lastResponse struct { - Response *Response - Timestamp time.Time -} - -// Checks to see if this responder is stalled. If it is, it will update the -// responder state to ResponderState_STALLED. Only runs if the responder is in -// the WORKING state, doesn't do anything otherwise. -func (l *lastResponse) checkStalled() { - if l.Response == nil || l.Response.GetState() != ResponderState_WORKING { - return - } - - // Calculate if it's stalled, but only if it has a `NextUpdateIn` value. - // Responders that do not provided a `NextUpdateIn` value are not considered - // for stalling - timeSinceLastUpdate := time.Since(l.Timestamp) - timeToNextUpdate := l.Response.GetNextUpdateIn().AsDuration() - if timeToNextUpdate > 0 && timeSinceLastUpdate > timeToNextUpdate { - l.Response.State = ResponderState_STALLED - } -} - -// SourceQuery represents the status of a query -type SourceQuery struct { - // A map of ResponderUUIDs to the last response we got from them - responders map[uuid.UUID]*lastResponse - respondersMu sync.Mutex - - // Channel storage for sending back to the user - responseChan chan<- *QueryResponse - - // Use to make sure a user doesn't try to start a request twice. This is an - // atomic to allow tests to directly inject messages using - // `handleQueryResponse` - startTimeoutElapsed atomic.Bool - - querySub *nats.Subscription - - cancel context.CancelFunc -} - -// The current progress of the tracked query -type SourceQueryProgress struct { - // How many responders are currently working on this query. This means they - // are active sending updates - Working int - - // Stalled responders are ones that have sent updates in the past, but the - // latest update is overdue. This likely indicates a problem with the - // responder - Stalled int - - // Responders that are complete - Complete int - - // Responders that failed - Error int - - // Responders that were cancelled. When cancelling the SourceQueryProgress - // does not wait for responders to acknowledge the cancellation, it simply - // sends the message and marks all responders that are currently "working" - // as "cancelled". It is possible that a responder will self-report - // cancellation, but given the timings this is unlikely as it would need to - // be very fast - Cancelled int - - // The total number of tracked responders - Responders int -} - -// RunSourceQuery returns a pointer to a SourceQuery object with the various -// internal members initialized. A startTimeout must also be provided, feel free -// to use `DefaultStartTimeout` if you don't have a specific value in mind. -func RunSourceQuery(ctx context.Context, query *Query, startTimeout time.Duration, ec EncodedConnection, responseChan chan<- *QueryResponse) (*SourceQuery, error) { - if startTimeout == 0 { - return nil, errors.New("startTimeout must be greater than 0") - } - - if ec.Underlying() == nil { - return nil, errors.New("nil NATS connection") - } - - if responseChan == nil { - return nil, errors.New("nil response channel") - } - - if query.GetScope() == "" { - return nil, errors.New("cannot execute request with blank scope") - } - - // Generate a UUID if required - if len(query.GetUUID()) == 0 { - u := uuid.New() - query.UUID = u[:] - } - - // Calculate the correct subject to send the message on - var requestSubject string - if query.GetScope() == WILDCARD { - requestSubject = "request.all" - } else { - requestSubject = fmt.Sprintf("request.scope.%v", query.GetScope()) - } - - // Create the channel that NATS responses will come through - natsResponses := make(chan *QueryResponse) - - // Create a timer for the start timeout - startTimeoutTimer := time.NewTimer(startTimeout) - - // Subscribe to the query subject and wait for responses - querySub, err := ec.Subscribe(query.Subject(), NewQueryResponseHandler("", func(ctx context.Context, qr *QueryResponse) { //nolint:contextcheck // we pass the context in the func - natsResponses <- qr - })) - if err != nil { - return nil, err - } - - ctx, cancel := context.WithCancel(ctx) - - sq := &SourceQuery{ - responders: make(map[uuid.UUID]*lastResponse), - startTimeoutElapsed: atomic.Bool{}, - querySub: querySub, - cancel: cancel, - responseChan: responseChan, - } - - // Main processing loop. This runs is the main goroutine that tracks this - // request - go func() { - // Initialise the stall check ticker - stallCheck := time.NewTicker(500 * time.Millisecond) - defer stallCheck.Stop() - ctx, span := tracing.Tracer().Start(ctx, "QueryProgress") - defer span.End() - - query.SetSpanAttributes(span) - - for { - select { - case <-ctx.Done(): - // If the connection is closed, we do not need to send a cancel message - if u := ec.Underlying(); u == nil || u.IsClosed() { - sq.markWorkingRespondersCancelled() - sq.cleanup(ctx) - return - } - // Since this context is done, we need a new context just to - // send the cancellation message - cancelCtx, cancelCtxCancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second) - defer cancelCtxCancel() - - // Send a cancel message to all responders - cancelRequest := CancelQuery{ - UUID: query.GetUUID(), - } - - var cancelSubject string - if query.GetScope() == WILDCARD { - cancelSubject = "cancel.all" - } else { - cancelSubject = fmt.Sprintf("cancel.scope.%v", query.GetScope()) - } - - err := ec.Publish(cancelCtx, cancelSubject, &cancelRequest) - if err != nil { - log.WithContext(ctx).WithError(err).Error("Error sending cancel message") - span.RecordError(err) - } - - sq.markWorkingRespondersCancelled() - sq.cleanup(ctx) - return - case <-startTimeoutTimer.C: - sq.startTimeoutElapsed.Store(true) - - if sq.finished() { - sq.cleanup(ctx) - return - } - case response := <-natsResponses: - // Handle the response - if sq.handleQueryResponse(ctx, response) { - // This means we are done - return - } - case <-stallCheck.C: - - // If we get here, it means that we haven't had a response - // in a while, so we should check to see if things have - // stalled - if sq.finished() { - sq.cleanup(ctx) - return - } - } - } - }() - - // Send the message to start the query - err = ec.Publish(ctx, requestSubject, query) - if err != nil { - return nil, err - } - - return sq, nil -} - -// Execute a given request and wait for it to finish, returns the items that -// were found and any errors. The third return error value will only be returned -// only if there is a problem making the request. Details of which responders -// have failed etc. should be determined using the typical methods like -// `NumError()`. -func RunSourceQuerySync(ctx context.Context, query *Query, startTimeout time.Duration, ec EncodedConnection) ([]*Item, []*Edge, []*QueryError, error) { - items := make([]*Item, 0) - edges := make([]*Edge, 0) - errs := make([]*QueryError, 0) - r := make(chan *QueryResponse, 128) - - if ec == nil { - return items, edges, errs, errors.New("nil NATS connection") - } - - _, err := RunSourceQuery(ctx, query, startTimeout, ec, r) - if err != nil { - return items, edges, errs, err - } - - // Read items and errors - for response := range r { - item := response.GetNewItem() - if item != nil { - items = append(items, item) - } - edge := response.GetEdge() - if edge != nil { - edges = append(edges, edge) - } - qErr := response.GetError() - if qErr != nil { - errs = append(errs, qErr) - } - // ignore status responses for now - // status := response.GetResponse() - // if status != nil { - // panic("qp: status not implemented yet") - // } - } - - // when the channel closes, we're done - return items, edges, errs, nil -} - -// Cancels the request, sending a cancel message to all responders and closing -// the response channel. The query can also be cancelled by cancelling the -// context that was passed in the `Start` method -func (sq *SourceQuery) Cancel() { - sq.cancel() -} - -// This is split out into its own function so that it can be tested more easily -// with out having to worry about race conditions. This returns a boolean which -// indicates if the request is complete or not -func (sq *SourceQuery) handleQueryResponse(ctx context.Context, response *QueryResponse) bool { - switch r := response.GetResponseType().(type) { - case *QueryResponse_NewItem: - sq.handleItem(r.NewItem) - case *QueryResponse_Edge: - sq.handleEdge(r.Edge) - case *QueryResponse_Error: - sq.handleError(r.Error) - case *QueryResponse_Response: - sq.handleResponse(ctx, r.Response) - - if sq.finished() { - sq.cleanup(ctx) - return true - } - } - - return false -} - -// markWorkingRespondersCancelled marks all working responders as cancelled -// internally, there is no need to wait for them to confirm the cancellation, as -// we're not going to wait for any further responses. -func (sq *SourceQuery) markWorkingRespondersCancelled() { - sq.respondersMu.Lock() - defer sq.respondersMu.Unlock() - - for _, lastResponse := range sq.responders { - if lastResponse.Response.GetState() == ResponderState_WORKING { - lastResponse.Response.State = ResponderState_CANCELLED - } - } -} - -// Whether the query should be considered finished or not. This is based on -// whether the start timeout has elapsed and all responders are done -func (sq *SourceQuery) finished() bool { - return sq.startTimeoutElapsed.Load() && sq.allDone() -} - -// Cleans up the query, unsubscribing from the query subject and closing the -// response channel -func (sq *SourceQuery) cleanup(ctx context.Context) { - span := trace.SpanFromContext(ctx) - if sq.querySub != nil && sq.querySub.IsValid() { - err := sq.querySub.Unsubscribe() - if err != nil { - log.WithField("error", err).Error("Error unsubscribing from query subject") - span.RecordError(err) - } - } - - close(sq.responseChan) - sq.cancel() -} - -// Sends the item back to the response channel, also extracts and synthesises -// edges from `LinkedItems` and `LinkedItemQueries` and sends them back too -func (sq *SourceQuery) handleItem(item *Item) { - if item == nil { - return - } - - // Send the item back over the channel - // TODO(LIQs): translation is not necessary anymore; update code and method comment - item, edges := TranslateLinksToEdges(item) - sq.responseChan <- &QueryResponse{ - ResponseType: &QueryResponse_NewItem{NewItem: item}, - } - for _, e := range edges { - sq.responseChan <- &QueryResponse{ - ResponseType: &QueryResponse_Edge{Edge: e}, - } - } -} - -// Sends the edge back to the response channel -func (sq *SourceQuery) handleEdge(edge *Edge) { - if edge == nil { - return - } - - sq.responseChan <- &QueryResponse{ - ResponseType: &QueryResponse_Edge{Edge: edge}, - } -} - -// Send the error back to the response channel -func (sq *SourceQuery) handleError(err *QueryError) { - if err == nil { - return - } - - sq.responseChan <- &QueryResponse{ - ResponseType: &QueryResponse_Error{ - Error: err, - }, - } -} - -// Update the internal state with the response -func (sq *SourceQuery) handleResponse(ctx context.Context, response *Response) { - span := trace.SpanFromContext(ctx) - - // do not deal with responses that do not have a responder UUID - ru, err := uuid.FromBytes(response.GetResponderUUID()) - if err != nil { - span.RecordError(fmt.Errorf("error parsing responder UUID: %w", err)) - return - } - - sq.respondersMu.Lock() - defer sq.respondersMu.Unlock() - - // Protect against out-of order responses. Do not mark a responder as - // working if it has already finished. this should never happen, but we want - // to know if it does as it will indicate a bug in the responder itself - last, exists := sq.responders[ru] - if exists { - if last.Response != nil { - switch last.Response.GetState() { - case ResponderState_COMPLETE, ResponderState_ERROR, ResponderState_CANCELLED: - err = fmt.Errorf("out-of-order response. Responder was already in the state %v, skipping update to %v", last.Response.String(), response.GetState().String()) - span.RecordError(err) - sentry.CaptureException(err) - return - case ResponderState_WORKING, ResponderState_STALLED: - // This is fine, we can update the state - } - } - } - - // Update the stored data - sq.responders[ru] = &lastResponse{ - Response: response, - Timestamp: time.Now(), - } -} - -// Checks whether all responders are done or not. A "Done" responder is one that -// is either: Complete, Error, Cancelled or Stalled -// -// Note that this doesn't perform locking if the mutex, this needs to be done by -// the caller -func (sq *SourceQuery) allDone() bool { - sq.respondersMu.Lock() - defer sq.respondersMu.Unlock() - - for _, lastResponse := range sq.responders { - // Recalculate the stall status - lastResponse.checkStalled() - - if lastResponse.Response.GetState() == ResponderState_WORKING { - return false - } - } - - return true -} - -// TranslateLinksToEdges Translates linked items and queries into edges. This is -// a temporary stop gap measure to allow parallel processing of items and edges -// in the gateway while allowing other parts of the system to be updated -// independently. See https://github.com/overmindtech/workspace/issues/753 -func TranslateLinksToEdges(item *Item) (*Item, []*Edge) { - // TODO(LIQs): translation is not necessary anymore; delete this method and all callsites - lis := item.GetLinkedItems() - item.LinkedItems = nil - liqs := item.GetLinkedItemQueries() - item.LinkedItemQueries = nil - - edges := []*Edge{} - - for _, li := range lis { - edges = append(edges, &Edge{ - From: item.Reference(), - To: li.GetItem(), - BlastPropagation: li.GetBlastPropagation(), - }) - } - - for _, liq := range liqs { - edges = append(edges, &Edge{ - From: item.Reference(), - To: liq.GetQuery().Reference(), - BlastPropagation: liq.GetBlastPropagation(), - }) - } - - return item, edges -} - -func (sq *SourceQuery) Progress() SourceQueryProgress { - sq.respondersMu.Lock() - defer sq.respondersMu.Unlock() - - var numWorking, numStalled, numComplete, numError, numCancelled int - - // Loop over all responders once and calculate the progress - for _, lastResponse := range sq.responders { - // Recalculate the stall status - lastResponse.checkStalled() - - switch lastResponse.Response.GetState() { - case ResponderState_WORKING: - numWorking++ - case ResponderState_STALLED: - numStalled++ - case ResponderState_COMPLETE: - numComplete++ - case ResponderState_ERROR: - numError++ - case ResponderState_CANCELLED: - numCancelled++ - } - } - - return SourceQueryProgress{ - Working: numWorking, - Stalled: numStalled, - Complete: numComplete, - Error: numError, - Cancelled: numCancelled, - Responders: len(sq.responders), - } -} - -func (sq *SourceQuery) String() string { - progress := sq.Progress() - - return fmt.Sprintf( - "Working: %v\nStalled: %v\nComplete: %v\nError: %v\nCancelled: %v\nResponders: %v\n", - progress.Working, - progress.Stalled, - progress.Complete, - progress.Error, - progress.Cancelled, - progress.Responders, - ) -} diff --git a/sdp-go/progress_test.go b/sdp-go/progress_test.go deleted file mode 100644 index 02c8adfd..00000000 --- a/sdp-go/progress_test.go +++ /dev/null @@ -1,1525 +0,0 @@ -package sdp - -import ( - "context" - "errors" - "fmt" - "math/rand" - "os" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/google/uuid" - "github.com/nats-io/nats.go" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/durationpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestRunSourceQueryParams(t *testing.T) { - u := uuid.New() - q := Query{ - Type: "person", - Method: QueryMethod_GET, - Query: "dylan", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "test", - IgnoreCache: false, - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(20 * time.Second)), - } - - t.Run("with no start timeout", func(t *testing.T) { - _, err := RunSourceQuery(t.Context(), &q, 0, nil, nil) - - if err == nil { - t.Error("expected an error when there is not startTimeout") - } - }) -} - -func TestResponseNilPublisher(t *testing.T) { - ctx := context.Background() - - rs := ResponseSender{ - ResponseInterval: (10 * time.Millisecond), - ResponseSubject: "responses", - } - - // Start sending responses with a nil connection, should not panic - rs.Start(ctx, nil, "test", uuid.New()) - - // Give it enough time for ~10 responses - time.Sleep(100 * time.Millisecond) - - // Stop - rs.DoneWithContext(ctx) -} - -func TestResponseSenderDone(t *testing.T) { - ctx := context.Background() - - rs := ResponseSender{ - ResponseInterval: (10 * time.Millisecond), - ResponseSubject: "responses", - } - - tc := TestConnection{IgnoreNoResponders: true} - - // Start sending responses - rs.Start(ctx, &tc, "test", uuid.New()) - - // Give it enough time for ~10 responses - time.Sleep(100 * time.Millisecond) - - // Stop - rs.DoneWithContext(ctx) - - // Let it drain down - time.Sleep(100 * time.Millisecond) - - // Inspect what was sent - tc.MessagesMu.Lock() - if len(tc.Messages) <= 10 { - t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) - } - - // Make sure that the final message was a completion one - finalMessage := tc.Messages[len(tc.Messages)-1] - tc.MessagesMu.Unlock() - - if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { - if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { - if finalResponse.Response.GetState() != ResponderState_COMPLETE { - t.Errorf("Expected final message state to be COMPLETE (1), found: %v", finalResponse.Response.GetState()) - } - } else { - t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) - } - } else { - t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) - } -} - -func TestResponseSenderError(t *testing.T) { - ctx := context.Background() - - rs := ResponseSender{ - ResponseInterval: (10 * time.Millisecond), - ResponseSubject: "responses", - } - - tc := TestConnection{IgnoreNoResponders: true} - - // Start sending responses - rs.Start(ctx, &tc, "test", uuid.New()) - - // Give it enough time for >10 responses - time.Sleep(120 * time.Millisecond) - - // Stop - rs.ErrorWithContext(ctx) - - // Let it drain down - time.Sleep(100 * time.Millisecond) - - // Inspect what was sent - tc.MessagesMu.Lock() - if len(tc.Messages) <= 10 { - t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) - } - - // Make sure that the final message was a completion one - finalMessage := tc.Messages[len(tc.Messages)-1] - tc.MessagesMu.Unlock() - - if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { - if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { - if finalResponse.Response.GetState() != ResponderState_ERROR { - t.Errorf("Expected final message state to be ERROR, found: %v", finalResponse.Response.GetState()) - } - } else { - t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) - } - } else { - t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) - } -} - -func TestResponseSenderCancel(t *testing.T) { - ctx := context.Background() - - rs := ResponseSender{ - ResponseInterval: (10 * time.Millisecond), - ResponseSubject: "responses", - } - - tc := TestConnection{IgnoreNoResponders: true} - - // Start sending responses - rs.Start(ctx, &tc, "test", uuid.New()) - - // Give it enough time for >10 responses - time.Sleep(120 * time.Millisecond) - - // Stop - rs.CancelWithContext(ctx) - - // Let it drain down - time.Sleep(100 * time.Millisecond) - - // Inspect what was sent - tc.MessagesMu.Lock() - if len(tc.Messages) <= 10 { - t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) - } - - // Make sure that the final message was a completion one - finalMessage := tc.Messages[len(tc.Messages)-1] - tc.MessagesMu.Unlock() - - if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { - if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { - if finalResponse.Response.GetState() != ResponderState_CANCELLED { - t.Errorf("Expected final message state to be CANCELLED, found: %v", finalResponse.Response.GetState()) - } - } else { - t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) - } - } else { - t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) - } -} - -func TestDefaultResponseInterval(t *testing.T) { - ctx := context.Background() - - rs := ResponseSender{} - - rs.Start(ctx, &TestConnection{}, "", uuid.New()) - rs.KillWithContext(ctx) - - if rs.ResponseInterval != DefaultResponseInterval { - t.Fatal("Response sender interval failed to default") - } -} - -// ExpectToMatch Checks that metrics are as expected and returns an error if not -func (expected SourceQueryProgress) ExpectToMatch(qp *SourceQuery) error { - actual := qp.Progress() - - var err error - - if expected.Working != actual.Working { - err = errors.Join(err, fmt.Errorf("Expected Working to be %v, got %v", expected.Working, actual.Working)) - } - if expected.Stalled != actual.Stalled { - err = errors.Join(err, fmt.Errorf("Expected Stalled to be %v, got %v", expected.Stalled, actual.Stalled)) - } - if expected.Complete != actual.Complete { - err = errors.Join(err, fmt.Errorf("Expected Complete to be %v, got %v", expected.Complete, actual.Complete)) - } - if expected.Error != actual.Error { - err = errors.Join(err, fmt.Errorf("Expected Error to be %v, got %v", expected.Error, actual.Error)) - } - if expected.Responders != actual.Responders { - err = errors.Join(err, fmt.Errorf("Expected Responders to be %v, got %v", expected.Responders, actual.Responders)) - } - if expected.Cancelled != actual.Cancelled { - err = errors.Join(err, fmt.Errorf("Expected Cancelled to be %v, got %v", expected.Cancelled, actual.Cancelled)) - } - - return err -} - -// Create a channel that discards everything -func devNull() chan<- *QueryResponse { - c := make(chan *QueryResponse, 128) - go func() { - for range c { - } - }() - return c -} - -func TestQueryProgressNormal(t *testing.T) { - t.Parallel() - - ctx := t.Context() - tc := TestConnection{IgnoreNoResponders: true} - sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) - if err != nil { - t.Fatal(err) - } - - ru1 := uuid.New() - ru2 := uuid.New() - ru3 := uuid.New() - t.Logf("UUIDs: %v %v %v", ru1, ru2, ru3) - - // Make sure that the details are correct initially - var expected SourceQueryProgress - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 0, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - - t.Run("Processing initial response", func(t *testing.T) { - // Test the initial response - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 1, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("Processing when other scopes also responding", func(t *testing.T) { - // Then another scope starts working - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru2[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru3[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 3, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 3, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("When some are complete and some are not", func(t *testing.T) { - time.Sleep(5 * time.Millisecond) - - // test 1 still working - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - // Test 2 finishes - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru2[:], - State: ResponderState_COMPLETE, - }, - }, - }) - - // Test 3 still working - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru3[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 2, - Stalled: 0, - Complete: 1, - Error: 0, - Responders: 3, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("When one is cancelled", func(t *testing.T) { - time.Sleep(5 * time.Millisecond) - - // test 1 still working - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - // Test 3 cancelled - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru3[:], - State: ResponderState_CANCELLED, - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 1, - Stalled: 0, - Complete: 1, - Error: 0, - Cancelled: 1, - Responders: 3, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("When the final responder finishes", func(t *testing.T) { - time.Sleep(5 * time.Millisecond) - - // Test 1 finishes - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_COMPLETE, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 2, - Error: 0, - Cancelled: 1, - Responders: 3, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - if sq.allDone() == false { - t.Error("expected allDone() to be true") - } -} - -func TestQueryProgressParallel(t *testing.T) { - t.Parallel() - - ctx := t.Context() - tc := TestConnection{IgnoreNoResponders: true} - sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) - if err != nil { - t.Fatal(err) - } - - ru1 := uuid.New() - - // Make sure that the details are correct initially - var expected SourceQueryProgress - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 0, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - - t.Run("Processing many bunched responses", func(t *testing.T) { - var wg sync.WaitGroup - - for i := 0; i != 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - // Test the initial response - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - }() - } - - wg.Wait() - - expected = SourceQueryProgress{ - Working: 1, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) -} - -func TestQueryProgressStalled(t *testing.T) { - t.Parallel() - - ctx := t.Context() - tc := TestConnection{IgnoreNoResponders: true} - sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) - if err != nil { - t.Fatal(err) - } - - ru1 := uuid.New() - - // Make sure that the details are correct initially - var expected SourceQueryProgress - - t.Run("Processing the initial response", func(t *testing.T) { - // Test the initial response - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 1, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("After a responder has stalled", func(t *testing.T) { - // Wait long enough for the thing to be marked as stalled - time.Sleep(20 * time.Millisecond) - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 1, - Complete: 0, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - - sq.respondersMu.Lock() - defer sq.respondersMu.Unlock() - if _, ok := sq.responders[ru1]; !ok { - t.Error("Could not get responder for scope test1") - } - }) - - t.Run("After a responder recovers from a stall", func(t *testing.T) { - // See if it will un-stall itself - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_COMPLETE, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 1, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - if sq.allDone() == false { - t.Error("expected allDone() to be true") - } -} - -func TestRogueResponder(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(t.Context()) - tc := TestConnection{IgnoreNoResponders: true} - sq, err := RunSourceQuery(ctx, &query, 100*time.Millisecond, &tc, devNull()) - if err != nil { - t.Fatal(err) - } - - rur := uuid.New() - - // Create our rogue responder that doesn't cancel when it should - ticker := time.NewTicker(5 * time.Second) - tickerCtx, tickerCancel := context.WithCancel(context.Background()) - defer tickerCancel() - defer ticker.Stop() - - go func() { - // Send an initial response - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: rur[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(5 * time.Second), - }, - }, - }) - - // Now start ticking - for { - select { - case <-ticker.C: - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: rur[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(5 * time.Second), - }, - }, - }) - case <-tickerCtx.Done(): - return - } - } - }() - - time.Sleep(300 * time.Millisecond) - - // Check that we've noticed the testRogue responder - if sq.allDone() == true { - t.Error("expected allDone() to be false") - } - - cancel() - - time.Sleep(100 * time.Millisecond) - - // We expect that it has been marked as cancelled, regardless of what the - // responder actually did - expected := SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 1, - Cancelled: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } -} - -func TestQueryProgressError(t *testing.T) { - t.Parallel() - - ctx := t.Context() - tc := TestConnection{IgnoreNoResponders: true} - sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) - if err != nil { - t.Fatal(err) - } - - ru1 := uuid.New() - - // Make sure that the details are correct initially - var expected SourceQueryProgress - - t.Run("Processing the initial response", func(t *testing.T) { - // Test the initial response - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(10 * time.Millisecond), - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 1, - Stalled: 0, - Complete: 0, - Error: 0, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("After a responder has failed", func(t *testing.T) { - sq.handleQueryResponse(ctx, &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_ERROR, - }, - }, - }) - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 0, - Error: 1, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - t.Run("Ensuring that a failed responder does not get marked as stalled", func(t *testing.T) { - time.Sleep(12 * time.Millisecond) - - expected = SourceQueryProgress{ - Working: 0, - Stalled: 0, - Complete: 0, - Error: 1, - Responders: 1, - } - - if err := expected.ExpectToMatch(sq); err != nil { - t.Error(err) - } - }) - - if sq.allDone() == false { - t.Error("expected allDone() to be true") - } -} - -func TestStart(t *testing.T) { - t.Parallel() - - ctx := t.Context() - tc := TestConnection{IgnoreNoResponders: true} - - responses := make(chan *QueryResponse, 128) - // this emulates a source - sourceHit := atomic.Bool{} - - _, err := tc.Subscribe(fmt.Sprintf("request.scope.%v", query.GetScope()), func(msg *nats.Msg) { - sourceHit.Store(true) - response := QueryResponse{ - ResponseType: &QueryResponse_NewItem{ - NewItem: &item, - }, - } - // Test that the handlers work - err := tc.Publish(ctx, query.Subject(), &response) - if err != nil { - t.Fatal(err) - } - }) - if err != nil { - t.Fatal(err) - } - - _, err = RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, responses) - if err != nil { - t.Fatal(err) - } - - response := <-responses - - tc.MessagesMu.Lock() - if len(tc.Messages) != 2 { - t.Errorf("expected 2 messages to be sent, got %v", len(tc.Messages)) - } - tc.MessagesMu.Unlock() - - returnedItem := response.GetNewItem() - if returnedItem == nil { - t.Fatal("expected item to be returned") - } - if returnedItem.Hash() != item.Hash() { - t.Error("item hash mismatch") - } - if !sourceHit.Load() { - t.Error("source was not hit") - } -} - -func TestExecute(t *testing.T) { - t.Parallel() - - t.Run("with no responders", func(t *testing.T) { - conn := TestConnection{} - _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) {}) - if err != nil { - t.Fatal(err) - } - u := uuid.New() - q := Query{ - Type: "user", - Method: QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "global", - IgnoreCache: false, - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), - } - - _, _, _, err = RunSourceQuerySync(t.Context(), &q, 100*time.Millisecond, &conn) - - if err != nil { - t.Fatal(err) - } - }) - - t.Run("with a full response set", func(t *testing.T) { - conn := TestConnection{} - _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) {}) - if err != nil { - t.Fatal(err) - } - u := uuid.New() - q := Query{ - Type: "user", - Method: QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 0, - }, - Scope: "global", - IgnoreCache: false, - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), - } - - querySent := make(chan struct{}) - done := make(chan struct{}) - - go func() { - defer close(done) - // wait for the query to be sent - <-querySent - - ru1 := uuid.New() - - err := conn.Publish(context.Background(), q.Subject(), &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - UUID: q.GetUUID(), - NextUpdateIn: &durationpb.Duration{ - Seconds: 10, - Nanos: 0, - }, - }, - }, - }) - if err != nil { - t.Error(err) - } - - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ - ResponseType: &QueryResponse_NewItem{ - NewItem: &item, - }, - }) - if err != nil { - t.Error(err) - } - - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ - ResponseType: &QueryResponse_NewItem{ - NewItem: &item, - }, - }) - if err != nil { - t.Error(err) - } - - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ - ResponseType: &QueryResponse_Response{ - Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_COMPLETE, - UUID: q.GetUUID(), - }, - }, - }) - if err != nil { - t.Error(err) - } - }() - - responseChan := make(chan *QueryResponse) - // items, _, errs, err := RunSourceQuerySync(t.Context(), &q, DefaultStartTimeout, &conn) - _, err = RunSourceQuery(t.Context(), &q, DefaultStartTimeout, &conn, responseChan) - if err != nil { - t.Fatal(err) - } - - close(querySent) - - items := []*Item{} - errs := []*QueryError{} - - for r := range responseChan { - if r == nil { - t.Fatal("expected a response") - } - switch r.GetResponseType().(type) { - case *QueryResponse_NewItem: - items = append(items, r.GetNewItem()) - case *QueryResponse_Error: - errs = append(errs, r.GetError()) - default: - t.Errorf("unexpected response type: %T", r.GetResponseType()) - } - } - - <-done - - if len(items) != 2 { - t.Errorf("expected 2 items got %v: %v", len(items), items) - } - - if len(errs) != 0 { - t.Errorf("expected 0 errors got %v: %v", len(errs), errs) - } - }) -} - -func TestRealNats(t *testing.T) { - nc, err := nats.Connect("nats://localhost,nats://nats") - if err != nil { - t.Skip("No NATS connection") - } - - enc := EncodedConnectionImpl{Conn: nc} - - u := uuid.New() - q := Query{ - Type: "person", - Method: QueryMethod_GET, - Query: "dylan", - Scope: "global", - UUID: u[:], - } - - ru1 := uuid.New() - - ready := make(chan bool) - - go func() { - _, err := enc.Subscribe("request.scope.global", NewQueryHandler("test", func(ctx context.Context, handledQuery *Query) { - delay := 100 * time.Millisecond - - time.Sleep(delay) - - err := enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_WORKING, - UUID: q.GetUUID(), - NextUpdateIn: &durationpb.Duration{ - Seconds: 10, - Nanos: 0, - }, - }}}) - if err != nil { - t.Error(err) - } - - time.Sleep(delay) - - err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: &item}}) - if err != nil { - t.Error(err) - } - - err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: &item}}) - if err != nil { - t.Error(err) - } - - err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: ru1[:], - State: ResponderState_COMPLETE, - UUID: q.GetUUID(), - }}}) - if err != nil { - t.Error(err) - } - })) - if err != nil { - t.Error(err) - } - ready <- true - }() - - <-ready - - slowChan := make(chan *QueryResponse) - - _, err = RunSourceQuery(t.Context(), &q, DefaultStartTimeout, &enc, slowChan) - if err != nil { - t.Fatal(err) - } - - for i := range slowChan { - time.Sleep(100 * time.Millisecond) - - t.Log(i) - } -} - -func TestFastFinisher(t *testing.T) { - t.Parallel() - - // Test for a situation where there is one responder that finishes really - // quickly and results in the other responders not getting a chance to start - conn := TestConnection{} - - fast := uuid.New() - slow := uuid.New() - - // Set up the fast responder, it should respond immediately and take only - // 100ms to complete its work - _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) { - // Make sure this is the request - var q Query - - err := proto.Unmarshal(msg.Data, &q) - - if err != nil { - t.Error(err) - } - - // Respond immediately saying we're started - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: fast[:], - State: ResponderState_WORKING, - UUID: q.GetUUID(), - NextUpdateIn: &durationpb.Duration{ - Seconds: 1, - Nanos: 0, - }, - }}}) - if err != nil { - t.Fatal(err) - } - - time.Sleep(100 * time.Millisecond) - - // Send an item - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: newItem()}}) - if err != nil { - t.Fatal(err) - } - - // Send a complete message - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: fast[:], - State: ResponderState_COMPLETE, - UUID: q.GetUUID(), - }}}) - if err != nil { - t.Fatal(err) - } - }) - if err != nil { - t.Fatal(err) - } - - // Set up another responder that takes 250ms to start - _, err = conn.Subscribe("request.scope.global", func(msg *nats.Msg) { - // Unmarshal the query - var q Query - - err := proto.Unmarshal(msg.Data, &q) - - if err != nil { - t.Error(err) - } - - // Wait 250ms before starting - time.Sleep(250 * time.Millisecond) - - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: slow[:], - State: ResponderState_WORKING, - UUID: q.GetUUID(), - NextUpdateIn: &durationpb.Duration{ - Seconds: 1, - Nanos: 0, - }, - }}}) - if err != nil { - t.Fatal(err) - } - - // Send an item - item := newItem() - err = item.GetAttributes().Set("name", "baz") - if err != nil { - t.Fatal(err) - } - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: item}}) - if err != nil { - t.Fatal(err) - } - - // Send a complete message - err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: "test", - ResponderUUID: slow[:], - State: ResponderState_COMPLETE, - UUID: q.GetUUID(), - }}}) - if err != nil { - t.Fatal(err) - } - }) - if err != nil { - t.Fatal(err) - } - - items, _, errs, err := RunSourceQuerySync(t.Context(), newQuery(), 500*time.Millisecond, &conn) - - if err != nil { - t.Fatal(err) - } - - if len(items) != 2 { - t.Errorf("Expected 2 items, got %d: %v", len(items), items) - } - - if len(errs) != 0 { - t.Errorf("Expected 0 errors, got %d: %v", len(errs), errs) - } -} - -// This source will simply respond to any query that it sent with a configured -// number of items, and configurable delays. This is designed to replicate a -// real system at scale -type SimpleSource struct { - // How many items to return from the query - NumItemsReturn int - - // How long to wait before starting work on the query - StartDelay time.Duration - - // How long to wait before sending each item - PerItemDelay time.Duration - - // How long to wait before sending the completion message - CompletionDelay time.Duration - - // The connection to use - Conn *TestConnection - - // The probability of stalling where 0 is no stall and 1 is always stall - StallProbability float64 - - // The probability of failing where 0 is no fail and 1 is always fail - FailProbability float64 - - // The responder name to use - ResponderName string -} - -func (s *SimpleSource) Start(ctx context.Context, t *testing.T) { - // ignore errors from test connection - _, _ = s.Conn.Subscribe("request.>", func(msg *nats.Msg) { - // Run these in parallel - go func(msg *nats.Msg) { - query := &Query{} - - err := Unmarshal(ctx, msg.Data, query) - if err != nil { - panic(fmt.Errorf("Unmarshal(%v): %w", query, err)) - } - - // Create the number of items that were requested - items := make([]*Item, s.NumItemsReturn) - for i := range s.NumItemsReturn { - items[i] = newItem() - } - - // Make a UUID for yourself - responderUUID := uuid.New() - - // Wait for the start delay - time.Sleep(s.StartDelay) - - // Calculate the expected duration of the query - expectedQueryDuration := (s.PerItemDelay * time.Duration(s.NumItemsReturn)) + s.CompletionDelay + 500*time.Millisecond - - err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: s.ResponderName, - ResponderUUID: responderUUID[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(expectedQueryDuration), - UUID: query.GetUUID(), - }}}) - if err != nil { - t.Errorf("error publishing response: %v", err) - } - - for _, item := range items { - time.Sleep(s.PerItemDelay) - err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: item}}) - if err != nil { - t.Errorf("error publishing item: %v", err) - } - } - - // Stall with a certain probability - if rand.Float64() < s.StallProbability { - return - } - - // Fail with a certain probability - if rand.Float64() < s.FailProbability { - err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: s.ResponderName, - ResponderUUID: responderUUID[:], - State: ResponderState_ERROR, - UUID: query.GetUUID(), - }}}) - if err != nil { - t.Errorf("error publishing response: %v", err) - } - return - } - - time.Sleep(s.CompletionDelay) - err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ - Responder: s.ResponderName, - ResponderUUID: responderUUID[:], - State: ResponderState_COMPLETE, - UUID: query.GetUUID(), - }}}) - if err != nil { - t.Errorf("error publishing response: %v", err) - } - }(msg) - }) -} - -func TestMassiveScale(t *testing.T) { - t.Parallel() - - if _, exists := os.LookupEnv("GITHUB_ACTIONS"); exists { - // Note that in these tests we can push things even further, to 10,000 - // sources for example. The problem is that once the CPU is context - // switching too heavily you end up in a position where the sources - // start getting marked as stalled as they don't have enough CPU to send - // their messages quickly enough and they blow through their expected - // timeout. - // - // They can also fail locally when using -race as this puts a lot more - // load on the CPU than there would normally be - t.Skip("These tests are too flaky due to reliance on wall clock time and fast timings") - } - - tests := []struct { - // The number of sources to create - NumSources int - // The maximum time to wait before starting - MaxStartDelayMilliseconds int - // The maximum time to wait between items - MaxPerItemDelayMilliseconds int - // The maximum time to wait before completion - MaxCompletionDelayMilliseconds int - // The maximum number of items to return - MaxItemsToReturn int - // The probability of a source stalling where 0 is no stall and 1 is - // always stall - StallProbability float64 - // The probability of a source failing where 0 is no fail and 1 is - // always fail - FailProbability float64 - // How long to give sources to start responding, over and above the - // maxStartDelayMilliseconds - StartDelayGracePeriodMilliseconds int - }{ - { - NumSources: 100, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.0, - FailProbability: 0.0, - StartDelayGracePeriodMilliseconds: 100, - }, - { - NumSources: 1_000, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.0, - FailProbability: 0.0, - StartDelayGracePeriodMilliseconds: 100, - }, - { - NumSources: 100, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.3, - FailProbability: 0.0, - StartDelayGracePeriodMilliseconds: 100, - }, - { - NumSources: 1_000, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.3, - FailProbability: 0.0, - StartDelayGracePeriodMilliseconds: 100, - }, - { - NumSources: 100, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.3, - FailProbability: 0.3, - StartDelayGracePeriodMilliseconds: 100, - }, - { - NumSources: 1_000, - MaxStartDelayMilliseconds: 100, - MaxPerItemDelayMilliseconds: 10, - MaxCompletionDelayMilliseconds: 100, - MaxItemsToReturn: 100, - StallProbability: 0.3, - FailProbability: 0.3, - StartDelayGracePeriodMilliseconds: 100, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("NumSources %v, MaxStartDelay %v, MaxPerItemDelay %v, MaxCompletionDelay %v, MaxItemsToReturn %v, StallProbability %v, FailProbability %v, StartDelayGracePeriod %v", - test.NumSources, - test.MaxStartDelayMilliseconds, - test.MaxPerItemDelayMilliseconds, - test.MaxCompletionDelayMilliseconds, - test.MaxItemsToReturn, - test.StallProbability, - test.FailProbability, - test.StartDelayGracePeriodMilliseconds, - ), func(t *testing.T) { - tConn := TestConnection{} - - // Generate a random duration between 0 and maxDuration - randomDuration := func(maxDuration int) time.Duration { - return time.Duration(rand.Intn(maxDuration)) * time.Millisecond - } - - expectedItems := 0 - - // Start all the sources - sources := make([]*SimpleSource, test.NumSources) - for i := range sources { - numItemsReturn := rand.Intn(test.MaxItemsToReturn) - expectedItems += numItemsReturn // Count how many items we expect to receive - startDelay := randomDuration(test.MaxStartDelayMilliseconds) - perItemDelay := randomDuration(test.MaxPerItemDelayMilliseconds) - completionDelay := randomDuration(test.MaxCompletionDelayMilliseconds) - - sources[i] = &SimpleSource{ - NumItemsReturn: numItemsReturn, - StartDelay: startDelay, - PerItemDelay: perItemDelay, - CompletionDelay: completionDelay, - StallProbability: test.StallProbability, - FailProbability: test.FailProbability, - Conn: &tConn, - ResponderName: fmt.Sprintf("NumItems %v, StartDelay %v, PerItemDelay %v CompletionDelay %v", - numItemsReturn, - startDelay.String(), - perItemDelay.String(), - completionDelay.String(), - ), - } - - sources[i].Start(context.Background(), t) - } - - // Create the query - u := uuid.New() - q := Query{ - Type: "massive-scale-test", - Method: QueryMethod_GET, - Query: "GO!!!!!", - Scope: "test", - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(60 * time.Second)), - } - - responseChan := make(chan *QueryResponse) - doneChan := make(chan struct{}) - - // Begin handling the responses - actualItems := 0 - go func() { - for { - select { - case <-t.Context().Done(): - return - case response, ok := <-responseChan: - if !ok { - // Channel closed - close(doneChan) - return - } - - switch response.GetResponseType().(type) { - case *QueryResponse_NewItem: - actualItems++ - } - } - } - }() - - // Start the query - startTimeout := time.Duration(test.MaxStartDelayMilliseconds+test.StartDelayGracePeriodMilliseconds) * time.Millisecond - qp, err := RunSourceQuery(t.Context(), &q, startTimeout, &tConn, responseChan) - if err != nil { - t.Fatal(err) - } - - // Wait for the query to finish - <-doneChan - - if actualItems != expectedItems { - t.Errorf("Expected %v items, got %v", expectedItems, actualItems) - } - - progress := qp.Progress() - - if progress.Responders != test.NumSources { - t.Errorf("Expected %v responders, got %v", test.NumSources, progress.Responders) - } - - fmt.Printf("Num Complete: %v\n", progress.Complete) - fmt.Printf("Num Working: %v\n", progress.Working) - fmt.Printf("Num Stalled: %v\n", progress.Stalled) - fmt.Printf("Num Error: %v\n", progress.Error) - fmt.Printf("Num Cancelled: %v\n", progress.Cancelled) - fmt.Printf("Num Responders: %v\n", progress.Responders) - fmt.Printf("Num Items: %v\n", actualItems) - }) - } -} diff --git a/sdp-go/proto_clone_test.go b/sdp-go/proto_clone_test.go deleted file mode 100644 index 0a73c4eb..00000000 --- a/sdp-go/proto_clone_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package sdp - -import ( - "testing" - - "github.com/google/uuid" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" -) - -// TestProtoCloneReplacesCustomCopy validates that proto.Clone works correctly -// for all SDP types and can replace the custom Copy methods -func TestProtoCloneReplacesCustomCopy(t *testing.T) { - t.Run("Reference with all fields", func(t *testing.T) { - original := &Reference{ - Type: "test", - UniqueAttributeValue: "value", - Scope: "scope", - IsQuery: true, - Method: QueryMethod_SEARCH, - Query: "search-term", - } - - cloned := proto.Clone(original).(*Reference) - - if !proto.Equal(original, cloned) { - t.Errorf("proto.Clone failed for Reference: %+v != %+v", original, cloned) - } - - // Specifically check the fields that Copy() was missing - if cloned.GetIsQuery() != original.GetIsQuery() { - t.Errorf("IsQuery field not cloned correctly: got %v, want %v", cloned.GetIsQuery(), original.GetIsQuery()) - } - if cloned.GetMethod() != original.GetMethod() { - t.Errorf("Method field not cloned correctly: got %v, want %v", cloned.GetMethod(), original.GetMethod()) - } - if cloned.GetQuery() != original.GetQuery() { - t.Errorf("Query field not cloned correctly: got %v, want %v", cloned.GetQuery(), original.GetQuery()) - } - }) - - t.Run("Query with all fields", func(t *testing.T) { - u := uuid.New() - original := &Query{ - Type: "test", - Method: QueryMethod_GET, - Query: "value", - Scope: "scope", - UUID: u[:], - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 5, - FollowOnlyBlastPropagation: true, - }, - IgnoreCache: true, - Deadline: timestamppb.Now(), - } - - cloned := proto.Clone(original).(*Query) - - if !proto.Equal(original, cloned) { - t.Errorf("proto.Clone failed for Query: %+v != %+v", original, cloned) - } - }) - - t.Run("Item with all fields", func(t *testing.T) { - original := &Item{ - Type: "test", - UniqueAttribute: "id", - Scope: "scope", - Metadata: &Metadata{ - SourceName: "test-source", - Hidden: true, - Timestamp: timestamppb.Now(), - }, - Health: Health_HEALTH_OK.Enum(), - Tags: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - } - - // Add attributes - attrs, err := ToAttributes(map[string]interface{}{ - "name": "test-item", - "port": 8080, - }) - if err != nil { - t.Fatal(err) - } - original.Attributes = attrs - - cloned := proto.Clone(original).(*Item) - - if !proto.Equal(original, cloned) { - t.Errorf("proto.Clone failed for Item: %+v != %+v", original, cloned) - } - }) - - t.Run("All other SDP types", func(t *testing.T) { - // BlastPropagation - bp := &BlastPropagation{In: true, Out: false} - bpClone := proto.Clone(bp).(*BlastPropagation) - if !proto.Equal(bp, bpClone) { - t.Errorf("proto.Clone failed for BlastPropagation") - } - - // LinkedItemQuery - liq := &LinkedItemQuery{ - Query: &Query{Type: "test", Method: QueryMethod_LIST}, - BlastPropagation: bp, - } - liqClone := proto.Clone(liq).(*LinkedItemQuery) - if !proto.Equal(liq, liqClone) { - t.Errorf("proto.Clone failed for LinkedItemQuery") - } - - // LinkedItem - li := &LinkedItem{ - Item: &Reference{Type: "test", Scope: "scope"}, - BlastPropagation: bp, - } - liClone := proto.Clone(li).(*LinkedItem) - if !proto.Equal(li, liClone) { - t.Errorf("proto.Clone failed for LinkedItem") - } - - // Metadata - metadata := &Metadata{ - SourceName: "test-source", - Hidden: true, - Timestamp: timestamppb.Now(), - } - metadataClone := proto.Clone(metadata).(*Metadata) - if !proto.Equal(metadata, metadataClone) { - t.Errorf("proto.Clone failed for Metadata") - } - - // CancelQuery - u := uuid.New() - cancelQuery := &CancelQuery{UUID: u[:]} - cancelQueryClone := proto.Clone(cancelQuery).(*CancelQuery) - if !proto.Equal(cancelQuery, cancelQueryClone) { - t.Errorf("proto.Clone failed for CancelQuery") - } - }) -} diff --git a/sdp-go/responses.go b/sdp-go/responses.go deleted file mode 100644 index 1d23b94e..00000000 --- a/sdp-go/responses.go +++ /dev/null @@ -1,27 +0,0 @@ -package sdp - -// TODO: instead of translating, unify this -func (r *Response) ToQueryStatus() *QueryStatus { - return &QueryStatus{ - UUID: r.GetUUID(), - Status: r.GetState().ToQueryStatus(), - } -} - -// TODO: instead of translating, unify this -func (r ResponderState) ToQueryStatus() QueryStatus_Status { - switch r { - case ResponderState_WORKING: - return QueryStatus_STARTED - case ResponderState_COMPLETE: - return QueryStatus_FINISHED - case ResponderState_ERROR: - return QueryStatus_ERRORED - case ResponderState_CANCELLED: - return QueryStatus_CANCELLED - case ResponderState_STALLED: - return QueryStatus_ERRORED - default: - return QueryStatus_UNSPECIFIED - } -} diff --git a/sdp-go/responses.pb.go b/sdp-go/responses.pb.go deleted file mode 100644 index 677b024f..00000000 --- a/sdp-go/responses.pb.go +++ /dev/null @@ -1,246 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: responses.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// ResponderState represents the state of the responder, note that both -// COMPLETE and ERROR are completion states i.e. do not expect any more items -// to be returned from the query -type ResponderState int32 - -const ( - // The responder is still gathering data - ResponderState_WORKING ResponderState = 0 - // The query is complete - ResponderState_COMPLETE ResponderState = 1 - // All sources have returned errors - ResponderState_ERROR ResponderState = 2 - // Work has been cancelled while in progress - ResponderState_CANCELLED ResponderState = 3 - // The responder has not set a response in the expected interval - ResponderState_STALLED ResponderState = 4 -) - -// Enum value maps for ResponderState. -var ( - ResponderState_name = map[int32]string{ - 0: "WORKING", - 1: "COMPLETE", - 2: "ERROR", - 3: "CANCELLED", - 4: "STALLED", - } - ResponderState_value = map[string]int32{ - "WORKING": 0, - "COMPLETE": 1, - "ERROR": 2, - "CANCELLED": 3, - "STALLED": 4, - } -) - -func (x ResponderState) Enum() *ResponderState { - p := new(ResponderState) - *p = x - return p -} - -func (x ResponderState) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ResponderState) Descriptor() protoreflect.EnumDescriptor { - return file_responses_proto_enumTypes[0].Descriptor() -} - -func (ResponderState) Type() protoreflect.EnumType { - return &file_responses_proto_enumTypes[0] -} - -func (x ResponderState) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ResponderState.Descriptor instead. -func (ResponderState) EnumDescriptor() ([]byte, []int) { - return file_responses_proto_rawDescGZIP(), []int{0} -} - -// Response is returned when a query is made -type Response struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The name of the responder that is working on a response. This is purely - // informational - Responder string `protobuf:"bytes,1,opt,name=responder,proto3" json:"responder,omitempty"` - // The state of the responder - State ResponderState `protobuf:"varint,2,opt,name=state,proto3,enum=ResponderState" json:"state,omitempty"` - // The timespan within which to expect the next update. (e.g. 10s) If no - // further interim responses are received within this time the connection - // can be considered stale and the requester may give up - NextUpdateIn *durationpb.Duration `protobuf:"bytes,3,opt,name=nextUpdateIn,proto3" json:"nextUpdateIn,omitempty"` - // UUID of the item query that this response is in relation to (in binary - // format) - UUID []byte `protobuf:"bytes,4,opt,name=UUID,proto3" json:"UUID,omitempty"` - // The ID of the responder that is working on a response. This is used for - // internal bookkeeping and should remain constant for the duration of a - // request, preferably over the lifetime of the source process. - ResponderUUID []byte `protobuf:"bytes,5,opt,name=responderUUID,proto3" json:"responderUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Response) Reset() { - *x = Response{} - mi := &file_responses_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Response) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Response) ProtoMessage() {} - -func (x *Response) ProtoReflect() protoreflect.Message { - mi := &file_responses_proto_msgTypes[0] - 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 Response.ProtoReflect.Descriptor instead. -func (*Response) Descriptor() ([]byte, []int) { - return file_responses_proto_rawDescGZIP(), []int{0} -} - -func (x *Response) GetResponder() string { - if x != nil { - return x.Responder - } - return "" -} - -func (x *Response) GetState() ResponderState { - if x != nil { - return x.State - } - return ResponderState_WORKING -} - -func (x *Response) GetNextUpdateIn() *durationpb.Duration { - if x != nil { - return x.NextUpdateIn - } - return nil -} - -func (x *Response) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *Response) GetResponderUUID() []byte { - if x != nil { - return x.ResponderUUID - } - return nil -} - -var File_responses_proto protoreflect.FileDescriptor - -const file_responses_proto_rawDesc = "" + - "\n" + - "\x0fresponses.proto\x1a\x1egoogle/protobuf/duration.proto\"\xc8\x01\n" + - "\bResponse\x12\x1c\n" + - "\tresponder\x18\x01 \x01(\tR\tresponder\x12%\n" + - "\x05state\x18\x02 \x01(\x0e2\x0f.ResponderStateR\x05state\x12=\n" + - "\fnextUpdateIn\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\fnextUpdateIn\x12\x12\n" + - "\x04UUID\x18\x04 \x01(\fR\x04UUID\x12$\n" + - "\rresponderUUID\x18\x05 \x01(\fR\rresponderUUID*R\n" + - "\x0eResponderState\x12\v\n" + - "\aWORKING\x10\x00\x12\f\n" + - "\bCOMPLETE\x10\x01\x12\t\n" + - "\x05ERROR\x10\x02\x12\r\n" + - "\tCANCELLED\x10\x03\x12\v\n" + - "\aSTALLED\x10\x04B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_responses_proto_rawDescOnce sync.Once - file_responses_proto_rawDescData []byte -) - -func file_responses_proto_rawDescGZIP() []byte { - file_responses_proto_rawDescOnce.Do(func() { - file_responses_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_responses_proto_rawDesc), len(file_responses_proto_rawDesc))) - }) - return file_responses_proto_rawDescData -} - -var file_responses_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_responses_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_responses_proto_goTypes = []any{ - (ResponderState)(0), // 0: ResponderState - (*Response)(nil), // 1: Response - (*durationpb.Duration)(nil), // 2: google.protobuf.Duration -} -var file_responses_proto_depIdxs = []int32{ - 0, // 0: Response.state:type_name -> ResponderState - 2, // 1: Response.nextUpdateIn:type_name -> google.protobuf.Duration - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name -} - -func init() { file_responses_proto_init() } -func file_responses_proto_init() { - if File_responses_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_responses_proto_rawDesc), len(file_responses_proto_rawDesc)), - NumEnums: 1, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_responses_proto_goTypes, - DependencyIndexes: file_responses_proto_depIdxs, - EnumInfos: file_responses_proto_enumTypes, - MessageInfos: file_responses_proto_msgTypes, - }.Build() - File_responses_proto = out.File - file_responses_proto_goTypes = nil - file_responses_proto_depIdxs = nil -} diff --git a/sdp-go/revlink.pb.go b/sdp-go/revlink.pb.go deleted file mode 100644 index d152bdb2..00000000 --- a/sdp-go/revlink.pb.go +++ /dev/null @@ -1,447 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: revlink.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - _ "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetReverseEdgesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The account that the item belongs to - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - // The item that you would like to find reverse edges for - Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - // set to true to only return edges that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,3,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetReverseEdgesRequest) Reset() { - *x = GetReverseEdgesRequest{} - mi := &file_revlink_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetReverseEdgesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetReverseEdgesRequest) ProtoMessage() {} - -func (x *GetReverseEdgesRequest) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[0] - 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 GetReverseEdgesRequest.ProtoReflect.Descriptor instead. -func (*GetReverseEdgesRequest) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{0} -} - -func (x *GetReverseEdgesRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *GetReverseEdgesRequest) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *GetReverseEdgesRequest) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - -type GetReverseEdgesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The edges to the requested item - Edges []*Edge `protobuf:"bytes,1,rep,name=edges,proto3" json:"edges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetReverseEdgesResponse) Reset() { - *x = GetReverseEdgesResponse{} - mi := &file_revlink_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetReverseEdgesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetReverseEdgesResponse) ProtoMessage() {} - -func (x *GetReverseEdgesResponse) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[1] - 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 GetReverseEdgesResponse.ProtoReflect.Descriptor instead. -func (*GetReverseEdgesResponse) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{1} -} - -func (x *GetReverseEdgesResponse) GetEdges() []*Edge { - if x != nil { - return x.Edges - } - return nil -} - -type IngestGatewayResponseRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The account that the response belongs to - Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - // The response type to ingest - // - // Types that are valid to be assigned to ResponseType: - // - // *IngestGatewayResponseRequest_NewItem - // *IngestGatewayResponseRequest_NewEdge - ResponseType isIngestGatewayResponseRequest_ResponseType `protobuf_oneof:"response_type"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *IngestGatewayResponseRequest) Reset() { - *x = IngestGatewayResponseRequest{} - mi := &file_revlink_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *IngestGatewayResponseRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*IngestGatewayResponseRequest) ProtoMessage() {} - -func (x *IngestGatewayResponseRequest) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[2] - 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 IngestGatewayResponseRequest.ProtoReflect.Descriptor instead. -func (*IngestGatewayResponseRequest) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{2} -} - -func (x *IngestGatewayResponseRequest) GetAccount() string { - if x != nil { - return x.Account - } - return "" -} - -func (x *IngestGatewayResponseRequest) GetResponseType() isIngestGatewayResponseRequest_ResponseType { - if x != nil { - return x.ResponseType - } - return nil -} - -func (x *IngestGatewayResponseRequest) GetNewItem() *Item { - if x != nil { - if x, ok := x.ResponseType.(*IngestGatewayResponseRequest_NewItem); ok { - return x.NewItem - } - } - return nil -} - -func (x *IngestGatewayResponseRequest) GetNewEdge() *Edge { - if x != nil { - if x, ok := x.ResponseType.(*IngestGatewayResponseRequest_NewEdge); ok { - return x.NewEdge - } - } - return nil -} - -type isIngestGatewayResponseRequest_ResponseType interface { - isIngestGatewayResponseRequest_ResponseType() -} - -type IngestGatewayResponseRequest_NewItem struct { - NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered -} - -type IngestGatewayResponseRequest_NewEdge struct { - NewEdge *Edge `protobuf:"bytes,3,opt,name=newEdge,proto3,oneof"` // A new edge between two items -} - -func (*IngestGatewayResponseRequest_NewItem) isIngestGatewayResponseRequest_ResponseType() {} - -func (*IngestGatewayResponseRequest_NewEdge) isIngestGatewayResponseRequest_ResponseType() {} - -type IngestGatewayResponsesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - NumItemsReceived int32 `protobuf:"varint,1,opt,name=numItemsReceived,proto3" json:"numItemsReceived,omitempty"` - NumEdgesReceived int32 `protobuf:"varint,2,opt,name=numEdgesReceived,proto3" json:"numEdgesReceived,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *IngestGatewayResponsesResponse) Reset() { - *x = IngestGatewayResponsesResponse{} - mi := &file_revlink_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *IngestGatewayResponsesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*IngestGatewayResponsesResponse) ProtoMessage() {} - -func (x *IngestGatewayResponsesResponse) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[3] - 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 IngestGatewayResponsesResponse.ProtoReflect.Descriptor instead. -func (*IngestGatewayResponsesResponse) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{3} -} - -func (x *IngestGatewayResponsesResponse) GetNumItemsReceived() int32 { - if x != nil { - return x.NumItemsReceived - } - return 0 -} - -func (x *IngestGatewayResponsesResponse) GetNumEdgesReceived() int32 { - if x != nil { - return x.NumEdgesReceived - } - return 0 -} - -type CheckpointRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CheckpointRequest) Reset() { - *x = CheckpointRequest{} - mi := &file_revlink_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CheckpointRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckpointRequest) ProtoMessage() {} - -func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[4] - 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 CheckpointRequest.ProtoReflect.Descriptor instead. -func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{4} -} - -type CheckpointResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CheckpointResponse) Reset() { - *x = CheckpointResponse{} - mi := &file_revlink_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CheckpointResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckpointResponse) ProtoMessage() {} - -func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_revlink_proto_msgTypes[5] - 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 CheckpointResponse.ProtoReflect.Descriptor instead. -func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_revlink_proto_rawDescGZIP(), []int{5} -} - -var File_revlink_proto protoreflect.FileDescriptor - -const file_revlink_proto_rawDesc = "" + - "\n" + - "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x92\x01\n" + - "\x16GetReverseEdgesRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x12\x1e\n" + - "\x04item\x18\x02 \x01(\v2\n" + - ".ReferenceR\x04item\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x03 \x01(\bR\x1afollowOnlyBlastPropagation\"6\n" + - "\x17GetReverseEdgesResponse\x12\x1b\n" + - "\x05edges\x18\x01 \x03(\v2\x05.EdgeR\x05edges\"\x8f\x01\n" + - "\x1cIngestGatewayResponseRequest\x12\x18\n" + - "\aaccount\x18\x01 \x01(\tR\aaccount\x12!\n" + - "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12!\n" + - "\anewEdge\x18\x03 \x01(\v2\x05.EdgeH\x00R\anewEdgeB\x0f\n" + - "\rresponse_type\"x\n" + - "\x1eIngestGatewayResponsesResponse\x12*\n" + - "\x10numItemsReceived\x18\x01 \x01(\x05R\x10numItemsReceived\x12*\n" + - "\x10numEdgesReceived\x18\x02 \x01(\x05R\x10numEdgesReceived\"\x13\n" + - "\x11CheckpointRequest\"\x14\n" + - "\x12CheckpointResponse2\x99\x02\n" + - "\x0eRevlinkService\x12T\n" + - "\x0fGetReverseEdges\x12\x1f.revlink.GetReverseEdgesRequest\x1a .revlink.GetReverseEdgesResponse\x12j\n" + - "\x16IngestGatewayResponses\x12%.revlink.IngestGatewayResponseRequest\x1a'.revlink.IngestGatewayResponsesResponse(\x01\x12E\n" + - "\n" + - "Checkpoint\x12\x1a.revlink.CheckpointRequest\x1a\x1b.revlink.CheckpointResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_revlink_proto_rawDescOnce sync.Once - file_revlink_proto_rawDescData []byte -) - -func file_revlink_proto_rawDescGZIP() []byte { - file_revlink_proto_rawDescOnce.Do(func() { - file_revlink_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_revlink_proto_rawDesc), len(file_revlink_proto_rawDesc))) - }) - return file_revlink_proto_rawDescData -} - -var file_revlink_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_revlink_proto_goTypes = []any{ - (*GetReverseEdgesRequest)(nil), // 0: revlink.GetReverseEdgesRequest - (*GetReverseEdgesResponse)(nil), // 1: revlink.GetReverseEdgesResponse - (*IngestGatewayResponseRequest)(nil), // 2: revlink.IngestGatewayResponseRequest - (*IngestGatewayResponsesResponse)(nil), // 3: revlink.IngestGatewayResponsesResponse - (*CheckpointRequest)(nil), // 4: revlink.CheckpointRequest - (*CheckpointResponse)(nil), // 5: revlink.CheckpointResponse - (*Reference)(nil), // 6: Reference - (*Edge)(nil), // 7: Edge - (*Item)(nil), // 8: Item -} -var file_revlink_proto_depIdxs = []int32{ - 6, // 0: revlink.GetReverseEdgesRequest.item:type_name -> Reference - 7, // 1: revlink.GetReverseEdgesResponse.edges:type_name -> Edge - 8, // 2: revlink.IngestGatewayResponseRequest.newItem:type_name -> Item - 7, // 3: revlink.IngestGatewayResponseRequest.newEdge:type_name -> Edge - 0, // 4: revlink.RevlinkService.GetReverseEdges:input_type -> revlink.GetReverseEdgesRequest - 2, // 5: revlink.RevlinkService.IngestGatewayResponses:input_type -> revlink.IngestGatewayResponseRequest - 4, // 6: revlink.RevlinkService.Checkpoint:input_type -> revlink.CheckpointRequest - 1, // 7: revlink.RevlinkService.GetReverseEdges:output_type -> revlink.GetReverseEdgesResponse - 3, // 8: revlink.RevlinkService.IngestGatewayResponses:output_type -> revlink.IngestGatewayResponsesResponse - 5, // 9: revlink.RevlinkService.Checkpoint:output_type -> revlink.CheckpointResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name -} - -func init() { file_revlink_proto_init() } -func file_revlink_proto_init() { - if File_revlink_proto != nil { - return - } - file_items_proto_init() - file_revlink_proto_msgTypes[2].OneofWrappers = []any{ - (*IngestGatewayResponseRequest_NewItem)(nil), - (*IngestGatewayResponseRequest_NewEdge)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_revlink_proto_rawDesc), len(file_revlink_proto_rawDesc)), - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_revlink_proto_goTypes, - DependencyIndexes: file_revlink_proto_depIdxs, - MessageInfos: file_revlink_proto_msgTypes, - }.Build() - File_revlink_proto = out.File - file_revlink_proto_goTypes = nil - file_revlink_proto_depIdxs = nil -} diff --git a/sdp-go/sdpconnect/account.connect.go b/sdp-go/sdpconnect/account.connect.go deleted file mode 100644 index 80989d25..00000000 --- a/sdp-go/sdpconnect/account.connect.go +++ /dev/null @@ -1,1187 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: account.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // AdminServiceName is the fully-qualified name of the AdminService service. - AdminServiceName = "account.AdminService" - // ManagementServiceName is the fully-qualified name of the ManagementService service. - ManagementServiceName = "account.ManagementService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // AdminServiceListAccountsProcedure is the fully-qualified name of the AdminService's ListAccounts - // RPC. - AdminServiceListAccountsProcedure = "/account.AdminService/ListAccounts" - // AdminServiceCreateAccountProcedure is the fully-qualified name of the AdminService's - // CreateAccount RPC. - AdminServiceCreateAccountProcedure = "/account.AdminService/CreateAccount" - // AdminServiceUpdateAccountProcedure is the fully-qualified name of the AdminService's - // UpdateAccount RPC. - AdminServiceUpdateAccountProcedure = "/account.AdminService/UpdateAccount" - // AdminServiceGetAccountProcedure is the fully-qualified name of the AdminService's GetAccount RPC. - AdminServiceGetAccountProcedure = "/account.AdminService/GetAccount" - // AdminServiceDeleteAccountProcedure is the fully-qualified name of the AdminService's - // DeleteAccount RPC. - AdminServiceDeleteAccountProcedure = "/account.AdminService/DeleteAccount" - // AdminServiceListSourcesProcedure is the fully-qualified name of the AdminService's ListSources - // RPC. - AdminServiceListSourcesProcedure = "/account.AdminService/ListSources" - // AdminServiceCreateSourceProcedure is the fully-qualified name of the AdminService's CreateSource - // RPC. - AdminServiceCreateSourceProcedure = "/account.AdminService/CreateSource" - // AdminServiceGetSourceProcedure is the fully-qualified name of the AdminService's GetSource RPC. - AdminServiceGetSourceProcedure = "/account.AdminService/GetSource" - // AdminServiceUpdateSourceProcedure is the fully-qualified name of the AdminService's UpdateSource - // RPC. - AdminServiceUpdateSourceProcedure = "/account.AdminService/UpdateSource" - // AdminServiceDeleteSourceProcedure is the fully-qualified name of the AdminService's DeleteSource - // RPC. - AdminServiceDeleteSourceProcedure = "/account.AdminService/DeleteSource" - // AdminServiceKeepaliveSourcesProcedure is the fully-qualified name of the AdminService's - // KeepaliveSources RPC. - AdminServiceKeepaliveSourcesProcedure = "/account.AdminService/KeepaliveSources" - // AdminServiceCreateTokenProcedure is the fully-qualified name of the AdminService's CreateToken - // RPC. - AdminServiceCreateTokenProcedure = "/account.AdminService/CreateToken" - // ManagementServiceGetAccountProcedure is the fully-qualified name of the ManagementService's - // GetAccount RPC. - ManagementServiceGetAccountProcedure = "/account.ManagementService/GetAccount" - // ManagementServiceDeleteAccountProcedure is the fully-qualified name of the ManagementService's - // DeleteAccount RPC. - ManagementServiceDeleteAccountProcedure = "/account.ManagementService/DeleteAccount" - // ManagementServiceListSourcesProcedure is the fully-qualified name of the ManagementService's - // ListSources RPC. - ManagementServiceListSourcesProcedure = "/account.ManagementService/ListSources" - // ManagementServiceCreateSourceProcedure is the fully-qualified name of the ManagementService's - // CreateSource RPC. - ManagementServiceCreateSourceProcedure = "/account.ManagementService/CreateSource" - // ManagementServiceGetSourceProcedure is the fully-qualified name of the ManagementService's - // GetSource RPC. - ManagementServiceGetSourceProcedure = "/account.ManagementService/GetSource" - // ManagementServiceUpdateSourceProcedure is the fully-qualified name of the ManagementService's - // UpdateSource RPC. - ManagementServiceUpdateSourceProcedure = "/account.ManagementService/UpdateSource" - // ManagementServiceDeleteSourceProcedure is the fully-qualified name of the ManagementService's - // DeleteSource RPC. - ManagementServiceDeleteSourceProcedure = "/account.ManagementService/DeleteSource" - // ManagementServiceListAllSourcesStatusProcedure is the fully-qualified name of the - // ManagementService's ListAllSourcesStatus RPC. - ManagementServiceListAllSourcesStatusProcedure = "/account.ManagementService/ListAllSourcesStatus" - // ManagementServiceListActiveSourcesStatusProcedure is the fully-qualified name of the - // ManagementService's ListActiveSourcesStatus RPC. - ManagementServiceListActiveSourcesStatusProcedure = "/account.ManagementService/ListActiveSourcesStatus" - // ManagementServiceSubmitSourceHeartbeatProcedure is the fully-qualified name of the - // ManagementService's SubmitSourceHeartbeat RPC. - ManagementServiceSubmitSourceHeartbeatProcedure = "/account.ManagementService/SubmitSourceHeartbeat" - // ManagementServiceKeepaliveSourcesProcedure is the fully-qualified name of the ManagementService's - // KeepaliveSources RPC. - ManagementServiceKeepaliveSourcesProcedure = "/account.ManagementService/KeepaliveSources" - // ManagementServiceCreateTokenProcedure is the fully-qualified name of the ManagementService's - // CreateToken RPC. - ManagementServiceCreateTokenProcedure = "/account.ManagementService/CreateToken" - // ManagementServiceRevlinkWarmupProcedure is the fully-qualified name of the ManagementService's - // RevlinkWarmup RPC. - ManagementServiceRevlinkWarmupProcedure = "/account.ManagementService/RevlinkWarmup" - // ManagementServiceListAvailableItemTypesProcedure is the fully-qualified name of the - // ManagementService's ListAvailableItemTypes RPC. - ManagementServiceListAvailableItemTypesProcedure = "/account.ManagementService/ListAvailableItemTypes" - // ManagementServiceGetSourceStatusProcedure is the fully-qualified name of the ManagementService's - // GetSourceStatus RPC. - ManagementServiceGetSourceStatusProcedure = "/account.ManagementService/GetSourceStatus" - // ManagementServiceGetUserOnboardingStatusProcedure is the fully-qualified name of the - // ManagementService's GetUserOnboardingStatus RPC. - ManagementServiceGetUserOnboardingStatusProcedure = "/account.ManagementService/GetUserOnboardingStatus" - // ManagementServiceSetUserOnboardingStatusProcedure is the fully-qualified name of the - // ManagementService's SetUserOnboardingStatus RPC. - ManagementServiceSetUserOnboardingStatusProcedure = "/account.ManagementService/SetUserOnboardingStatus" - // ManagementServiceListTeamMembersProcedure is the fully-qualified name of the ManagementService's - // ListTeamMembers RPC. - ManagementServiceListTeamMembersProcedure = "/account.ManagementService/ListTeamMembers" - // ManagementServiceGetWelcomeScreenInformationProcedure is the fully-qualified name of the - // ManagementService's GetWelcomeScreenInformation RPC. - ManagementServiceGetWelcomeScreenInformationProcedure = "/account.ManagementService/GetWelcomeScreenInformation" - // ManagementServiceSetGithubInstallationIDProcedure is the fully-qualified name of the - // ManagementService's SetGithubInstallationID RPC. - ManagementServiceSetGithubInstallationIDProcedure = "/account.ManagementService/SetGithubInstallationID" - // ManagementServiceUnsetGithubInstallationIDProcedure is the fully-qualified name of the - // ManagementService's UnsetGithubInstallationID RPC. - ManagementServiceUnsetGithubInstallationIDProcedure = "/account.ManagementService/UnsetGithubInstallationID" -) - -// AdminServiceClient is a client for the account.AdminService service. -type AdminServiceClient interface { - // Lists the details of all NATS Accounts - ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) - // Creates a new account, public_nkey will be autogenerated - CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) - // Updates account details, returns the account - UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) - // Get the details of a given account - GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) - // Completely deletes an account. This includes all of the data in that - // account, bookmarks, changes etc. It also deletes all users from Auth0 - // that are associated with this account - DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) - // Lists all sources within the chosen account - ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) - // Creates a new source within the chosen account - CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) - // Get the details of a source within the chosen account - GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) - // Update the details of a source within the chosen account - UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) - // Deletes a source from a chosen account - DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) - // Updates sources to keep them running in the background. This can be used - // to add explicit action, when the built-in keepalives are not sufficient. - KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) - // Create a new NATS token for a given public NKey. The user requesting must - // control the associated private key also in order to connect to NATS as - // the token is not enough on its own - CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) -} - -// NewAdminServiceClient constructs a client for the account.AdminService service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewAdminServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AdminServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - adminServiceMethods := sdp_go.File_account_proto.Services().ByName("AdminService").Methods() - return &adminServiceClient{ - listAccounts: connect.NewClient[sdp_go.ListAccountsRequest, sdp_go.ListAccountsResponse]( - httpClient, - baseURL+AdminServiceListAccountsProcedure, - connect.WithSchema(adminServiceMethods.ByName("ListAccounts")), - connect.WithClientOptions(opts...), - ), - createAccount: connect.NewClient[sdp_go.CreateAccountRequest, sdp_go.CreateAccountResponse]( - httpClient, - baseURL+AdminServiceCreateAccountProcedure, - connect.WithSchema(adminServiceMethods.ByName("CreateAccount")), - connect.WithClientOptions(opts...), - ), - updateAccount: connect.NewClient[sdp_go.AdminUpdateAccountRequest, sdp_go.UpdateAccountResponse]( - httpClient, - baseURL+AdminServiceUpdateAccountProcedure, - connect.WithSchema(adminServiceMethods.ByName("UpdateAccount")), - connect.WithClientOptions(opts...), - ), - getAccount: connect.NewClient[sdp_go.AdminGetAccountRequest, sdp_go.GetAccountResponse]( - httpClient, - baseURL+AdminServiceGetAccountProcedure, - connect.WithSchema(adminServiceMethods.ByName("GetAccount")), - connect.WithClientOptions(opts...), - ), - deleteAccount: connect.NewClient[sdp_go.AdminDeleteAccountRequest, sdp_go.AdminDeleteAccountResponse]( - httpClient, - baseURL+AdminServiceDeleteAccountProcedure, - connect.WithSchema(adminServiceMethods.ByName("DeleteAccount")), - connect.WithClientOptions(opts...), - ), - listSources: connect.NewClient[sdp_go.AdminListSourcesRequest, sdp_go.ListSourcesResponse]( - httpClient, - baseURL+AdminServiceListSourcesProcedure, - connect.WithSchema(adminServiceMethods.ByName("ListSources")), - connect.WithClientOptions(opts...), - ), - createSource: connect.NewClient[sdp_go.AdminCreateSourceRequest, sdp_go.CreateSourceResponse]( - httpClient, - baseURL+AdminServiceCreateSourceProcedure, - connect.WithSchema(adminServiceMethods.ByName("CreateSource")), - connect.WithClientOptions(opts...), - ), - getSource: connect.NewClient[sdp_go.AdminGetSourceRequest, sdp_go.GetSourceResponse]( - httpClient, - baseURL+AdminServiceGetSourceProcedure, - connect.WithSchema(adminServiceMethods.ByName("GetSource")), - connect.WithClientOptions(opts...), - ), - updateSource: connect.NewClient[sdp_go.AdminUpdateSourceRequest, sdp_go.UpdateSourceResponse]( - httpClient, - baseURL+AdminServiceUpdateSourceProcedure, - connect.WithSchema(adminServiceMethods.ByName("UpdateSource")), - connect.WithClientOptions(opts...), - ), - deleteSource: connect.NewClient[sdp_go.AdminDeleteSourceRequest, sdp_go.DeleteSourceResponse]( - httpClient, - baseURL+AdminServiceDeleteSourceProcedure, - connect.WithSchema(adminServiceMethods.ByName("DeleteSource")), - connect.WithClientOptions(opts...), - ), - keepaliveSources: connect.NewClient[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( - httpClient, - baseURL+AdminServiceKeepaliveSourcesProcedure, - connect.WithSchema(adminServiceMethods.ByName("KeepaliveSources")), - connect.WithClientOptions(opts...), - ), - createToken: connect.NewClient[sdp_go.AdminCreateTokenRequest, sdp_go.CreateTokenResponse]( - httpClient, - baseURL+AdminServiceCreateTokenProcedure, - connect.WithSchema(adminServiceMethods.ByName("CreateToken")), - connect.WithClientOptions(opts...), - ), - } -} - -// adminServiceClient implements AdminServiceClient. -type adminServiceClient struct { - listAccounts *connect.Client[sdp_go.ListAccountsRequest, sdp_go.ListAccountsResponse] - createAccount *connect.Client[sdp_go.CreateAccountRequest, sdp_go.CreateAccountResponse] - updateAccount *connect.Client[sdp_go.AdminUpdateAccountRequest, sdp_go.UpdateAccountResponse] - getAccount *connect.Client[sdp_go.AdminGetAccountRequest, sdp_go.GetAccountResponse] - deleteAccount *connect.Client[sdp_go.AdminDeleteAccountRequest, sdp_go.AdminDeleteAccountResponse] - listSources *connect.Client[sdp_go.AdminListSourcesRequest, sdp_go.ListSourcesResponse] - createSource *connect.Client[sdp_go.AdminCreateSourceRequest, sdp_go.CreateSourceResponse] - getSource *connect.Client[sdp_go.AdminGetSourceRequest, sdp_go.GetSourceResponse] - updateSource *connect.Client[sdp_go.AdminUpdateSourceRequest, sdp_go.UpdateSourceResponse] - deleteSource *connect.Client[sdp_go.AdminDeleteSourceRequest, sdp_go.DeleteSourceResponse] - keepaliveSources *connect.Client[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] - createToken *connect.Client[sdp_go.AdminCreateTokenRequest, sdp_go.CreateTokenResponse] -} - -// ListAccounts calls account.AdminService.ListAccounts. -func (c *adminServiceClient) ListAccounts(ctx context.Context, req *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) { - return c.listAccounts.CallUnary(ctx, req) -} - -// CreateAccount calls account.AdminService.CreateAccount. -func (c *adminServiceClient) CreateAccount(ctx context.Context, req *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) { - return c.createAccount.CallUnary(ctx, req) -} - -// UpdateAccount calls account.AdminService.UpdateAccount. -func (c *adminServiceClient) UpdateAccount(ctx context.Context, req *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) { - return c.updateAccount.CallUnary(ctx, req) -} - -// GetAccount calls account.AdminService.GetAccount. -func (c *adminServiceClient) GetAccount(ctx context.Context, req *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { - return c.getAccount.CallUnary(ctx, req) -} - -// DeleteAccount calls account.AdminService.DeleteAccount. -func (c *adminServiceClient) DeleteAccount(ctx context.Context, req *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) { - return c.deleteAccount.CallUnary(ctx, req) -} - -// ListSources calls account.AdminService.ListSources. -func (c *adminServiceClient) ListSources(ctx context.Context, req *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { - return c.listSources.CallUnary(ctx, req) -} - -// CreateSource calls account.AdminService.CreateSource. -func (c *adminServiceClient) CreateSource(ctx context.Context, req *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { - return c.createSource.CallUnary(ctx, req) -} - -// GetSource calls account.AdminService.GetSource. -func (c *adminServiceClient) GetSource(ctx context.Context, req *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { - return c.getSource.CallUnary(ctx, req) -} - -// UpdateSource calls account.AdminService.UpdateSource. -func (c *adminServiceClient) UpdateSource(ctx context.Context, req *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { - return c.updateSource.CallUnary(ctx, req) -} - -// DeleteSource calls account.AdminService.DeleteSource. -func (c *adminServiceClient) DeleteSource(ctx context.Context, req *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { - return c.deleteSource.CallUnary(ctx, req) -} - -// KeepaliveSources calls account.AdminService.KeepaliveSources. -func (c *adminServiceClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return c.keepaliveSources.CallUnary(ctx, req) -} - -// CreateToken calls account.AdminService.CreateToken. -func (c *adminServiceClient) CreateToken(ctx context.Context, req *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { - return c.createToken.CallUnary(ctx, req) -} - -// AdminServiceHandler is an implementation of the account.AdminService service. -type AdminServiceHandler interface { - // Lists the details of all NATS Accounts - ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) - // Creates a new account, public_nkey will be autogenerated - CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) - // Updates account details, returns the account - UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) - // Get the details of a given account - GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) - // Completely deletes an account. This includes all of the data in that - // account, bookmarks, changes etc. It also deletes all users from Auth0 - // that are associated with this account - DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) - // Lists all sources within the chosen account - ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) - // Creates a new source within the chosen account - CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) - // Get the details of a source within the chosen account - GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) - // Update the details of a source within the chosen account - UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) - // Deletes a source from a chosen account - DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) - // Updates sources to keep them running in the background. This can be used - // to add explicit action, when the built-in keepalives are not sufficient. - KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) - // Create a new NATS token for a given public NKey. The user requesting must - // control the associated private key also in order to connect to NATS as - // the token is not enough on its own - CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) -} - -// NewAdminServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewAdminServiceHandler(svc AdminServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - adminServiceMethods := sdp_go.File_account_proto.Services().ByName("AdminService").Methods() - adminServiceListAccountsHandler := connect.NewUnaryHandler( - AdminServiceListAccountsProcedure, - svc.ListAccounts, - connect.WithSchema(adminServiceMethods.ByName("ListAccounts")), - connect.WithHandlerOptions(opts...), - ) - adminServiceCreateAccountHandler := connect.NewUnaryHandler( - AdminServiceCreateAccountProcedure, - svc.CreateAccount, - connect.WithSchema(adminServiceMethods.ByName("CreateAccount")), - connect.WithHandlerOptions(opts...), - ) - adminServiceUpdateAccountHandler := connect.NewUnaryHandler( - AdminServiceUpdateAccountProcedure, - svc.UpdateAccount, - connect.WithSchema(adminServiceMethods.ByName("UpdateAccount")), - connect.WithHandlerOptions(opts...), - ) - adminServiceGetAccountHandler := connect.NewUnaryHandler( - AdminServiceGetAccountProcedure, - svc.GetAccount, - connect.WithSchema(adminServiceMethods.ByName("GetAccount")), - connect.WithHandlerOptions(opts...), - ) - adminServiceDeleteAccountHandler := connect.NewUnaryHandler( - AdminServiceDeleteAccountProcedure, - svc.DeleteAccount, - connect.WithSchema(adminServiceMethods.ByName("DeleteAccount")), - connect.WithHandlerOptions(opts...), - ) - adminServiceListSourcesHandler := connect.NewUnaryHandler( - AdminServiceListSourcesProcedure, - svc.ListSources, - connect.WithSchema(adminServiceMethods.ByName("ListSources")), - connect.WithHandlerOptions(opts...), - ) - adminServiceCreateSourceHandler := connect.NewUnaryHandler( - AdminServiceCreateSourceProcedure, - svc.CreateSource, - connect.WithSchema(adminServiceMethods.ByName("CreateSource")), - connect.WithHandlerOptions(opts...), - ) - adminServiceGetSourceHandler := connect.NewUnaryHandler( - AdminServiceGetSourceProcedure, - svc.GetSource, - connect.WithSchema(adminServiceMethods.ByName("GetSource")), - connect.WithHandlerOptions(opts...), - ) - adminServiceUpdateSourceHandler := connect.NewUnaryHandler( - AdminServiceUpdateSourceProcedure, - svc.UpdateSource, - connect.WithSchema(adminServiceMethods.ByName("UpdateSource")), - connect.WithHandlerOptions(opts...), - ) - adminServiceDeleteSourceHandler := connect.NewUnaryHandler( - AdminServiceDeleteSourceProcedure, - svc.DeleteSource, - connect.WithSchema(adminServiceMethods.ByName("DeleteSource")), - connect.WithHandlerOptions(opts...), - ) - adminServiceKeepaliveSourcesHandler := connect.NewUnaryHandler( - AdminServiceKeepaliveSourcesProcedure, - svc.KeepaliveSources, - connect.WithSchema(adminServiceMethods.ByName("KeepaliveSources")), - connect.WithHandlerOptions(opts...), - ) - adminServiceCreateTokenHandler := connect.NewUnaryHandler( - AdminServiceCreateTokenProcedure, - svc.CreateToken, - connect.WithSchema(adminServiceMethods.ByName("CreateToken")), - connect.WithHandlerOptions(opts...), - ) - return "/account.AdminService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case AdminServiceListAccountsProcedure: - adminServiceListAccountsHandler.ServeHTTP(w, r) - case AdminServiceCreateAccountProcedure: - adminServiceCreateAccountHandler.ServeHTTP(w, r) - case AdminServiceUpdateAccountProcedure: - adminServiceUpdateAccountHandler.ServeHTTP(w, r) - case AdminServiceGetAccountProcedure: - adminServiceGetAccountHandler.ServeHTTP(w, r) - case AdminServiceDeleteAccountProcedure: - adminServiceDeleteAccountHandler.ServeHTTP(w, r) - case AdminServiceListSourcesProcedure: - adminServiceListSourcesHandler.ServeHTTP(w, r) - case AdminServiceCreateSourceProcedure: - adminServiceCreateSourceHandler.ServeHTTP(w, r) - case AdminServiceGetSourceProcedure: - adminServiceGetSourceHandler.ServeHTTP(w, r) - case AdminServiceUpdateSourceProcedure: - adminServiceUpdateSourceHandler.ServeHTTP(w, r) - case AdminServiceDeleteSourceProcedure: - adminServiceDeleteSourceHandler.ServeHTTP(w, r) - case AdminServiceKeepaliveSourcesProcedure: - adminServiceKeepaliveSourcesHandler.ServeHTTP(w, r) - case AdminServiceCreateTokenProcedure: - adminServiceCreateTokenHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedAdminServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedAdminServiceHandler struct{} - -func (UnimplementedAdminServiceHandler) ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.ListAccounts is not implemented")) -} - -func (UnimplementedAdminServiceHandler) CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateAccount is not implemented")) -} - -func (UnimplementedAdminServiceHandler) UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.UpdateAccount is not implemented")) -} - -func (UnimplementedAdminServiceHandler) GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.GetAccount is not implemented")) -} - -func (UnimplementedAdminServiceHandler) DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.DeleteAccount is not implemented")) -} - -func (UnimplementedAdminServiceHandler) ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.ListSources is not implemented")) -} - -func (UnimplementedAdminServiceHandler) CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateSource is not implemented")) -} - -func (UnimplementedAdminServiceHandler) GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.GetSource is not implemented")) -} - -func (UnimplementedAdminServiceHandler) UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.UpdateSource is not implemented")) -} - -func (UnimplementedAdminServiceHandler) DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.DeleteSource is not implemented")) -} - -func (UnimplementedAdminServiceHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.KeepaliveSources is not implemented")) -} - -func (UnimplementedAdminServiceHandler) CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateToken is not implemented")) -} - -// ManagementServiceClient is a client for the account.ManagementService service. -type ManagementServiceClient interface { - // Get the details of the account that this user belongs to - GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) - // Completely deletes the user's account. This includes all of the data in - // that account, bookmarks, changes etc. It also deletes the current user, - // and all other users in that account from Auth0 - DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) - // Lists all sources within the user's account - ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) - // Creates a new source within the user's account - CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) - // Get the details of a source - GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) - // Update the details of a source - UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) - // Deletes a source from a user's account - DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) - // Sources heartbeat and health - // List of all recently active sources and their health, includes information from srcman - // meaning that it can show the status of managed sources that have not started and - // connected yet - ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) - // Lists all active sources and their health. This should be used to determine - // what types, scopes etc are available rather than `ListAllSourcesStatus` since - // this endpoint only include running, available sources - ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) - // Heartbeat from a source to keep it registered and healthy - SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) - // Updates sources to keep them running in the background. This can be used - // to add explicit action, when the built-in keepalives are not sufficient. - // A user can specify how long they are willing to wait and will get a - // response either when all sources start, or when the timeout is reached. - // If the timeout is reached the response will contain the current state of - // all sources at that moment - KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) - // Create a new NATS token for a given public NKey. The user requesting must - // control the associated private key also in order to connect to NATS as - // the token is not enough on its own - CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) - // Ensure that all reverse links are populated. This does internal debouncing - // so the actual logic does only run when required. - RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest]) (*connect.ServerStreamForClient[sdp_go.RevlinkWarmupResponse], error) - // Lists all the available item types that can be discovered by sources that are running and healthy - ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) - // Get status of a single source by UUID - GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) - // Get and set onboarding status for users - GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) - SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) - // List team members in the current user's account (excludes the active user) - ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) - // Get welcome information for the current user - GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) - // Set github installation ID for the account - SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) - // this will unset the github installation ID for the account, allowing the user to install the github app again - // it will also remove the organisation profile, so we no longer generate signals for that org - UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) -} - -// NewManagementServiceClient constructs a client for the account.ManagementService service. By -// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, -// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewManagementServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ManagementServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - managementServiceMethods := sdp_go.File_account_proto.Services().ByName("ManagementService").Methods() - return &managementServiceClient{ - getAccount: connect.NewClient[sdp_go.GetAccountRequest, sdp_go.GetAccountResponse]( - httpClient, - baseURL+ManagementServiceGetAccountProcedure, - connect.WithSchema(managementServiceMethods.ByName("GetAccount")), - connect.WithClientOptions(opts...), - ), - deleteAccount: connect.NewClient[sdp_go.DeleteAccountRequest, sdp_go.DeleteAccountResponse]( - httpClient, - baseURL+ManagementServiceDeleteAccountProcedure, - connect.WithSchema(managementServiceMethods.ByName("DeleteAccount")), - connect.WithClientOptions(opts...), - ), - listSources: connect.NewClient[sdp_go.ListSourcesRequest, sdp_go.ListSourcesResponse]( - httpClient, - baseURL+ManagementServiceListSourcesProcedure, - connect.WithSchema(managementServiceMethods.ByName("ListSources")), - connect.WithClientOptions(opts...), - ), - createSource: connect.NewClient[sdp_go.CreateSourceRequest, sdp_go.CreateSourceResponse]( - httpClient, - baseURL+ManagementServiceCreateSourceProcedure, - connect.WithSchema(managementServiceMethods.ByName("CreateSource")), - connect.WithClientOptions(opts...), - ), - getSource: connect.NewClient[sdp_go.GetSourceRequest, sdp_go.GetSourceResponse]( - httpClient, - baseURL+ManagementServiceGetSourceProcedure, - connect.WithSchema(managementServiceMethods.ByName("GetSource")), - connect.WithClientOptions(opts...), - ), - updateSource: connect.NewClient[sdp_go.UpdateSourceRequest, sdp_go.UpdateSourceResponse]( - httpClient, - baseURL+ManagementServiceUpdateSourceProcedure, - connect.WithSchema(managementServiceMethods.ByName("UpdateSource")), - connect.WithClientOptions(opts...), - ), - deleteSource: connect.NewClient[sdp_go.DeleteSourceRequest, sdp_go.DeleteSourceResponse]( - httpClient, - baseURL+ManagementServiceDeleteSourceProcedure, - connect.WithSchema(managementServiceMethods.ByName("DeleteSource")), - connect.WithClientOptions(opts...), - ), - listAllSourcesStatus: connect.NewClient[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse]( - httpClient, - baseURL+ManagementServiceListAllSourcesStatusProcedure, - connect.WithSchema(managementServiceMethods.ByName("ListAllSourcesStatus")), - connect.WithClientOptions(opts...), - ), - listActiveSourcesStatus: connect.NewClient[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse]( - httpClient, - baseURL+ManagementServiceListActiveSourcesStatusProcedure, - connect.WithSchema(managementServiceMethods.ByName("ListActiveSourcesStatus")), - connect.WithClientOptions(opts...), - ), - submitSourceHeartbeat: connect.NewClient[sdp_go.SubmitSourceHeartbeatRequest, sdp_go.SubmitSourceHeartbeatResponse]( - httpClient, - baseURL+ManagementServiceSubmitSourceHeartbeatProcedure, - connect.WithSchema(managementServiceMethods.ByName("SubmitSourceHeartbeat")), - connect.WithClientOptions(opts...), - ), - keepaliveSources: connect.NewClient[sdp_go.KeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( - httpClient, - baseURL+ManagementServiceKeepaliveSourcesProcedure, - connect.WithSchema(managementServiceMethods.ByName("KeepaliveSources")), - connect.WithClientOptions(opts...), - ), - createToken: connect.NewClient[sdp_go.CreateTokenRequest, sdp_go.CreateTokenResponse]( - httpClient, - baseURL+ManagementServiceCreateTokenProcedure, - connect.WithSchema(managementServiceMethods.ByName("CreateToken")), - connect.WithClientOptions(opts...), - ), - revlinkWarmup: connect.NewClient[sdp_go.RevlinkWarmupRequest, sdp_go.RevlinkWarmupResponse]( - httpClient, - baseURL+ManagementServiceRevlinkWarmupProcedure, - connect.WithSchema(managementServiceMethods.ByName("RevlinkWarmup")), - connect.WithClientOptions(opts...), - ), - listAvailableItemTypes: connect.NewClient[sdp_go.ListAvailableItemTypesRequest, sdp_go.ListAvailableItemTypesResponse]( - httpClient, - baseURL+ManagementServiceListAvailableItemTypesProcedure, - connect.WithSchema(managementServiceMethods.ByName("ListAvailableItemTypes")), - connect.WithClientOptions(opts...), - ), - getSourceStatus: connect.NewClient[sdp_go.GetSourceStatusRequest, sdp_go.GetSourceStatusResponse]( - httpClient, - baseURL+ManagementServiceGetSourceStatusProcedure, - connect.WithSchema(managementServiceMethods.ByName("GetSourceStatus")), - connect.WithClientOptions(opts...), - ), - getUserOnboardingStatus: connect.NewClient[sdp_go.GetUserOnboardingStatusRequest, sdp_go.GetUserOnboardingStatusResponse]( - httpClient, - baseURL+ManagementServiceGetUserOnboardingStatusProcedure, - connect.WithSchema(managementServiceMethods.ByName("GetUserOnboardingStatus")), - connect.WithClientOptions(opts...), - ), - setUserOnboardingStatus: connect.NewClient[sdp_go.SetUserOnboardingStatusRequest, sdp_go.SetUserOnboardingStatusResponse]( - httpClient, - baseURL+ManagementServiceSetUserOnboardingStatusProcedure, - connect.WithSchema(managementServiceMethods.ByName("SetUserOnboardingStatus")), - connect.WithClientOptions(opts...), - ), - listTeamMembers: connect.NewClient[sdp_go.ListTeamMembersRequest, sdp_go.ListTeamMembersResponse]( - httpClient, - baseURL+ManagementServiceListTeamMembersProcedure, - connect.WithSchema(managementServiceMethods.ByName("ListTeamMembers")), - connect.WithClientOptions(opts...), - ), - getWelcomeScreenInformation: connect.NewClient[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse]( - httpClient, - baseURL+ManagementServiceGetWelcomeScreenInformationProcedure, - connect.WithSchema(managementServiceMethods.ByName("GetWelcomeScreenInformation")), - connect.WithClientOptions(opts...), - ), - setGithubInstallationID: connect.NewClient[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse]( - httpClient, - baseURL+ManagementServiceSetGithubInstallationIDProcedure, - connect.WithSchema(managementServiceMethods.ByName("SetGithubInstallationID")), - connect.WithClientOptions(opts...), - ), - unsetGithubInstallationID: connect.NewClient[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse]( - httpClient, - baseURL+ManagementServiceUnsetGithubInstallationIDProcedure, - connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), - connect.WithClientOptions(opts...), - ), - } -} - -// managementServiceClient implements ManagementServiceClient. -type managementServiceClient struct { - getAccount *connect.Client[sdp_go.GetAccountRequest, sdp_go.GetAccountResponse] - deleteAccount *connect.Client[sdp_go.DeleteAccountRequest, sdp_go.DeleteAccountResponse] - listSources *connect.Client[sdp_go.ListSourcesRequest, sdp_go.ListSourcesResponse] - createSource *connect.Client[sdp_go.CreateSourceRequest, sdp_go.CreateSourceResponse] - getSource *connect.Client[sdp_go.GetSourceRequest, sdp_go.GetSourceResponse] - updateSource *connect.Client[sdp_go.UpdateSourceRequest, sdp_go.UpdateSourceResponse] - deleteSource *connect.Client[sdp_go.DeleteSourceRequest, sdp_go.DeleteSourceResponse] - listAllSourcesStatus *connect.Client[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse] - listActiveSourcesStatus *connect.Client[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse] - submitSourceHeartbeat *connect.Client[sdp_go.SubmitSourceHeartbeatRequest, sdp_go.SubmitSourceHeartbeatResponse] - keepaliveSources *connect.Client[sdp_go.KeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] - createToken *connect.Client[sdp_go.CreateTokenRequest, sdp_go.CreateTokenResponse] - revlinkWarmup *connect.Client[sdp_go.RevlinkWarmupRequest, sdp_go.RevlinkWarmupResponse] - listAvailableItemTypes *connect.Client[sdp_go.ListAvailableItemTypesRequest, sdp_go.ListAvailableItemTypesResponse] - getSourceStatus *connect.Client[sdp_go.GetSourceStatusRequest, sdp_go.GetSourceStatusResponse] - getUserOnboardingStatus *connect.Client[sdp_go.GetUserOnboardingStatusRequest, sdp_go.GetUserOnboardingStatusResponse] - setUserOnboardingStatus *connect.Client[sdp_go.SetUserOnboardingStatusRequest, sdp_go.SetUserOnboardingStatusResponse] - listTeamMembers *connect.Client[sdp_go.ListTeamMembersRequest, sdp_go.ListTeamMembersResponse] - getWelcomeScreenInformation *connect.Client[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse] - setGithubInstallationID *connect.Client[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse] - unsetGithubInstallationID *connect.Client[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse] -} - -// GetAccount calls account.ManagementService.GetAccount. -func (c *managementServiceClient) GetAccount(ctx context.Context, req *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { - return c.getAccount.CallUnary(ctx, req) -} - -// DeleteAccount calls account.ManagementService.DeleteAccount. -func (c *managementServiceClient) DeleteAccount(ctx context.Context, req *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) { - return c.deleteAccount.CallUnary(ctx, req) -} - -// ListSources calls account.ManagementService.ListSources. -func (c *managementServiceClient) ListSources(ctx context.Context, req *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { - return c.listSources.CallUnary(ctx, req) -} - -// CreateSource calls account.ManagementService.CreateSource. -func (c *managementServiceClient) CreateSource(ctx context.Context, req *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { - return c.createSource.CallUnary(ctx, req) -} - -// GetSource calls account.ManagementService.GetSource. -func (c *managementServiceClient) GetSource(ctx context.Context, req *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { - return c.getSource.CallUnary(ctx, req) -} - -// UpdateSource calls account.ManagementService.UpdateSource. -func (c *managementServiceClient) UpdateSource(ctx context.Context, req *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { - return c.updateSource.CallUnary(ctx, req) -} - -// DeleteSource calls account.ManagementService.DeleteSource. -func (c *managementServiceClient) DeleteSource(ctx context.Context, req *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { - return c.deleteSource.CallUnary(ctx, req) -} - -// ListAllSourcesStatus calls account.ManagementService.ListAllSourcesStatus. -func (c *managementServiceClient) ListAllSourcesStatus(ctx context.Context, req *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { - return c.listAllSourcesStatus.CallUnary(ctx, req) -} - -// ListActiveSourcesStatus calls account.ManagementService.ListActiveSourcesStatus. -func (c *managementServiceClient) ListActiveSourcesStatus(ctx context.Context, req *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { - return c.listActiveSourcesStatus.CallUnary(ctx, req) -} - -// SubmitSourceHeartbeat calls account.ManagementService.SubmitSourceHeartbeat. -func (c *managementServiceClient) SubmitSourceHeartbeat(ctx context.Context, req *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) { - return c.submitSourceHeartbeat.CallUnary(ctx, req) -} - -// KeepaliveSources calls account.ManagementService.KeepaliveSources. -func (c *managementServiceClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return c.keepaliveSources.CallUnary(ctx, req) -} - -// CreateToken calls account.ManagementService.CreateToken. -func (c *managementServiceClient) CreateToken(ctx context.Context, req *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { - return c.createToken.CallUnary(ctx, req) -} - -// RevlinkWarmup calls account.ManagementService.RevlinkWarmup. -func (c *managementServiceClient) RevlinkWarmup(ctx context.Context, req *connect.Request[sdp_go.RevlinkWarmupRequest]) (*connect.ServerStreamForClient[sdp_go.RevlinkWarmupResponse], error) { - return c.revlinkWarmup.CallServerStream(ctx, req) -} - -// ListAvailableItemTypes calls account.ManagementService.ListAvailableItemTypes. -func (c *managementServiceClient) ListAvailableItemTypes(ctx context.Context, req *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) { - return c.listAvailableItemTypes.CallUnary(ctx, req) -} - -// GetSourceStatus calls account.ManagementService.GetSourceStatus. -func (c *managementServiceClient) GetSourceStatus(ctx context.Context, req *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) { - return c.getSourceStatus.CallUnary(ctx, req) -} - -// GetUserOnboardingStatus calls account.ManagementService.GetUserOnboardingStatus. -func (c *managementServiceClient) GetUserOnboardingStatus(ctx context.Context, req *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) { - return c.getUserOnboardingStatus.CallUnary(ctx, req) -} - -// SetUserOnboardingStatus calls account.ManagementService.SetUserOnboardingStatus. -func (c *managementServiceClient) SetUserOnboardingStatus(ctx context.Context, req *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) { - return c.setUserOnboardingStatus.CallUnary(ctx, req) -} - -// ListTeamMembers calls account.ManagementService.ListTeamMembers. -func (c *managementServiceClient) ListTeamMembers(ctx context.Context, req *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) { - return c.listTeamMembers.CallUnary(ctx, req) -} - -// GetWelcomeScreenInformation calls account.ManagementService.GetWelcomeScreenInformation. -func (c *managementServiceClient) GetWelcomeScreenInformation(ctx context.Context, req *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) { - return c.getWelcomeScreenInformation.CallUnary(ctx, req) -} - -// SetGithubInstallationID calls account.ManagementService.SetGithubInstallationID. -func (c *managementServiceClient) SetGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) { - return c.setGithubInstallationID.CallUnary(ctx, req) -} - -// UnsetGithubInstallationID calls account.ManagementService.UnsetGithubInstallationID. -func (c *managementServiceClient) UnsetGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { - return c.unsetGithubInstallationID.CallUnary(ctx, req) -} - -// ManagementServiceHandler is an implementation of the account.ManagementService service. -type ManagementServiceHandler interface { - // Get the details of the account that this user belongs to - GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) - // Completely deletes the user's account. This includes all of the data in - // that account, bookmarks, changes etc. It also deletes the current user, - // and all other users in that account from Auth0 - DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) - // Lists all sources within the user's account - ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) - // Creates a new source within the user's account - CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) - // Get the details of a source - GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) - // Update the details of a source - UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) - // Deletes a source from a user's account - DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) - // Sources heartbeat and health - // List of all recently active sources and their health, includes information from srcman - // meaning that it can show the status of managed sources that have not started and - // connected yet - ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) - // Lists all active sources and their health. This should be used to determine - // what types, scopes etc are available rather than `ListAllSourcesStatus` since - // this endpoint only include running, available sources - ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) - // Heartbeat from a source to keep it registered and healthy - SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) - // Updates sources to keep them running in the background. This can be used - // to add explicit action, when the built-in keepalives are not sufficient. - // A user can specify how long they are willing to wait and will get a - // response either when all sources start, or when the timeout is reached. - // If the timeout is reached the response will contain the current state of - // all sources at that moment - KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) - // Create a new NATS token for a given public NKey. The user requesting must - // control the associated private key also in order to connect to NATS as - // the token is not enough on its own - CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) - // Ensure that all reverse links are populated. This does internal debouncing - // so the actual logic does only run when required. - RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest], *connect.ServerStream[sdp_go.RevlinkWarmupResponse]) error - // Lists all the available item types that can be discovered by sources that are running and healthy - ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) - // Get status of a single source by UUID - GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) - // Get and set onboarding status for users - GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) - SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) - // List team members in the current user's account (excludes the active user) - ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) - // Get welcome information for the current user - GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) - // Set github installation ID for the account - SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) - // this will unset the github installation ID for the account, allowing the user to install the github app again - // it will also remove the organisation profile, so we no longer generate signals for that org - UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) -} - -// NewManagementServiceHandler builds an HTTP handler from the service implementation. It returns -// the path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - managementServiceMethods := sdp_go.File_account_proto.Services().ByName("ManagementService").Methods() - managementServiceGetAccountHandler := connect.NewUnaryHandler( - ManagementServiceGetAccountProcedure, - svc.GetAccount, - connect.WithSchema(managementServiceMethods.ByName("GetAccount")), - connect.WithHandlerOptions(opts...), - ) - managementServiceDeleteAccountHandler := connect.NewUnaryHandler( - ManagementServiceDeleteAccountProcedure, - svc.DeleteAccount, - connect.WithSchema(managementServiceMethods.ByName("DeleteAccount")), - connect.WithHandlerOptions(opts...), - ) - managementServiceListSourcesHandler := connect.NewUnaryHandler( - ManagementServiceListSourcesProcedure, - svc.ListSources, - connect.WithSchema(managementServiceMethods.ByName("ListSources")), - connect.WithHandlerOptions(opts...), - ) - managementServiceCreateSourceHandler := connect.NewUnaryHandler( - ManagementServiceCreateSourceProcedure, - svc.CreateSource, - connect.WithSchema(managementServiceMethods.ByName("CreateSource")), - connect.WithHandlerOptions(opts...), - ) - managementServiceGetSourceHandler := connect.NewUnaryHandler( - ManagementServiceGetSourceProcedure, - svc.GetSource, - connect.WithSchema(managementServiceMethods.ByName("GetSource")), - connect.WithHandlerOptions(opts...), - ) - managementServiceUpdateSourceHandler := connect.NewUnaryHandler( - ManagementServiceUpdateSourceProcedure, - svc.UpdateSource, - connect.WithSchema(managementServiceMethods.ByName("UpdateSource")), - connect.WithHandlerOptions(opts...), - ) - managementServiceDeleteSourceHandler := connect.NewUnaryHandler( - ManagementServiceDeleteSourceProcedure, - svc.DeleteSource, - connect.WithSchema(managementServiceMethods.ByName("DeleteSource")), - connect.WithHandlerOptions(opts...), - ) - managementServiceListAllSourcesStatusHandler := connect.NewUnaryHandler( - ManagementServiceListAllSourcesStatusProcedure, - svc.ListAllSourcesStatus, - connect.WithSchema(managementServiceMethods.ByName("ListAllSourcesStatus")), - connect.WithHandlerOptions(opts...), - ) - managementServiceListActiveSourcesStatusHandler := connect.NewUnaryHandler( - ManagementServiceListActiveSourcesStatusProcedure, - svc.ListActiveSourcesStatus, - connect.WithSchema(managementServiceMethods.ByName("ListActiveSourcesStatus")), - connect.WithHandlerOptions(opts...), - ) - managementServiceSubmitSourceHeartbeatHandler := connect.NewUnaryHandler( - ManagementServiceSubmitSourceHeartbeatProcedure, - svc.SubmitSourceHeartbeat, - connect.WithSchema(managementServiceMethods.ByName("SubmitSourceHeartbeat")), - connect.WithHandlerOptions(opts...), - ) - managementServiceKeepaliveSourcesHandler := connect.NewUnaryHandler( - ManagementServiceKeepaliveSourcesProcedure, - svc.KeepaliveSources, - connect.WithSchema(managementServiceMethods.ByName("KeepaliveSources")), - connect.WithHandlerOptions(opts...), - ) - managementServiceCreateTokenHandler := connect.NewUnaryHandler( - ManagementServiceCreateTokenProcedure, - svc.CreateToken, - connect.WithSchema(managementServiceMethods.ByName("CreateToken")), - connect.WithHandlerOptions(opts...), - ) - managementServiceRevlinkWarmupHandler := connect.NewServerStreamHandler( - ManagementServiceRevlinkWarmupProcedure, - svc.RevlinkWarmup, - connect.WithSchema(managementServiceMethods.ByName("RevlinkWarmup")), - connect.WithHandlerOptions(opts...), - ) - managementServiceListAvailableItemTypesHandler := connect.NewUnaryHandler( - ManagementServiceListAvailableItemTypesProcedure, - svc.ListAvailableItemTypes, - connect.WithSchema(managementServiceMethods.ByName("ListAvailableItemTypes")), - connect.WithHandlerOptions(opts...), - ) - managementServiceGetSourceStatusHandler := connect.NewUnaryHandler( - ManagementServiceGetSourceStatusProcedure, - svc.GetSourceStatus, - connect.WithSchema(managementServiceMethods.ByName("GetSourceStatus")), - connect.WithHandlerOptions(opts...), - ) - managementServiceGetUserOnboardingStatusHandler := connect.NewUnaryHandler( - ManagementServiceGetUserOnboardingStatusProcedure, - svc.GetUserOnboardingStatus, - connect.WithSchema(managementServiceMethods.ByName("GetUserOnboardingStatus")), - connect.WithHandlerOptions(opts...), - ) - managementServiceSetUserOnboardingStatusHandler := connect.NewUnaryHandler( - ManagementServiceSetUserOnboardingStatusProcedure, - svc.SetUserOnboardingStatus, - connect.WithSchema(managementServiceMethods.ByName("SetUserOnboardingStatus")), - connect.WithHandlerOptions(opts...), - ) - managementServiceListTeamMembersHandler := connect.NewUnaryHandler( - ManagementServiceListTeamMembersProcedure, - svc.ListTeamMembers, - connect.WithSchema(managementServiceMethods.ByName("ListTeamMembers")), - connect.WithHandlerOptions(opts...), - ) - managementServiceGetWelcomeScreenInformationHandler := connect.NewUnaryHandler( - ManagementServiceGetWelcomeScreenInformationProcedure, - svc.GetWelcomeScreenInformation, - connect.WithSchema(managementServiceMethods.ByName("GetWelcomeScreenInformation")), - connect.WithHandlerOptions(opts...), - ) - managementServiceSetGithubInstallationIDHandler := connect.NewUnaryHandler( - ManagementServiceSetGithubInstallationIDProcedure, - svc.SetGithubInstallationID, - connect.WithSchema(managementServiceMethods.ByName("SetGithubInstallationID")), - connect.WithHandlerOptions(opts...), - ) - managementServiceUnsetGithubInstallationIDHandler := connect.NewUnaryHandler( - ManagementServiceUnsetGithubInstallationIDProcedure, - svc.UnsetGithubInstallationID, - connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), - connect.WithHandlerOptions(opts...), - ) - return "/account.ManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case ManagementServiceGetAccountProcedure: - managementServiceGetAccountHandler.ServeHTTP(w, r) - case ManagementServiceDeleteAccountProcedure: - managementServiceDeleteAccountHandler.ServeHTTP(w, r) - case ManagementServiceListSourcesProcedure: - managementServiceListSourcesHandler.ServeHTTP(w, r) - case ManagementServiceCreateSourceProcedure: - managementServiceCreateSourceHandler.ServeHTTP(w, r) - case ManagementServiceGetSourceProcedure: - managementServiceGetSourceHandler.ServeHTTP(w, r) - case ManagementServiceUpdateSourceProcedure: - managementServiceUpdateSourceHandler.ServeHTTP(w, r) - case ManagementServiceDeleteSourceProcedure: - managementServiceDeleteSourceHandler.ServeHTTP(w, r) - case ManagementServiceListAllSourcesStatusProcedure: - managementServiceListAllSourcesStatusHandler.ServeHTTP(w, r) - case ManagementServiceListActiveSourcesStatusProcedure: - managementServiceListActiveSourcesStatusHandler.ServeHTTP(w, r) - case ManagementServiceSubmitSourceHeartbeatProcedure: - managementServiceSubmitSourceHeartbeatHandler.ServeHTTP(w, r) - case ManagementServiceKeepaliveSourcesProcedure: - managementServiceKeepaliveSourcesHandler.ServeHTTP(w, r) - case ManagementServiceCreateTokenProcedure: - managementServiceCreateTokenHandler.ServeHTTP(w, r) - case ManagementServiceRevlinkWarmupProcedure: - managementServiceRevlinkWarmupHandler.ServeHTTP(w, r) - case ManagementServiceListAvailableItemTypesProcedure: - managementServiceListAvailableItemTypesHandler.ServeHTTP(w, r) - case ManagementServiceGetSourceStatusProcedure: - managementServiceGetSourceStatusHandler.ServeHTTP(w, r) - case ManagementServiceGetUserOnboardingStatusProcedure: - managementServiceGetUserOnboardingStatusHandler.ServeHTTP(w, r) - case ManagementServiceSetUserOnboardingStatusProcedure: - managementServiceSetUserOnboardingStatusHandler.ServeHTTP(w, r) - case ManagementServiceListTeamMembersProcedure: - managementServiceListTeamMembersHandler.ServeHTTP(w, r) - case ManagementServiceGetWelcomeScreenInformationProcedure: - managementServiceGetWelcomeScreenInformationHandler.ServeHTTP(w, r) - case ManagementServiceSetGithubInstallationIDProcedure: - managementServiceSetGithubInstallationIDHandler.ServeHTTP(w, r) - case ManagementServiceUnsetGithubInstallationIDProcedure: - managementServiceUnsetGithubInstallationIDHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedManagementServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedManagementServiceHandler struct{} - -func (UnimplementedManagementServiceHandler) GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetAccount is not implemented")) -} - -func (UnimplementedManagementServiceHandler) DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.DeleteAccount is not implemented")) -} - -func (UnimplementedManagementServiceHandler) ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListSources is not implemented")) -} - -func (UnimplementedManagementServiceHandler) CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.CreateSource is not implemented")) -} - -func (UnimplementedManagementServiceHandler) GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetSource is not implemented")) -} - -func (UnimplementedManagementServiceHandler) UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UpdateSource is not implemented")) -} - -func (UnimplementedManagementServiceHandler) DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.DeleteSource is not implemented")) -} - -func (UnimplementedManagementServiceHandler) ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListAllSourcesStatus is not implemented")) -} - -func (UnimplementedManagementServiceHandler) ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListActiveSourcesStatus is not implemented")) -} - -func (UnimplementedManagementServiceHandler) SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SubmitSourceHeartbeat is not implemented")) -} - -func (UnimplementedManagementServiceHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.KeepaliveSources is not implemented")) -} - -func (UnimplementedManagementServiceHandler) CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.CreateToken is not implemented")) -} - -func (UnimplementedManagementServiceHandler) RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest], *connect.ServerStream[sdp_go.RevlinkWarmupResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.RevlinkWarmup is not implemented")) -} - -func (UnimplementedManagementServiceHandler) ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListAvailableItemTypes is not implemented")) -} - -func (UnimplementedManagementServiceHandler) GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetSourceStatus is not implemented")) -} - -func (UnimplementedManagementServiceHandler) GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetUserOnboardingStatus is not implemented")) -} - -func (UnimplementedManagementServiceHandler) SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SetUserOnboardingStatus is not implemented")) -} - -func (UnimplementedManagementServiceHandler) ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListTeamMembers is not implemented")) -} - -func (UnimplementedManagementServiceHandler) GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetWelcomeScreenInformation is not implemented")) -} - -func (UnimplementedManagementServiceHandler) SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SetGithubInstallationID is not implemented")) -} - -func (UnimplementedManagementServiceHandler) UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UnsetGithubInstallationID is not implemented")) -} diff --git a/sdp-go/sdpconnect/apikeys.connect.go b/sdp-go/sdpconnect/apikeys.connect.go deleted file mode 100644 index 6098fb80..00000000 --- a/sdp-go/sdpconnect/apikeys.connect.go +++ /dev/null @@ -1,298 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: apikeys.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // ApiKeyServiceName is the fully-qualified name of the ApiKeyService service. - ApiKeyServiceName = "apikeys.ApiKeyService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // ApiKeyServiceCreateAPIKeyProcedure is the fully-qualified name of the ApiKeyService's - // CreateAPIKey RPC. - ApiKeyServiceCreateAPIKeyProcedure = "/apikeys.ApiKeyService/CreateAPIKey" - // ApiKeyServiceRefreshAPIKeyProcedure is the fully-qualified name of the ApiKeyService's - // RefreshAPIKey RPC. - ApiKeyServiceRefreshAPIKeyProcedure = "/apikeys.ApiKeyService/RefreshAPIKey" - // ApiKeyServiceGetAPIKeyProcedure is the fully-qualified name of the ApiKeyService's GetAPIKey RPC. - ApiKeyServiceGetAPIKeyProcedure = "/apikeys.ApiKeyService/GetAPIKey" - // ApiKeyServiceUpdateAPIKeyProcedure is the fully-qualified name of the ApiKeyService's - // UpdateAPIKey RPC. - ApiKeyServiceUpdateAPIKeyProcedure = "/apikeys.ApiKeyService/UpdateAPIKey" - // ApiKeyServiceListAPIKeysProcedure is the fully-qualified name of the ApiKeyService's ListAPIKeys - // RPC. - ApiKeyServiceListAPIKeysProcedure = "/apikeys.ApiKeyService/ListAPIKeys" - // ApiKeyServiceDeleteAPIKeyProcedure is the fully-qualified name of the ApiKeyService's - // DeleteAPIKey RPC. - ApiKeyServiceDeleteAPIKeyProcedure = "/apikeys.ApiKeyService/DeleteAPIKey" - // ApiKeyServiceExchangeKeyForTokenProcedure is the fully-qualified name of the ApiKeyService's - // ExchangeKeyForToken RPC. - ApiKeyServiceExchangeKeyForTokenProcedure = "/apikeys.ApiKeyService/ExchangeKeyForToken" -) - -// ApiKeyServiceClient is a client for the apikeys.ApiKeyService service. -type ApiKeyServiceClient interface { - // Creates an API key, pending access token generation from Auth0. The key - // cannot be used until the user has been redirected to the given URL which - // allows Auth0 to actually generate an access token - CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) - // Refreshes an API key, returning a new one with the same metadata and - // properties. The response will be the same as CreateAPIKey, and requires - // the same redirect handling to authenticate the new key. - RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) - GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) - UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) - ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) - DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) - // Exchanges an Overmind API key for an Oauth access token. That token can - // then be used to access all other Overmind APIs - ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) -} - -// NewApiKeyServiceClient constructs a client for the apikeys.ApiKeyService service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewApiKeyServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ApiKeyServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - apiKeyServiceMethods := sdp_go.File_apikeys_proto.Services().ByName("ApiKeyService").Methods() - return &apiKeyServiceClient{ - createAPIKey: connect.NewClient[sdp_go.CreateAPIKeyRequest, sdp_go.CreateAPIKeyResponse]( - httpClient, - baseURL+ApiKeyServiceCreateAPIKeyProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("CreateAPIKey")), - connect.WithClientOptions(opts...), - ), - refreshAPIKey: connect.NewClient[sdp_go.RefreshAPIKeyRequest, sdp_go.RefreshAPIKeyResponse]( - httpClient, - baseURL+ApiKeyServiceRefreshAPIKeyProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("RefreshAPIKey")), - connect.WithClientOptions(opts...), - ), - getAPIKey: connect.NewClient[sdp_go.GetAPIKeyRequest, sdp_go.GetAPIKeyResponse]( - httpClient, - baseURL+ApiKeyServiceGetAPIKeyProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("GetAPIKey")), - connect.WithClientOptions(opts...), - ), - updateAPIKey: connect.NewClient[sdp_go.UpdateAPIKeyRequest, sdp_go.UpdateAPIKeyResponse]( - httpClient, - baseURL+ApiKeyServiceUpdateAPIKeyProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("UpdateAPIKey")), - connect.WithClientOptions(opts...), - ), - listAPIKeys: connect.NewClient[sdp_go.ListAPIKeysRequest, sdp_go.ListAPIKeysResponse]( - httpClient, - baseURL+ApiKeyServiceListAPIKeysProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("ListAPIKeys")), - connect.WithClientOptions(opts...), - ), - deleteAPIKey: connect.NewClient[sdp_go.DeleteAPIKeyRequest, sdp_go.DeleteAPIKeyResponse]( - httpClient, - baseURL+ApiKeyServiceDeleteAPIKeyProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("DeleteAPIKey")), - connect.WithClientOptions(opts...), - ), - exchangeKeyForToken: connect.NewClient[sdp_go.ExchangeKeyForTokenRequest, sdp_go.ExchangeKeyForTokenResponse]( - httpClient, - baseURL+ApiKeyServiceExchangeKeyForTokenProcedure, - connect.WithSchema(apiKeyServiceMethods.ByName("ExchangeKeyForToken")), - connect.WithClientOptions(opts...), - ), - } -} - -// apiKeyServiceClient implements ApiKeyServiceClient. -type apiKeyServiceClient struct { - createAPIKey *connect.Client[sdp_go.CreateAPIKeyRequest, sdp_go.CreateAPIKeyResponse] - refreshAPIKey *connect.Client[sdp_go.RefreshAPIKeyRequest, sdp_go.RefreshAPIKeyResponse] - getAPIKey *connect.Client[sdp_go.GetAPIKeyRequest, sdp_go.GetAPIKeyResponse] - updateAPIKey *connect.Client[sdp_go.UpdateAPIKeyRequest, sdp_go.UpdateAPIKeyResponse] - listAPIKeys *connect.Client[sdp_go.ListAPIKeysRequest, sdp_go.ListAPIKeysResponse] - deleteAPIKey *connect.Client[sdp_go.DeleteAPIKeyRequest, sdp_go.DeleteAPIKeyResponse] - exchangeKeyForToken *connect.Client[sdp_go.ExchangeKeyForTokenRequest, sdp_go.ExchangeKeyForTokenResponse] -} - -// CreateAPIKey calls apikeys.ApiKeyService.CreateAPIKey. -func (c *apiKeyServiceClient) CreateAPIKey(ctx context.Context, req *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) { - return c.createAPIKey.CallUnary(ctx, req) -} - -// RefreshAPIKey calls apikeys.ApiKeyService.RefreshAPIKey. -func (c *apiKeyServiceClient) RefreshAPIKey(ctx context.Context, req *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) { - return c.refreshAPIKey.CallUnary(ctx, req) -} - -// GetAPIKey calls apikeys.ApiKeyService.GetAPIKey. -func (c *apiKeyServiceClient) GetAPIKey(ctx context.Context, req *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) { - return c.getAPIKey.CallUnary(ctx, req) -} - -// UpdateAPIKey calls apikeys.ApiKeyService.UpdateAPIKey. -func (c *apiKeyServiceClient) UpdateAPIKey(ctx context.Context, req *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) { - return c.updateAPIKey.CallUnary(ctx, req) -} - -// ListAPIKeys calls apikeys.ApiKeyService.ListAPIKeys. -func (c *apiKeyServiceClient) ListAPIKeys(ctx context.Context, req *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) { - return c.listAPIKeys.CallUnary(ctx, req) -} - -// DeleteAPIKey calls apikeys.ApiKeyService.DeleteAPIKey. -func (c *apiKeyServiceClient) DeleteAPIKey(ctx context.Context, req *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) { - return c.deleteAPIKey.CallUnary(ctx, req) -} - -// ExchangeKeyForToken calls apikeys.ApiKeyService.ExchangeKeyForToken. -func (c *apiKeyServiceClient) ExchangeKeyForToken(ctx context.Context, req *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) { - return c.exchangeKeyForToken.CallUnary(ctx, req) -} - -// ApiKeyServiceHandler is an implementation of the apikeys.ApiKeyService service. -type ApiKeyServiceHandler interface { - // Creates an API key, pending access token generation from Auth0. The key - // cannot be used until the user has been redirected to the given URL which - // allows Auth0 to actually generate an access token - CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) - // Refreshes an API key, returning a new one with the same metadata and - // properties. The response will be the same as CreateAPIKey, and requires - // the same redirect handling to authenticate the new key. - RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) - GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) - UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) - ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) - DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) - // Exchanges an Overmind API key for an Oauth access token. That token can - // then be used to access all other Overmind APIs - ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) -} - -// NewApiKeyServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewApiKeyServiceHandler(svc ApiKeyServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - apiKeyServiceMethods := sdp_go.File_apikeys_proto.Services().ByName("ApiKeyService").Methods() - apiKeyServiceCreateAPIKeyHandler := connect.NewUnaryHandler( - ApiKeyServiceCreateAPIKeyProcedure, - svc.CreateAPIKey, - connect.WithSchema(apiKeyServiceMethods.ByName("CreateAPIKey")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceRefreshAPIKeyHandler := connect.NewUnaryHandler( - ApiKeyServiceRefreshAPIKeyProcedure, - svc.RefreshAPIKey, - connect.WithSchema(apiKeyServiceMethods.ByName("RefreshAPIKey")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceGetAPIKeyHandler := connect.NewUnaryHandler( - ApiKeyServiceGetAPIKeyProcedure, - svc.GetAPIKey, - connect.WithSchema(apiKeyServiceMethods.ByName("GetAPIKey")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceUpdateAPIKeyHandler := connect.NewUnaryHandler( - ApiKeyServiceUpdateAPIKeyProcedure, - svc.UpdateAPIKey, - connect.WithSchema(apiKeyServiceMethods.ByName("UpdateAPIKey")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceListAPIKeysHandler := connect.NewUnaryHandler( - ApiKeyServiceListAPIKeysProcedure, - svc.ListAPIKeys, - connect.WithSchema(apiKeyServiceMethods.ByName("ListAPIKeys")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceDeleteAPIKeyHandler := connect.NewUnaryHandler( - ApiKeyServiceDeleteAPIKeyProcedure, - svc.DeleteAPIKey, - connect.WithSchema(apiKeyServiceMethods.ByName("DeleteAPIKey")), - connect.WithHandlerOptions(opts...), - ) - apiKeyServiceExchangeKeyForTokenHandler := connect.NewUnaryHandler( - ApiKeyServiceExchangeKeyForTokenProcedure, - svc.ExchangeKeyForToken, - connect.WithSchema(apiKeyServiceMethods.ByName("ExchangeKeyForToken")), - connect.WithHandlerOptions(opts...), - ) - return "/apikeys.ApiKeyService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case ApiKeyServiceCreateAPIKeyProcedure: - apiKeyServiceCreateAPIKeyHandler.ServeHTTP(w, r) - case ApiKeyServiceRefreshAPIKeyProcedure: - apiKeyServiceRefreshAPIKeyHandler.ServeHTTP(w, r) - case ApiKeyServiceGetAPIKeyProcedure: - apiKeyServiceGetAPIKeyHandler.ServeHTTP(w, r) - case ApiKeyServiceUpdateAPIKeyProcedure: - apiKeyServiceUpdateAPIKeyHandler.ServeHTTP(w, r) - case ApiKeyServiceListAPIKeysProcedure: - apiKeyServiceListAPIKeysHandler.ServeHTTP(w, r) - case ApiKeyServiceDeleteAPIKeyProcedure: - apiKeyServiceDeleteAPIKeyHandler.ServeHTTP(w, r) - case ApiKeyServiceExchangeKeyForTokenProcedure: - apiKeyServiceExchangeKeyForTokenHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedApiKeyServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedApiKeyServiceHandler struct{} - -func (UnimplementedApiKeyServiceHandler) CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.CreateAPIKey is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.RefreshAPIKey is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.GetAPIKey is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.UpdateAPIKey is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.ListAPIKeys is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.DeleteAPIKey is not implemented")) -} - -func (UnimplementedApiKeyServiceHandler) ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.ExchangeKeyForToken is not implemented")) -} diff --git a/sdp-go/sdpconnect/area51.connect.go b/sdp-go/sdpconnect/area51.connect.go deleted file mode 100644 index 10098428..00000000 --- a/sdp-go/sdpconnect/area51.connect.go +++ /dev/null @@ -1,113 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: area51.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // Area51ServiceName is the fully-qualified name of the Area51Service service. - Area51ServiceName = "area51.Area51Service" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // Area51ServiceGetChangeArchiveProcedure is the fully-qualified name of the Area51Service's - // GetChangeArchive RPC. - Area51ServiceGetChangeArchiveProcedure = "/area51.Area51Service/GetChangeArchive" -) - -// Area51ServiceClient is a client for the area51.Area51Service service. -type Area51ServiceClient interface { - // This is not implemented at all, it prevents javascript generation errors - // we manually use the generated sdp objects in area51 service - GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) -} - -// NewArea51ServiceClient constructs a client for the area51.Area51Service service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewArea51ServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) Area51ServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - area51ServiceMethods := sdp_go.File_area51_proto.Services().ByName("Area51Service").Methods() - return &area51ServiceClient{ - getChangeArchive: connect.NewClient[sdp_go.GetChangeArchiveRequest, sdp_go.GetChangeArchiveResponse]( - httpClient, - baseURL+Area51ServiceGetChangeArchiveProcedure, - connect.WithSchema(area51ServiceMethods.ByName("GetChangeArchive")), - connect.WithClientOptions(opts...), - ), - } -} - -// area51ServiceClient implements Area51ServiceClient. -type area51ServiceClient struct { - getChangeArchive *connect.Client[sdp_go.GetChangeArchiveRequest, sdp_go.GetChangeArchiveResponse] -} - -// GetChangeArchive calls area51.Area51Service.GetChangeArchive. -func (c *area51ServiceClient) GetChangeArchive(ctx context.Context, req *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) { - return c.getChangeArchive.CallUnary(ctx, req) -} - -// Area51ServiceHandler is an implementation of the area51.Area51Service service. -type Area51ServiceHandler interface { - // This is not implemented at all, it prevents javascript generation errors - // we manually use the generated sdp objects in area51 service - GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) -} - -// NewArea51ServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewArea51ServiceHandler(svc Area51ServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - area51ServiceMethods := sdp_go.File_area51_proto.Services().ByName("Area51Service").Methods() - area51ServiceGetChangeArchiveHandler := connect.NewUnaryHandler( - Area51ServiceGetChangeArchiveProcedure, - svc.GetChangeArchive, - connect.WithSchema(area51ServiceMethods.ByName("GetChangeArchive")), - connect.WithHandlerOptions(opts...), - ) - return "/area51.Area51Service/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case Area51ServiceGetChangeArchiveProcedure: - area51ServiceGetChangeArchiveHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedArea51ServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedArea51ServiceHandler struct{} - -func (UnimplementedArea51ServiceHandler) GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("area51.Area51Service.GetChangeArchive is not implemented")) -} diff --git a/sdp-go/sdpconnect/auth0support.connect.go b/sdp-go/sdpconnect/auth0support.connect.go deleted file mode 100644 index f9b619d8..00000000 --- a/sdp-go/sdpconnect/auth0support.connect.go +++ /dev/null @@ -1,145 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: auth0support.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // Auth0SupportName is the fully-qualified name of the Auth0Support service. - Auth0SupportName = "auth0support.Auth0Support" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // Auth0SupportCreateUserProcedure is the fully-qualified name of the Auth0Support's CreateUser RPC. - Auth0SupportCreateUserProcedure = "/auth0support.Auth0Support/CreateUser" - // Auth0SupportKeepaliveSourcesProcedure is the fully-qualified name of the Auth0Support's - // KeepaliveSources RPC. - Auth0SupportKeepaliveSourcesProcedure = "/auth0support.Auth0Support/KeepaliveSources" -) - -// Auth0SupportClient is a client for the auth0support.Auth0Support service. -type Auth0SupportClient interface { - // create a new user on first login - CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) - // Updates sources to keep them running in the background. This is called on - // login by auth0 to give us a chance to boot up sources while the app is - // loading. - KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) -} - -// NewAuth0SupportClient constructs a client for the auth0support.Auth0Support service. By default, -// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and -// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() -// or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewAuth0SupportClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) Auth0SupportClient { - baseURL = strings.TrimRight(baseURL, "/") - auth0SupportMethods := sdp_go.File_auth0support_proto.Services().ByName("Auth0Support").Methods() - return &auth0SupportClient{ - createUser: connect.NewClient[sdp_go.Auth0CreateUserRequest, sdp_go.Auth0CreateUserResponse]( - httpClient, - baseURL+Auth0SupportCreateUserProcedure, - connect.WithSchema(auth0SupportMethods.ByName("CreateUser")), - connect.WithClientOptions(opts...), - ), - keepaliveSources: connect.NewClient[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( - httpClient, - baseURL+Auth0SupportKeepaliveSourcesProcedure, - connect.WithSchema(auth0SupportMethods.ByName("KeepaliveSources")), - connect.WithClientOptions(opts...), - ), - } -} - -// auth0SupportClient implements Auth0SupportClient. -type auth0SupportClient struct { - createUser *connect.Client[sdp_go.Auth0CreateUserRequest, sdp_go.Auth0CreateUserResponse] - keepaliveSources *connect.Client[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] -} - -// CreateUser calls auth0support.Auth0Support.CreateUser. -func (c *auth0SupportClient) CreateUser(ctx context.Context, req *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) { - return c.createUser.CallUnary(ctx, req) -} - -// KeepaliveSources calls auth0support.Auth0Support.KeepaliveSources. -func (c *auth0SupportClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return c.keepaliveSources.CallUnary(ctx, req) -} - -// Auth0SupportHandler is an implementation of the auth0support.Auth0Support service. -type Auth0SupportHandler interface { - // create a new user on first login - CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) - // Updates sources to keep them running in the background. This is called on - // login by auth0 to give us a chance to boot up sources while the app is - // loading. - KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) -} - -// NewAuth0SupportHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewAuth0SupportHandler(svc Auth0SupportHandler, opts ...connect.HandlerOption) (string, http.Handler) { - auth0SupportMethods := sdp_go.File_auth0support_proto.Services().ByName("Auth0Support").Methods() - auth0SupportCreateUserHandler := connect.NewUnaryHandler( - Auth0SupportCreateUserProcedure, - svc.CreateUser, - connect.WithSchema(auth0SupportMethods.ByName("CreateUser")), - connect.WithHandlerOptions(opts...), - ) - auth0SupportKeepaliveSourcesHandler := connect.NewUnaryHandler( - Auth0SupportKeepaliveSourcesProcedure, - svc.KeepaliveSources, - connect.WithSchema(auth0SupportMethods.ByName("KeepaliveSources")), - connect.WithHandlerOptions(opts...), - ) - return "/auth0support.Auth0Support/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case Auth0SupportCreateUserProcedure: - auth0SupportCreateUserHandler.ServeHTTP(w, r) - case Auth0SupportKeepaliveSourcesProcedure: - auth0SupportKeepaliveSourcesHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedAuth0SupportHandler returns CodeUnimplemented from all methods. -type UnimplementedAuth0SupportHandler struct{} - -func (UnimplementedAuth0SupportHandler) CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auth0support.Auth0Support.CreateUser is not implemented")) -} - -func (UnimplementedAuth0SupportHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auth0support.Auth0Support.KeepaliveSources is not implemented")) -} diff --git a/sdp-go/sdpconnect/bookmarks.connect.go b/sdp-go/sdpconnect/bookmarks.connect.go deleted file mode 100644 index 796833de..00000000 --- a/sdp-go/sdpconnect/bookmarks.connect.go +++ /dev/null @@ -1,262 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: bookmarks.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // BookmarksServiceName is the fully-qualified name of the BookmarksService service. - BookmarksServiceName = "bookmarks.BookmarksService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // BookmarksServiceListBookmarksProcedure is the fully-qualified name of the BookmarksService's - // ListBookmarks RPC. - BookmarksServiceListBookmarksProcedure = "/bookmarks.BookmarksService/ListBookmarks" - // BookmarksServiceCreateBookmarkProcedure is the fully-qualified name of the BookmarksService's - // CreateBookmark RPC. - BookmarksServiceCreateBookmarkProcedure = "/bookmarks.BookmarksService/CreateBookmark" - // BookmarksServiceGetBookmarkProcedure is the fully-qualified name of the BookmarksService's - // GetBookmark RPC. - BookmarksServiceGetBookmarkProcedure = "/bookmarks.BookmarksService/GetBookmark" - // BookmarksServiceUpdateBookmarkProcedure is the fully-qualified name of the BookmarksService's - // UpdateBookmark RPC. - BookmarksServiceUpdateBookmarkProcedure = "/bookmarks.BookmarksService/UpdateBookmark" - // BookmarksServiceDeleteBookmarkProcedure is the fully-qualified name of the BookmarksService's - // DeleteBookmark RPC. - BookmarksServiceDeleteBookmarkProcedure = "/bookmarks.BookmarksService/DeleteBookmark" - // BookmarksServiceGetAffectedBookmarksProcedure is the fully-qualified name of the - // BookmarksService's GetAffectedBookmarks RPC. - BookmarksServiceGetAffectedBookmarksProcedure = "/bookmarks.BookmarksService/GetAffectedBookmarks" -) - -// BookmarksServiceClient is a client for the bookmarks.BookmarksService service. -type BookmarksServiceClient interface { - // ListBookmarks returns all bookmarks of the current user. note that this does not include the actual bookmark data, use GetBookmark for that - ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) - // CreateBookmark creates a new bookmark - CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) - // GetBookmark returns the bookmark with the given UUID. This can also return snapshots as bookmarks and will strip the stored items from the response. - GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) - UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) - DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) - // a helper method to find all affected apps for a given blast radius snapshot - GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) -} - -// NewBookmarksServiceClient constructs a client for the bookmarks.BookmarksService service. By -// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, -// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewBookmarksServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) BookmarksServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - bookmarksServiceMethods := sdp_go.File_bookmarks_proto.Services().ByName("BookmarksService").Methods() - return &bookmarksServiceClient{ - listBookmarks: connect.NewClient[sdp_go.ListBookmarksRequest, sdp_go.ListBookmarkResponse]( - httpClient, - baseURL+BookmarksServiceListBookmarksProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("ListBookmarks")), - connect.WithClientOptions(opts...), - ), - createBookmark: connect.NewClient[sdp_go.CreateBookmarkRequest, sdp_go.CreateBookmarkResponse]( - httpClient, - baseURL+BookmarksServiceCreateBookmarkProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("CreateBookmark")), - connect.WithClientOptions(opts...), - ), - getBookmark: connect.NewClient[sdp_go.GetBookmarkRequest, sdp_go.GetBookmarkResponse]( - httpClient, - baseURL+BookmarksServiceGetBookmarkProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("GetBookmark")), - connect.WithClientOptions(opts...), - ), - updateBookmark: connect.NewClient[sdp_go.UpdateBookmarkRequest, sdp_go.UpdateBookmarkResponse]( - httpClient, - baseURL+BookmarksServiceUpdateBookmarkProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("UpdateBookmark")), - connect.WithClientOptions(opts...), - ), - deleteBookmark: connect.NewClient[sdp_go.DeleteBookmarkRequest, sdp_go.DeleteBookmarkResponse]( - httpClient, - baseURL+BookmarksServiceDeleteBookmarkProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("DeleteBookmark")), - connect.WithClientOptions(opts...), - ), - getAffectedBookmarks: connect.NewClient[sdp_go.GetAffectedBookmarksRequest, sdp_go.GetAffectedBookmarksResponse]( - httpClient, - baseURL+BookmarksServiceGetAffectedBookmarksProcedure, - connect.WithSchema(bookmarksServiceMethods.ByName("GetAffectedBookmarks")), - connect.WithClientOptions(opts...), - ), - } -} - -// bookmarksServiceClient implements BookmarksServiceClient. -type bookmarksServiceClient struct { - listBookmarks *connect.Client[sdp_go.ListBookmarksRequest, sdp_go.ListBookmarkResponse] - createBookmark *connect.Client[sdp_go.CreateBookmarkRequest, sdp_go.CreateBookmarkResponse] - getBookmark *connect.Client[sdp_go.GetBookmarkRequest, sdp_go.GetBookmarkResponse] - updateBookmark *connect.Client[sdp_go.UpdateBookmarkRequest, sdp_go.UpdateBookmarkResponse] - deleteBookmark *connect.Client[sdp_go.DeleteBookmarkRequest, sdp_go.DeleteBookmarkResponse] - getAffectedBookmarks *connect.Client[sdp_go.GetAffectedBookmarksRequest, sdp_go.GetAffectedBookmarksResponse] -} - -// ListBookmarks calls bookmarks.BookmarksService.ListBookmarks. -func (c *bookmarksServiceClient) ListBookmarks(ctx context.Context, req *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) { - return c.listBookmarks.CallUnary(ctx, req) -} - -// CreateBookmark calls bookmarks.BookmarksService.CreateBookmark. -func (c *bookmarksServiceClient) CreateBookmark(ctx context.Context, req *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) { - return c.createBookmark.CallUnary(ctx, req) -} - -// GetBookmark calls bookmarks.BookmarksService.GetBookmark. -func (c *bookmarksServiceClient) GetBookmark(ctx context.Context, req *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) { - return c.getBookmark.CallUnary(ctx, req) -} - -// UpdateBookmark calls bookmarks.BookmarksService.UpdateBookmark. -func (c *bookmarksServiceClient) UpdateBookmark(ctx context.Context, req *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) { - return c.updateBookmark.CallUnary(ctx, req) -} - -// DeleteBookmark calls bookmarks.BookmarksService.DeleteBookmark. -func (c *bookmarksServiceClient) DeleteBookmark(ctx context.Context, req *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) { - return c.deleteBookmark.CallUnary(ctx, req) -} - -// GetAffectedBookmarks calls bookmarks.BookmarksService.GetAffectedBookmarks. -func (c *bookmarksServiceClient) GetAffectedBookmarks(ctx context.Context, req *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) { - return c.getAffectedBookmarks.CallUnary(ctx, req) -} - -// BookmarksServiceHandler is an implementation of the bookmarks.BookmarksService service. -type BookmarksServiceHandler interface { - // ListBookmarks returns all bookmarks of the current user. note that this does not include the actual bookmark data, use GetBookmark for that - ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) - // CreateBookmark creates a new bookmark - CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) - // GetBookmark returns the bookmark with the given UUID. This can also return snapshots as bookmarks and will strip the stored items from the response. - GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) - UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) - DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) - // a helper method to find all affected apps for a given blast radius snapshot - GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) -} - -// NewBookmarksServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewBookmarksServiceHandler(svc BookmarksServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - bookmarksServiceMethods := sdp_go.File_bookmarks_proto.Services().ByName("BookmarksService").Methods() - bookmarksServiceListBookmarksHandler := connect.NewUnaryHandler( - BookmarksServiceListBookmarksProcedure, - svc.ListBookmarks, - connect.WithSchema(bookmarksServiceMethods.ByName("ListBookmarks")), - connect.WithHandlerOptions(opts...), - ) - bookmarksServiceCreateBookmarkHandler := connect.NewUnaryHandler( - BookmarksServiceCreateBookmarkProcedure, - svc.CreateBookmark, - connect.WithSchema(bookmarksServiceMethods.ByName("CreateBookmark")), - connect.WithHandlerOptions(opts...), - ) - bookmarksServiceGetBookmarkHandler := connect.NewUnaryHandler( - BookmarksServiceGetBookmarkProcedure, - svc.GetBookmark, - connect.WithSchema(bookmarksServiceMethods.ByName("GetBookmark")), - connect.WithHandlerOptions(opts...), - ) - bookmarksServiceUpdateBookmarkHandler := connect.NewUnaryHandler( - BookmarksServiceUpdateBookmarkProcedure, - svc.UpdateBookmark, - connect.WithSchema(bookmarksServiceMethods.ByName("UpdateBookmark")), - connect.WithHandlerOptions(opts...), - ) - bookmarksServiceDeleteBookmarkHandler := connect.NewUnaryHandler( - BookmarksServiceDeleteBookmarkProcedure, - svc.DeleteBookmark, - connect.WithSchema(bookmarksServiceMethods.ByName("DeleteBookmark")), - connect.WithHandlerOptions(opts...), - ) - bookmarksServiceGetAffectedBookmarksHandler := connect.NewUnaryHandler( - BookmarksServiceGetAffectedBookmarksProcedure, - svc.GetAffectedBookmarks, - connect.WithSchema(bookmarksServiceMethods.ByName("GetAffectedBookmarks")), - connect.WithHandlerOptions(opts...), - ) - return "/bookmarks.BookmarksService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case BookmarksServiceListBookmarksProcedure: - bookmarksServiceListBookmarksHandler.ServeHTTP(w, r) - case BookmarksServiceCreateBookmarkProcedure: - bookmarksServiceCreateBookmarkHandler.ServeHTTP(w, r) - case BookmarksServiceGetBookmarkProcedure: - bookmarksServiceGetBookmarkHandler.ServeHTTP(w, r) - case BookmarksServiceUpdateBookmarkProcedure: - bookmarksServiceUpdateBookmarkHandler.ServeHTTP(w, r) - case BookmarksServiceDeleteBookmarkProcedure: - bookmarksServiceDeleteBookmarkHandler.ServeHTTP(w, r) - case BookmarksServiceGetAffectedBookmarksProcedure: - bookmarksServiceGetAffectedBookmarksHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedBookmarksServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedBookmarksServiceHandler struct{} - -func (UnimplementedBookmarksServiceHandler) ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.ListBookmarks is not implemented")) -} - -func (UnimplementedBookmarksServiceHandler) CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.CreateBookmark is not implemented")) -} - -func (UnimplementedBookmarksServiceHandler) GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.GetBookmark is not implemented")) -} - -func (UnimplementedBookmarksServiceHandler) UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.UpdateBookmark is not implemented")) -} - -func (UnimplementedBookmarksServiceHandler) DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.DeleteBookmark is not implemented")) -} - -func (UnimplementedBookmarksServiceHandler) GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.GetAffectedBookmarks is not implemented")) -} diff --git a/sdp-go/sdpconnect/changes.connect.go b/sdp-go/sdpconnect/changes.connect.go deleted file mode 100644 index 5b828fa8..00000000 --- a/sdp-go/sdpconnect/changes.connect.go +++ /dev/null @@ -1,1132 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: changes.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // ChangesServiceName is the fully-qualified name of the ChangesService service. - ChangesServiceName = "changes.ChangesService" - // LabelServiceName is the fully-qualified name of the LabelService service. - LabelServiceName = "changes.LabelService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // ChangesServiceListChangesProcedure is the fully-qualified name of the ChangesService's - // ListChanges RPC. - ChangesServiceListChangesProcedure = "/changes.ChangesService/ListChanges" - // ChangesServiceListChangesByStatusProcedure is the fully-qualified name of the ChangesService's - // ListChangesByStatus RPC. - ChangesServiceListChangesByStatusProcedure = "/changes.ChangesService/ListChangesByStatus" - // ChangesServiceCreateChangeProcedure is the fully-qualified name of the ChangesService's - // CreateChange RPC. - ChangesServiceCreateChangeProcedure = "/changes.ChangesService/CreateChange" - // ChangesServiceGetChangeProcedure is the fully-qualified name of the ChangesService's GetChange - // RPC. - ChangesServiceGetChangeProcedure = "/changes.ChangesService/GetChange" - // ChangesServiceGetChangeByTicketLinkProcedure is the fully-qualified name of the ChangesService's - // GetChangeByTicketLink RPC. - ChangesServiceGetChangeByTicketLinkProcedure = "/changes.ChangesService/GetChangeByTicketLink" - // ChangesServiceGetChangeSummaryProcedure is the fully-qualified name of the ChangesService's - // GetChangeSummary RPC. - ChangesServiceGetChangeSummaryProcedure = "/changes.ChangesService/GetChangeSummary" - // ChangesServiceGetChangeTimelineV2Procedure is the fully-qualified name of the ChangesService's - // GetChangeTimelineV2 RPC. - ChangesServiceGetChangeTimelineV2Procedure = "/changes.ChangesService/GetChangeTimelineV2" - // ChangesServiceGetChangeRisksProcedure is the fully-qualified name of the ChangesService's - // GetChangeRisks RPC. - ChangesServiceGetChangeRisksProcedure = "/changes.ChangesService/GetChangeRisks" - // ChangesServiceUpdateChangeProcedure is the fully-qualified name of the ChangesService's - // UpdateChange RPC. - ChangesServiceUpdateChangeProcedure = "/changes.ChangesService/UpdateChange" - // ChangesServiceDeleteChangeProcedure is the fully-qualified name of the ChangesService's - // DeleteChange RPC. - ChangesServiceDeleteChangeProcedure = "/changes.ChangesService/DeleteChange" - // ChangesServiceListChangesBySnapshotUUIDProcedure is the fully-qualified name of the - // ChangesService's ListChangesBySnapshotUUID RPC. - ChangesServiceListChangesBySnapshotUUIDProcedure = "/changes.ChangesService/ListChangesBySnapshotUUID" - // ChangesServiceRefreshStateProcedure is the fully-qualified name of the ChangesService's - // RefreshState RPC. - ChangesServiceRefreshStateProcedure = "/changes.ChangesService/RefreshState" - // ChangesServiceStartChangeProcedure is the fully-qualified name of the ChangesService's - // StartChange RPC. - ChangesServiceStartChangeProcedure = "/changes.ChangesService/StartChange" - // ChangesServiceEndChangeProcedure is the fully-qualified name of the ChangesService's EndChange - // RPC. - ChangesServiceEndChangeProcedure = "/changes.ChangesService/EndChange" - // ChangesServiceStartChangeSimpleProcedure is the fully-qualified name of the ChangesService's - // StartChangeSimple RPC. - ChangesServiceStartChangeSimpleProcedure = "/changes.ChangesService/StartChangeSimple" - // ChangesServiceEndChangeSimpleProcedure is the fully-qualified name of the ChangesService's - // EndChangeSimple RPC. - ChangesServiceEndChangeSimpleProcedure = "/changes.ChangesService/EndChangeSimple" - // ChangesServiceListHomeChangesProcedure is the fully-qualified name of the ChangesService's - // ListHomeChanges RPC. - ChangesServiceListHomeChangesProcedure = "/changes.ChangesService/ListHomeChanges" - // ChangesServiceStartChangeAnalysisProcedure is the fully-qualified name of the ChangesService's - // StartChangeAnalysis RPC. - ChangesServiceStartChangeAnalysisProcedure = "/changes.ChangesService/StartChangeAnalysis" - // ChangesServiceListChangingItemsSummaryProcedure is the fully-qualified name of the - // ChangesService's ListChangingItemsSummary RPC. - ChangesServiceListChangingItemsSummaryProcedure = "/changes.ChangesService/ListChangingItemsSummary" - // ChangesServiceGetDiffProcedure is the fully-qualified name of the ChangesService's GetDiff RPC. - ChangesServiceGetDiffProcedure = "/changes.ChangesService/GetDiff" - // ChangesServicePopulateChangeFiltersProcedure is the fully-qualified name of the ChangesService's - // PopulateChangeFilters RPC. - ChangesServicePopulateChangeFiltersProcedure = "/changes.ChangesService/PopulateChangeFilters" - // ChangesServiceGenerateRiskFixProcedure is the fully-qualified name of the ChangesService's - // GenerateRiskFix RPC. - ChangesServiceGenerateRiskFixProcedure = "/changes.ChangesService/GenerateRiskFix" - // ChangesServiceGetHypothesesDetailsProcedure is the fully-qualified name of the ChangesService's - // GetHypothesesDetails RPC. - ChangesServiceGetHypothesesDetailsProcedure = "/changes.ChangesService/GetHypothesesDetails" - // ChangesServiceGetChangeSignalsProcedure is the fully-qualified name of the ChangesService's - // GetChangeSignals RPC. - ChangesServiceGetChangeSignalsProcedure = "/changes.ChangesService/GetChangeSignals" - // LabelServiceListLabelRulesProcedure is the fully-qualified name of the LabelService's - // ListLabelRules RPC. - LabelServiceListLabelRulesProcedure = "/changes.LabelService/ListLabelRules" - // LabelServiceCreateLabelRuleProcedure is the fully-qualified name of the LabelService's - // CreateLabelRule RPC. - LabelServiceCreateLabelRuleProcedure = "/changes.LabelService/CreateLabelRule" - // LabelServiceGetLabelRuleProcedure is the fully-qualified name of the LabelService's GetLabelRule - // RPC. - LabelServiceGetLabelRuleProcedure = "/changes.LabelService/GetLabelRule" - // LabelServiceUpdateLabelRuleProcedure is the fully-qualified name of the LabelService's - // UpdateLabelRule RPC. - LabelServiceUpdateLabelRuleProcedure = "/changes.LabelService/UpdateLabelRule" - // LabelServiceDeleteLabelRuleProcedure is the fully-qualified name of the LabelService's - // DeleteLabelRule RPC. - LabelServiceDeleteLabelRuleProcedure = "/changes.LabelService/DeleteLabelRule" - // LabelServiceTestLabelRuleProcedure is the fully-qualified name of the LabelService's - // TestLabelRule RPC. - LabelServiceTestLabelRuleProcedure = "/changes.LabelService/TestLabelRule" - // LabelServiceReapplyLabelRuleInTimeRangeProcedure is the fully-qualified name of the - // LabelService's ReapplyLabelRuleInTimeRange RPC. - LabelServiceReapplyLabelRuleInTimeRangeProcedure = "/changes.LabelService/ReapplyLabelRuleInTimeRange" -) - -// ChangesServiceClient is a client for the changes.ChangesService service. -type ChangesServiceClient interface { - // Lists all changes - ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) - // list all changes in a specific status - ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) - // Creates a new change - CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) - // Gets the details of an existing change - GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) - // Get a change by the ticket link - GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) - // Gets the details of an existing change in markdown format - GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) - // Gets the full timeline for this change, this will send one response - // immediately and then hold the connection open, and send the entire - // timeline again if there are any changes - GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) - // This is used on the blast radius page to get the risks and status for a change. - GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) - // Updates an existing change - UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) - // Deletes a change - DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) - // Lists all changes for a snapshot UUID - ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) - // Ask the gateway to refresh all internal caches and status slots - // The RPC will return immediately doing all processing in the background - RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) - // Executing this RPC take a snapshot of the current blast radius and store it - // in `systemBeforeSnapshotUUID` and then advance the status to - // `STATUS_HAPPENING`. It can only be called once per change. - StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.ServerStreamForClient[sdp_go.StartChangeResponse], error) - // Takes the "after" snapshot, stores it in `systemAfterSnapshotUUID`, calculates - // the change diff and stores it as a list of DiffedItems and - // advances the change status to `STATUS_DONE` - EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.ServerStreamForClient[sdp_go.EndChangeResponse], error) - // Simple version of StartChange that returns immediately after enqueuing the job. - // Use this instead of StartChange for non-streaming clients. - StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) - // Simple version of EndChange that returns immediately after enqueuing the job. - // Use this instead of EndChange for non-streaming clients. - EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) - // Lists all changes, designed for use in the changes home page - ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) - // Start the change analysis process. This will calculate various things - // blast radius, risks etc. This will return immediately and - // the results can be fetched using the other RPCs - StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) - // Gets the diff summary for all items that were planned to change as part of - // this change. This includes the high level details of the item, and the - // status (e.g. changed, deleted) but not the diff itself - ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) - // Gets the full diff of everything that changed as part of this "change". - // This includes all items and also edges between them - GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) - // List all the available repos, authors and statuses that can be used to populate the dropdown filters - PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) - // Generates an AI-powered fix suggestion for a specific risk - GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) - // The full details of all of the hypotheses that were considered or are being - // considered as part of this change. - GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) - // Gets all signals for a change, including: - // - Overall signal for the change - // - Top level signals for each category - // - Routineness signals per item - // - Individual custom signals - // This is similar to GetChangeSummary but focused on signals data - GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) -} - -// NewChangesServiceClient constructs a client for the changes.ChangesService service. By default, -// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and -// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() -// or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewChangesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ChangesServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - changesServiceMethods := sdp_go.File_changes_proto.Services().ByName("ChangesService").Methods() - return &changesServiceClient{ - listChanges: connect.NewClient[sdp_go.ListChangesRequest, sdp_go.ListChangesResponse]( - httpClient, - baseURL+ChangesServiceListChangesProcedure, - connect.WithSchema(changesServiceMethods.ByName("ListChanges")), - connect.WithClientOptions(opts...), - ), - listChangesByStatus: connect.NewClient[sdp_go.ListChangesByStatusRequest, sdp_go.ListChangesByStatusResponse]( - httpClient, - baseURL+ChangesServiceListChangesByStatusProcedure, - connect.WithSchema(changesServiceMethods.ByName("ListChangesByStatus")), - connect.WithClientOptions(opts...), - ), - createChange: connect.NewClient[sdp_go.CreateChangeRequest, sdp_go.CreateChangeResponse]( - httpClient, - baseURL+ChangesServiceCreateChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("CreateChange")), - connect.WithClientOptions(opts...), - ), - getChange: connect.NewClient[sdp_go.GetChangeRequest, sdp_go.GetChangeResponse]( - httpClient, - baseURL+ChangesServiceGetChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetChange")), - connect.WithClientOptions(opts...), - ), - getChangeByTicketLink: connect.NewClient[sdp_go.GetChangeByTicketLinkRequest, sdp_go.GetChangeResponse]( - httpClient, - baseURL+ChangesServiceGetChangeByTicketLinkProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetChangeByTicketLink")), - connect.WithClientOptions(opts...), - ), - getChangeSummary: connect.NewClient[sdp_go.GetChangeSummaryRequest, sdp_go.GetChangeSummaryResponse]( - httpClient, - baseURL+ChangesServiceGetChangeSummaryProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetChangeSummary")), - connect.WithClientOptions(opts...), - ), - getChangeTimelineV2: connect.NewClient[sdp_go.GetChangeTimelineV2Request, sdp_go.GetChangeTimelineV2Response]( - httpClient, - baseURL+ChangesServiceGetChangeTimelineV2Procedure, - connect.WithSchema(changesServiceMethods.ByName("GetChangeTimelineV2")), - connect.WithClientOptions(opts...), - ), - getChangeRisks: connect.NewClient[sdp_go.GetChangeRisksRequest, sdp_go.GetChangeRisksResponse]( - httpClient, - baseURL+ChangesServiceGetChangeRisksProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetChangeRisks")), - connect.WithClientOptions(opts...), - ), - updateChange: connect.NewClient[sdp_go.UpdateChangeRequest, sdp_go.UpdateChangeResponse]( - httpClient, - baseURL+ChangesServiceUpdateChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("UpdateChange")), - connect.WithClientOptions(opts...), - ), - deleteChange: connect.NewClient[sdp_go.DeleteChangeRequest, sdp_go.DeleteChangeResponse]( - httpClient, - baseURL+ChangesServiceDeleteChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("DeleteChange")), - connect.WithClientOptions(opts...), - ), - listChangesBySnapshotUUID: connect.NewClient[sdp_go.ListChangesBySnapshotUUIDRequest, sdp_go.ListChangesBySnapshotUUIDResponse]( - httpClient, - baseURL+ChangesServiceListChangesBySnapshotUUIDProcedure, - connect.WithSchema(changesServiceMethods.ByName("ListChangesBySnapshotUUID")), - connect.WithClientOptions(opts...), - ), - refreshState: connect.NewClient[sdp_go.RefreshStateRequest, sdp_go.RefreshStateResponse]( - httpClient, - baseURL+ChangesServiceRefreshStateProcedure, - connect.WithSchema(changesServiceMethods.ByName("RefreshState")), - connect.WithClientOptions(opts...), - ), - startChange: connect.NewClient[sdp_go.StartChangeRequest, sdp_go.StartChangeResponse]( - httpClient, - baseURL+ChangesServiceStartChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("StartChange")), - connect.WithClientOptions(opts...), - ), - endChange: connect.NewClient[sdp_go.EndChangeRequest, sdp_go.EndChangeResponse]( - httpClient, - baseURL+ChangesServiceEndChangeProcedure, - connect.WithSchema(changesServiceMethods.ByName("EndChange")), - connect.WithClientOptions(opts...), - ), - startChangeSimple: connect.NewClient[sdp_go.StartChangeRequest, sdp_go.StartChangeSimpleResponse]( - httpClient, - baseURL+ChangesServiceStartChangeSimpleProcedure, - connect.WithSchema(changesServiceMethods.ByName("StartChangeSimple")), - connect.WithClientOptions(opts...), - ), - endChangeSimple: connect.NewClient[sdp_go.EndChangeRequest, sdp_go.EndChangeSimpleResponse]( - httpClient, - baseURL+ChangesServiceEndChangeSimpleProcedure, - connect.WithSchema(changesServiceMethods.ByName("EndChangeSimple")), - connect.WithClientOptions(opts...), - ), - listHomeChanges: connect.NewClient[sdp_go.ListHomeChangesRequest, sdp_go.ListHomeChangesResponse]( - httpClient, - baseURL+ChangesServiceListHomeChangesProcedure, - connect.WithSchema(changesServiceMethods.ByName("ListHomeChanges")), - connect.WithClientOptions(opts...), - ), - startChangeAnalysis: connect.NewClient[sdp_go.StartChangeAnalysisRequest, sdp_go.StartChangeAnalysisResponse]( - httpClient, - baseURL+ChangesServiceStartChangeAnalysisProcedure, - connect.WithSchema(changesServiceMethods.ByName("StartChangeAnalysis")), - connect.WithClientOptions(opts...), - ), - listChangingItemsSummary: connect.NewClient[sdp_go.ListChangingItemsSummaryRequest, sdp_go.ListChangingItemsSummaryResponse]( - httpClient, - baseURL+ChangesServiceListChangingItemsSummaryProcedure, - connect.WithSchema(changesServiceMethods.ByName("ListChangingItemsSummary")), - connect.WithClientOptions(opts...), - ), - getDiff: connect.NewClient[sdp_go.GetDiffRequest, sdp_go.GetDiffResponse]( - httpClient, - baseURL+ChangesServiceGetDiffProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetDiff")), - connect.WithClientOptions(opts...), - ), - populateChangeFilters: connect.NewClient[sdp_go.PopulateChangeFiltersRequest, sdp_go.PopulateChangeFiltersResponse]( - httpClient, - baseURL+ChangesServicePopulateChangeFiltersProcedure, - connect.WithSchema(changesServiceMethods.ByName("PopulateChangeFilters")), - connect.WithClientOptions(opts...), - ), - generateRiskFix: connect.NewClient[sdp_go.GenerateRiskFixRequest, sdp_go.GenerateRiskFixResponse]( - httpClient, - baseURL+ChangesServiceGenerateRiskFixProcedure, - connect.WithSchema(changesServiceMethods.ByName("GenerateRiskFix")), - connect.WithClientOptions(opts...), - ), - getHypothesesDetails: connect.NewClient[sdp_go.GetHypothesesDetailsRequest, sdp_go.GetHypothesesDetailsResponse]( - httpClient, - baseURL+ChangesServiceGetHypothesesDetailsProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetHypothesesDetails")), - connect.WithClientOptions(opts...), - ), - getChangeSignals: connect.NewClient[sdp_go.GetChangeSignalsRequest, sdp_go.GetChangeSignalsResponse]( - httpClient, - baseURL+ChangesServiceGetChangeSignalsProcedure, - connect.WithSchema(changesServiceMethods.ByName("GetChangeSignals")), - connect.WithClientOptions(opts...), - ), - } -} - -// changesServiceClient implements ChangesServiceClient. -type changesServiceClient struct { - listChanges *connect.Client[sdp_go.ListChangesRequest, sdp_go.ListChangesResponse] - listChangesByStatus *connect.Client[sdp_go.ListChangesByStatusRequest, sdp_go.ListChangesByStatusResponse] - createChange *connect.Client[sdp_go.CreateChangeRequest, sdp_go.CreateChangeResponse] - getChange *connect.Client[sdp_go.GetChangeRequest, sdp_go.GetChangeResponse] - getChangeByTicketLink *connect.Client[sdp_go.GetChangeByTicketLinkRequest, sdp_go.GetChangeResponse] - getChangeSummary *connect.Client[sdp_go.GetChangeSummaryRequest, sdp_go.GetChangeSummaryResponse] - getChangeTimelineV2 *connect.Client[sdp_go.GetChangeTimelineV2Request, sdp_go.GetChangeTimelineV2Response] - getChangeRisks *connect.Client[sdp_go.GetChangeRisksRequest, sdp_go.GetChangeRisksResponse] - updateChange *connect.Client[sdp_go.UpdateChangeRequest, sdp_go.UpdateChangeResponse] - deleteChange *connect.Client[sdp_go.DeleteChangeRequest, sdp_go.DeleteChangeResponse] - listChangesBySnapshotUUID *connect.Client[sdp_go.ListChangesBySnapshotUUIDRequest, sdp_go.ListChangesBySnapshotUUIDResponse] - refreshState *connect.Client[sdp_go.RefreshStateRequest, sdp_go.RefreshStateResponse] - startChange *connect.Client[sdp_go.StartChangeRequest, sdp_go.StartChangeResponse] - endChange *connect.Client[sdp_go.EndChangeRequest, sdp_go.EndChangeResponse] - startChangeSimple *connect.Client[sdp_go.StartChangeRequest, sdp_go.StartChangeSimpleResponse] - endChangeSimple *connect.Client[sdp_go.EndChangeRequest, sdp_go.EndChangeSimpleResponse] - listHomeChanges *connect.Client[sdp_go.ListHomeChangesRequest, sdp_go.ListHomeChangesResponse] - startChangeAnalysis *connect.Client[sdp_go.StartChangeAnalysisRequest, sdp_go.StartChangeAnalysisResponse] - listChangingItemsSummary *connect.Client[sdp_go.ListChangingItemsSummaryRequest, sdp_go.ListChangingItemsSummaryResponse] - getDiff *connect.Client[sdp_go.GetDiffRequest, sdp_go.GetDiffResponse] - populateChangeFilters *connect.Client[sdp_go.PopulateChangeFiltersRequest, sdp_go.PopulateChangeFiltersResponse] - generateRiskFix *connect.Client[sdp_go.GenerateRiskFixRequest, sdp_go.GenerateRiskFixResponse] - getHypothesesDetails *connect.Client[sdp_go.GetHypothesesDetailsRequest, sdp_go.GetHypothesesDetailsResponse] - getChangeSignals *connect.Client[sdp_go.GetChangeSignalsRequest, sdp_go.GetChangeSignalsResponse] -} - -// ListChanges calls changes.ChangesService.ListChanges. -func (c *changesServiceClient) ListChanges(ctx context.Context, req *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) { - return c.listChanges.CallUnary(ctx, req) -} - -// ListChangesByStatus calls changes.ChangesService.ListChangesByStatus. -func (c *changesServiceClient) ListChangesByStatus(ctx context.Context, req *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) { - return c.listChangesByStatus.CallUnary(ctx, req) -} - -// CreateChange calls changes.ChangesService.CreateChange. -func (c *changesServiceClient) CreateChange(ctx context.Context, req *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) { - return c.createChange.CallUnary(ctx, req) -} - -// GetChange calls changes.ChangesService.GetChange. -func (c *changesServiceClient) GetChange(ctx context.Context, req *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { - return c.getChange.CallUnary(ctx, req) -} - -// GetChangeByTicketLink calls changes.ChangesService.GetChangeByTicketLink. -func (c *changesServiceClient) GetChangeByTicketLink(ctx context.Context, req *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { - return c.getChangeByTicketLink.CallUnary(ctx, req) -} - -// GetChangeSummary calls changes.ChangesService.GetChangeSummary. -func (c *changesServiceClient) GetChangeSummary(ctx context.Context, req *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) { - return c.getChangeSummary.CallUnary(ctx, req) -} - -// GetChangeTimelineV2 calls changes.ChangesService.GetChangeTimelineV2. -func (c *changesServiceClient) GetChangeTimelineV2(ctx context.Context, req *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) { - return c.getChangeTimelineV2.CallUnary(ctx, req) -} - -// GetChangeRisks calls changes.ChangesService.GetChangeRisks. -func (c *changesServiceClient) GetChangeRisks(ctx context.Context, req *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) { - return c.getChangeRisks.CallUnary(ctx, req) -} - -// UpdateChange calls changes.ChangesService.UpdateChange. -func (c *changesServiceClient) UpdateChange(ctx context.Context, req *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) { - return c.updateChange.CallUnary(ctx, req) -} - -// DeleteChange calls changes.ChangesService.DeleteChange. -func (c *changesServiceClient) DeleteChange(ctx context.Context, req *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) { - return c.deleteChange.CallUnary(ctx, req) -} - -// ListChangesBySnapshotUUID calls changes.ChangesService.ListChangesBySnapshotUUID. -func (c *changesServiceClient) ListChangesBySnapshotUUID(ctx context.Context, req *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) { - return c.listChangesBySnapshotUUID.CallUnary(ctx, req) -} - -// RefreshState calls changes.ChangesService.RefreshState. -func (c *changesServiceClient) RefreshState(ctx context.Context, req *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) { - return c.refreshState.CallUnary(ctx, req) -} - -// StartChange calls changes.ChangesService.StartChange. -func (c *changesServiceClient) StartChange(ctx context.Context, req *connect.Request[sdp_go.StartChangeRequest]) (*connect.ServerStreamForClient[sdp_go.StartChangeResponse], error) { - return c.startChange.CallServerStream(ctx, req) -} - -// EndChange calls changes.ChangesService.EndChange. -func (c *changesServiceClient) EndChange(ctx context.Context, req *connect.Request[sdp_go.EndChangeRequest]) (*connect.ServerStreamForClient[sdp_go.EndChangeResponse], error) { - return c.endChange.CallServerStream(ctx, req) -} - -// StartChangeSimple calls changes.ChangesService.StartChangeSimple. -func (c *changesServiceClient) StartChangeSimple(ctx context.Context, req *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) { - return c.startChangeSimple.CallUnary(ctx, req) -} - -// EndChangeSimple calls changes.ChangesService.EndChangeSimple. -func (c *changesServiceClient) EndChangeSimple(ctx context.Context, req *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) { - return c.endChangeSimple.CallUnary(ctx, req) -} - -// ListHomeChanges calls changes.ChangesService.ListHomeChanges. -func (c *changesServiceClient) ListHomeChanges(ctx context.Context, req *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) { - return c.listHomeChanges.CallUnary(ctx, req) -} - -// StartChangeAnalysis calls changes.ChangesService.StartChangeAnalysis. -func (c *changesServiceClient) StartChangeAnalysis(ctx context.Context, req *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) { - return c.startChangeAnalysis.CallUnary(ctx, req) -} - -// ListChangingItemsSummary calls changes.ChangesService.ListChangingItemsSummary. -func (c *changesServiceClient) ListChangingItemsSummary(ctx context.Context, req *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) { - return c.listChangingItemsSummary.CallUnary(ctx, req) -} - -// GetDiff calls changes.ChangesService.GetDiff. -func (c *changesServiceClient) GetDiff(ctx context.Context, req *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) { - return c.getDiff.CallUnary(ctx, req) -} - -// PopulateChangeFilters calls changes.ChangesService.PopulateChangeFilters. -func (c *changesServiceClient) PopulateChangeFilters(ctx context.Context, req *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) { - return c.populateChangeFilters.CallUnary(ctx, req) -} - -// GenerateRiskFix calls changes.ChangesService.GenerateRiskFix. -func (c *changesServiceClient) GenerateRiskFix(ctx context.Context, req *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) { - return c.generateRiskFix.CallUnary(ctx, req) -} - -// GetHypothesesDetails calls changes.ChangesService.GetHypothesesDetails. -func (c *changesServiceClient) GetHypothesesDetails(ctx context.Context, req *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) { - return c.getHypothesesDetails.CallUnary(ctx, req) -} - -// GetChangeSignals calls changes.ChangesService.GetChangeSignals. -func (c *changesServiceClient) GetChangeSignals(ctx context.Context, req *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) { - return c.getChangeSignals.CallUnary(ctx, req) -} - -// ChangesServiceHandler is an implementation of the changes.ChangesService service. -type ChangesServiceHandler interface { - // Lists all changes - ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) - // list all changes in a specific status - ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) - // Creates a new change - CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) - // Gets the details of an existing change - GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) - // Get a change by the ticket link - GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) - // Gets the details of an existing change in markdown format - GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) - // Gets the full timeline for this change, this will send one response - // immediately and then hold the connection open, and send the entire - // timeline again if there are any changes - GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) - // This is used on the blast radius page to get the risks and status for a change. - GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) - // Updates an existing change - UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) - // Deletes a change - DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) - // Lists all changes for a snapshot UUID - ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) - // Ask the gateway to refresh all internal caches and status slots - // The RPC will return immediately doing all processing in the background - RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) - // Executing this RPC take a snapshot of the current blast radius and store it - // in `systemBeforeSnapshotUUID` and then advance the status to - // `STATUS_HAPPENING`. It can only be called once per change. - StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest], *connect.ServerStream[sdp_go.StartChangeResponse]) error - // Takes the "after" snapshot, stores it in `systemAfterSnapshotUUID`, calculates - // the change diff and stores it as a list of DiffedItems and - // advances the change status to `STATUS_DONE` - EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest], *connect.ServerStream[sdp_go.EndChangeResponse]) error - // Simple version of StartChange that returns immediately after enqueuing the job. - // Use this instead of StartChange for non-streaming clients. - StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) - // Simple version of EndChange that returns immediately after enqueuing the job. - // Use this instead of EndChange for non-streaming clients. - EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) - // Lists all changes, designed for use in the changes home page - ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) - // Start the change analysis process. This will calculate various things - // blast radius, risks etc. This will return immediately and - // the results can be fetched using the other RPCs - StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) - // Gets the diff summary for all items that were planned to change as part of - // this change. This includes the high level details of the item, and the - // status (e.g. changed, deleted) but not the diff itself - ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) - // Gets the full diff of everything that changed as part of this "change". - // This includes all items and also edges between them - GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) - // List all the available repos, authors and statuses that can be used to populate the dropdown filters - PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) - // Generates an AI-powered fix suggestion for a specific risk - GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) - // The full details of all of the hypotheses that were considered or are being - // considered as part of this change. - GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) - // Gets all signals for a change, including: - // - Overall signal for the change - // - Top level signals for each category - // - Routineness signals per item - // - Individual custom signals - // This is similar to GetChangeSummary but focused on signals data - GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) -} - -// NewChangesServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewChangesServiceHandler(svc ChangesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - changesServiceMethods := sdp_go.File_changes_proto.Services().ByName("ChangesService").Methods() - changesServiceListChangesHandler := connect.NewUnaryHandler( - ChangesServiceListChangesProcedure, - svc.ListChanges, - connect.WithSchema(changesServiceMethods.ByName("ListChanges")), - connect.WithHandlerOptions(opts...), - ) - changesServiceListChangesByStatusHandler := connect.NewUnaryHandler( - ChangesServiceListChangesByStatusProcedure, - svc.ListChangesByStatus, - connect.WithSchema(changesServiceMethods.ByName("ListChangesByStatus")), - connect.WithHandlerOptions(opts...), - ) - changesServiceCreateChangeHandler := connect.NewUnaryHandler( - ChangesServiceCreateChangeProcedure, - svc.CreateChange, - connect.WithSchema(changesServiceMethods.ByName("CreateChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeHandler := connect.NewUnaryHandler( - ChangesServiceGetChangeProcedure, - svc.GetChange, - connect.WithSchema(changesServiceMethods.ByName("GetChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeByTicketLinkHandler := connect.NewUnaryHandler( - ChangesServiceGetChangeByTicketLinkProcedure, - svc.GetChangeByTicketLink, - connect.WithSchema(changesServiceMethods.ByName("GetChangeByTicketLink")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeSummaryHandler := connect.NewUnaryHandler( - ChangesServiceGetChangeSummaryProcedure, - svc.GetChangeSummary, - connect.WithSchema(changesServiceMethods.ByName("GetChangeSummary")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeTimelineV2Handler := connect.NewUnaryHandler( - ChangesServiceGetChangeTimelineV2Procedure, - svc.GetChangeTimelineV2, - connect.WithSchema(changesServiceMethods.ByName("GetChangeTimelineV2")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeRisksHandler := connect.NewUnaryHandler( - ChangesServiceGetChangeRisksProcedure, - svc.GetChangeRisks, - connect.WithSchema(changesServiceMethods.ByName("GetChangeRisks")), - connect.WithHandlerOptions(opts...), - ) - changesServiceUpdateChangeHandler := connect.NewUnaryHandler( - ChangesServiceUpdateChangeProcedure, - svc.UpdateChange, - connect.WithSchema(changesServiceMethods.ByName("UpdateChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceDeleteChangeHandler := connect.NewUnaryHandler( - ChangesServiceDeleteChangeProcedure, - svc.DeleteChange, - connect.WithSchema(changesServiceMethods.ByName("DeleteChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceListChangesBySnapshotUUIDHandler := connect.NewUnaryHandler( - ChangesServiceListChangesBySnapshotUUIDProcedure, - svc.ListChangesBySnapshotUUID, - connect.WithSchema(changesServiceMethods.ByName("ListChangesBySnapshotUUID")), - connect.WithHandlerOptions(opts...), - ) - changesServiceRefreshStateHandler := connect.NewUnaryHandler( - ChangesServiceRefreshStateProcedure, - svc.RefreshState, - connect.WithSchema(changesServiceMethods.ByName("RefreshState")), - connect.WithHandlerOptions(opts...), - ) - changesServiceStartChangeHandler := connect.NewServerStreamHandler( - ChangesServiceStartChangeProcedure, - svc.StartChange, - connect.WithSchema(changesServiceMethods.ByName("StartChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceEndChangeHandler := connect.NewServerStreamHandler( - ChangesServiceEndChangeProcedure, - svc.EndChange, - connect.WithSchema(changesServiceMethods.ByName("EndChange")), - connect.WithHandlerOptions(opts...), - ) - changesServiceStartChangeSimpleHandler := connect.NewUnaryHandler( - ChangesServiceStartChangeSimpleProcedure, - svc.StartChangeSimple, - connect.WithSchema(changesServiceMethods.ByName("StartChangeSimple")), - connect.WithHandlerOptions(opts...), - ) - changesServiceEndChangeSimpleHandler := connect.NewUnaryHandler( - ChangesServiceEndChangeSimpleProcedure, - svc.EndChangeSimple, - connect.WithSchema(changesServiceMethods.ByName("EndChangeSimple")), - connect.WithHandlerOptions(opts...), - ) - changesServiceListHomeChangesHandler := connect.NewUnaryHandler( - ChangesServiceListHomeChangesProcedure, - svc.ListHomeChanges, - connect.WithSchema(changesServiceMethods.ByName("ListHomeChanges")), - connect.WithHandlerOptions(opts...), - ) - changesServiceStartChangeAnalysisHandler := connect.NewUnaryHandler( - ChangesServiceStartChangeAnalysisProcedure, - svc.StartChangeAnalysis, - connect.WithSchema(changesServiceMethods.ByName("StartChangeAnalysis")), - connect.WithHandlerOptions(opts...), - ) - changesServiceListChangingItemsSummaryHandler := connect.NewUnaryHandler( - ChangesServiceListChangingItemsSummaryProcedure, - svc.ListChangingItemsSummary, - connect.WithSchema(changesServiceMethods.ByName("ListChangingItemsSummary")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetDiffHandler := connect.NewUnaryHandler( - ChangesServiceGetDiffProcedure, - svc.GetDiff, - connect.WithSchema(changesServiceMethods.ByName("GetDiff")), - connect.WithHandlerOptions(opts...), - ) - changesServicePopulateChangeFiltersHandler := connect.NewUnaryHandler( - ChangesServicePopulateChangeFiltersProcedure, - svc.PopulateChangeFilters, - connect.WithSchema(changesServiceMethods.ByName("PopulateChangeFilters")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGenerateRiskFixHandler := connect.NewUnaryHandler( - ChangesServiceGenerateRiskFixProcedure, - svc.GenerateRiskFix, - connect.WithSchema(changesServiceMethods.ByName("GenerateRiskFix")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetHypothesesDetailsHandler := connect.NewUnaryHandler( - ChangesServiceGetHypothesesDetailsProcedure, - svc.GetHypothesesDetails, - connect.WithSchema(changesServiceMethods.ByName("GetHypothesesDetails")), - connect.WithHandlerOptions(opts...), - ) - changesServiceGetChangeSignalsHandler := connect.NewUnaryHandler( - ChangesServiceGetChangeSignalsProcedure, - svc.GetChangeSignals, - connect.WithSchema(changesServiceMethods.ByName("GetChangeSignals")), - connect.WithHandlerOptions(opts...), - ) - return "/changes.ChangesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case ChangesServiceListChangesProcedure: - changesServiceListChangesHandler.ServeHTTP(w, r) - case ChangesServiceListChangesByStatusProcedure: - changesServiceListChangesByStatusHandler.ServeHTTP(w, r) - case ChangesServiceCreateChangeProcedure: - changesServiceCreateChangeHandler.ServeHTTP(w, r) - case ChangesServiceGetChangeProcedure: - changesServiceGetChangeHandler.ServeHTTP(w, r) - case ChangesServiceGetChangeByTicketLinkProcedure: - changesServiceGetChangeByTicketLinkHandler.ServeHTTP(w, r) - case ChangesServiceGetChangeSummaryProcedure: - changesServiceGetChangeSummaryHandler.ServeHTTP(w, r) - case ChangesServiceGetChangeTimelineV2Procedure: - changesServiceGetChangeTimelineV2Handler.ServeHTTP(w, r) - case ChangesServiceGetChangeRisksProcedure: - changesServiceGetChangeRisksHandler.ServeHTTP(w, r) - case ChangesServiceUpdateChangeProcedure: - changesServiceUpdateChangeHandler.ServeHTTP(w, r) - case ChangesServiceDeleteChangeProcedure: - changesServiceDeleteChangeHandler.ServeHTTP(w, r) - case ChangesServiceListChangesBySnapshotUUIDProcedure: - changesServiceListChangesBySnapshotUUIDHandler.ServeHTTP(w, r) - case ChangesServiceRefreshStateProcedure: - changesServiceRefreshStateHandler.ServeHTTP(w, r) - case ChangesServiceStartChangeProcedure: - changesServiceStartChangeHandler.ServeHTTP(w, r) - case ChangesServiceEndChangeProcedure: - changesServiceEndChangeHandler.ServeHTTP(w, r) - case ChangesServiceStartChangeSimpleProcedure: - changesServiceStartChangeSimpleHandler.ServeHTTP(w, r) - case ChangesServiceEndChangeSimpleProcedure: - changesServiceEndChangeSimpleHandler.ServeHTTP(w, r) - case ChangesServiceListHomeChangesProcedure: - changesServiceListHomeChangesHandler.ServeHTTP(w, r) - case ChangesServiceStartChangeAnalysisProcedure: - changesServiceStartChangeAnalysisHandler.ServeHTTP(w, r) - case ChangesServiceListChangingItemsSummaryProcedure: - changesServiceListChangingItemsSummaryHandler.ServeHTTP(w, r) - case ChangesServiceGetDiffProcedure: - changesServiceGetDiffHandler.ServeHTTP(w, r) - case ChangesServicePopulateChangeFiltersProcedure: - changesServicePopulateChangeFiltersHandler.ServeHTTP(w, r) - case ChangesServiceGenerateRiskFixProcedure: - changesServiceGenerateRiskFixHandler.ServeHTTP(w, r) - case ChangesServiceGetHypothesesDetailsProcedure: - changesServiceGetHypothesesDetailsHandler.ServeHTTP(w, r) - case ChangesServiceGetChangeSignalsProcedure: - changesServiceGetChangeSignalsHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedChangesServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedChangesServiceHandler struct{} - -func (UnimplementedChangesServiceHandler) ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChanges is not implemented")) -} - -func (UnimplementedChangesServiceHandler) ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangesByStatus is not implemented")) -} - -func (UnimplementedChangesServiceHandler) CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.CreateChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeByTicketLink is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeSummary is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeTimelineV2 is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeRisks is not implemented")) -} - -func (UnimplementedChangesServiceHandler) UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.UpdateChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.DeleteChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangesBySnapshotUUID is not implemented")) -} - -func (UnimplementedChangesServiceHandler) RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.RefreshState is not implemented")) -} - -func (UnimplementedChangesServiceHandler) StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest], *connect.ServerStream[sdp_go.StartChangeResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest], *connect.ServerStream[sdp_go.EndChangeResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.EndChange is not implemented")) -} - -func (UnimplementedChangesServiceHandler) StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChangeSimple is not implemented")) -} - -func (UnimplementedChangesServiceHandler) EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.EndChangeSimple is not implemented")) -} - -func (UnimplementedChangesServiceHandler) ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListHomeChanges is not implemented")) -} - -func (UnimplementedChangesServiceHandler) StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChangeAnalysis is not implemented")) -} - -func (UnimplementedChangesServiceHandler) ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangingItemsSummary is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetDiff is not implemented")) -} - -func (UnimplementedChangesServiceHandler) PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.PopulateChangeFilters is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GenerateRiskFix is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetHypothesesDetails is not implemented")) -} - -func (UnimplementedChangesServiceHandler) GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeSignals is not implemented")) -} - -// LabelServiceClient is a client for the changes.LabelService service. -type LabelServiceClient interface { - // Lists all label rules for an account - ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) - // Creates a new label rule - CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) - // Gets the details of a label rule - GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) - // Updates a label rule - UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) - // Deletes a label rule - // this also removes the label from all changes that are currently labelled with this rule - DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) - // Test a label rule against a list of changes, this does not apply the label to the changes, it only tests if the label should be applied - TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest]) (*connect.ServerStreamForClient[sdp_go.TestLabelRuleResponse], error) - // Re-apply a label rule across all changes within a specified time window: - // 1. Removes the label from all relevant changes in the period that match this rule - // 2. Applies (or re-applies) the label to eligible changes in the period - ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) -} - -// NewLabelServiceClient constructs a client for the changes.LabelService service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewLabelServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) LabelServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - labelServiceMethods := sdp_go.File_changes_proto.Services().ByName("LabelService").Methods() - return &labelServiceClient{ - listLabelRules: connect.NewClient[sdp_go.ListLabelRulesRequest, sdp_go.ListLabelRulesResponse]( - httpClient, - baseURL+LabelServiceListLabelRulesProcedure, - connect.WithSchema(labelServiceMethods.ByName("ListLabelRules")), - connect.WithClientOptions(opts...), - ), - createLabelRule: connect.NewClient[sdp_go.CreateLabelRuleRequest, sdp_go.CreateLabelRuleResponse]( - httpClient, - baseURL+LabelServiceCreateLabelRuleProcedure, - connect.WithSchema(labelServiceMethods.ByName("CreateLabelRule")), - connect.WithClientOptions(opts...), - ), - getLabelRule: connect.NewClient[sdp_go.GetLabelRuleRequest, sdp_go.GetLabelRuleResponse]( - httpClient, - baseURL+LabelServiceGetLabelRuleProcedure, - connect.WithSchema(labelServiceMethods.ByName("GetLabelRule")), - connect.WithClientOptions(opts...), - ), - updateLabelRule: connect.NewClient[sdp_go.UpdateLabelRuleRequest, sdp_go.UpdateLabelRuleResponse]( - httpClient, - baseURL+LabelServiceUpdateLabelRuleProcedure, - connect.WithSchema(labelServiceMethods.ByName("UpdateLabelRule")), - connect.WithClientOptions(opts...), - ), - deleteLabelRule: connect.NewClient[sdp_go.DeleteLabelRuleRequest, sdp_go.DeleteLabelRuleResponse]( - httpClient, - baseURL+LabelServiceDeleteLabelRuleProcedure, - connect.WithSchema(labelServiceMethods.ByName("DeleteLabelRule")), - connect.WithClientOptions(opts...), - ), - testLabelRule: connect.NewClient[sdp_go.TestLabelRuleRequest, sdp_go.TestLabelRuleResponse]( - httpClient, - baseURL+LabelServiceTestLabelRuleProcedure, - connect.WithSchema(labelServiceMethods.ByName("TestLabelRule")), - connect.WithClientOptions(opts...), - ), - reapplyLabelRuleInTimeRange: connect.NewClient[sdp_go.ReapplyLabelRuleInTimeRangeRequest, sdp_go.ReapplyLabelRuleInTimeRangeResponse]( - httpClient, - baseURL+LabelServiceReapplyLabelRuleInTimeRangeProcedure, - connect.WithSchema(labelServiceMethods.ByName("ReapplyLabelRuleInTimeRange")), - connect.WithClientOptions(opts...), - ), - } -} - -// labelServiceClient implements LabelServiceClient. -type labelServiceClient struct { - listLabelRules *connect.Client[sdp_go.ListLabelRulesRequest, sdp_go.ListLabelRulesResponse] - createLabelRule *connect.Client[sdp_go.CreateLabelRuleRequest, sdp_go.CreateLabelRuleResponse] - getLabelRule *connect.Client[sdp_go.GetLabelRuleRequest, sdp_go.GetLabelRuleResponse] - updateLabelRule *connect.Client[sdp_go.UpdateLabelRuleRequest, sdp_go.UpdateLabelRuleResponse] - deleteLabelRule *connect.Client[sdp_go.DeleteLabelRuleRequest, sdp_go.DeleteLabelRuleResponse] - testLabelRule *connect.Client[sdp_go.TestLabelRuleRequest, sdp_go.TestLabelRuleResponse] - reapplyLabelRuleInTimeRange *connect.Client[sdp_go.ReapplyLabelRuleInTimeRangeRequest, sdp_go.ReapplyLabelRuleInTimeRangeResponse] -} - -// ListLabelRules calls changes.LabelService.ListLabelRules. -func (c *labelServiceClient) ListLabelRules(ctx context.Context, req *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) { - return c.listLabelRules.CallUnary(ctx, req) -} - -// CreateLabelRule calls changes.LabelService.CreateLabelRule. -func (c *labelServiceClient) CreateLabelRule(ctx context.Context, req *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) { - return c.createLabelRule.CallUnary(ctx, req) -} - -// GetLabelRule calls changes.LabelService.GetLabelRule. -func (c *labelServiceClient) GetLabelRule(ctx context.Context, req *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) { - return c.getLabelRule.CallUnary(ctx, req) -} - -// UpdateLabelRule calls changes.LabelService.UpdateLabelRule. -func (c *labelServiceClient) UpdateLabelRule(ctx context.Context, req *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) { - return c.updateLabelRule.CallUnary(ctx, req) -} - -// DeleteLabelRule calls changes.LabelService.DeleteLabelRule. -func (c *labelServiceClient) DeleteLabelRule(ctx context.Context, req *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) { - return c.deleteLabelRule.CallUnary(ctx, req) -} - -// TestLabelRule calls changes.LabelService.TestLabelRule. -func (c *labelServiceClient) TestLabelRule(ctx context.Context, req *connect.Request[sdp_go.TestLabelRuleRequest]) (*connect.ServerStreamForClient[sdp_go.TestLabelRuleResponse], error) { - return c.testLabelRule.CallServerStream(ctx, req) -} - -// ReapplyLabelRuleInTimeRange calls changes.LabelService.ReapplyLabelRuleInTimeRange. -func (c *labelServiceClient) ReapplyLabelRuleInTimeRange(ctx context.Context, req *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) { - return c.reapplyLabelRuleInTimeRange.CallUnary(ctx, req) -} - -// LabelServiceHandler is an implementation of the changes.LabelService service. -type LabelServiceHandler interface { - // Lists all label rules for an account - ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) - // Creates a new label rule - CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) - // Gets the details of a label rule - GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) - // Updates a label rule - UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) - // Deletes a label rule - // this also removes the label from all changes that are currently labelled with this rule - DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) - // Test a label rule against a list of changes, this does not apply the label to the changes, it only tests if the label should be applied - TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest], *connect.ServerStream[sdp_go.TestLabelRuleResponse]) error - // Re-apply a label rule across all changes within a specified time window: - // 1. Removes the label from all relevant changes in the period that match this rule - // 2. Applies (or re-applies) the label to eligible changes in the period - ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) -} - -// NewLabelServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewLabelServiceHandler(svc LabelServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - labelServiceMethods := sdp_go.File_changes_proto.Services().ByName("LabelService").Methods() - labelServiceListLabelRulesHandler := connect.NewUnaryHandler( - LabelServiceListLabelRulesProcedure, - svc.ListLabelRules, - connect.WithSchema(labelServiceMethods.ByName("ListLabelRules")), - connect.WithHandlerOptions(opts...), - ) - labelServiceCreateLabelRuleHandler := connect.NewUnaryHandler( - LabelServiceCreateLabelRuleProcedure, - svc.CreateLabelRule, - connect.WithSchema(labelServiceMethods.ByName("CreateLabelRule")), - connect.WithHandlerOptions(opts...), - ) - labelServiceGetLabelRuleHandler := connect.NewUnaryHandler( - LabelServiceGetLabelRuleProcedure, - svc.GetLabelRule, - connect.WithSchema(labelServiceMethods.ByName("GetLabelRule")), - connect.WithHandlerOptions(opts...), - ) - labelServiceUpdateLabelRuleHandler := connect.NewUnaryHandler( - LabelServiceUpdateLabelRuleProcedure, - svc.UpdateLabelRule, - connect.WithSchema(labelServiceMethods.ByName("UpdateLabelRule")), - connect.WithHandlerOptions(opts...), - ) - labelServiceDeleteLabelRuleHandler := connect.NewUnaryHandler( - LabelServiceDeleteLabelRuleProcedure, - svc.DeleteLabelRule, - connect.WithSchema(labelServiceMethods.ByName("DeleteLabelRule")), - connect.WithHandlerOptions(opts...), - ) - labelServiceTestLabelRuleHandler := connect.NewServerStreamHandler( - LabelServiceTestLabelRuleProcedure, - svc.TestLabelRule, - connect.WithSchema(labelServiceMethods.ByName("TestLabelRule")), - connect.WithHandlerOptions(opts...), - ) - labelServiceReapplyLabelRuleInTimeRangeHandler := connect.NewUnaryHandler( - LabelServiceReapplyLabelRuleInTimeRangeProcedure, - svc.ReapplyLabelRuleInTimeRange, - connect.WithSchema(labelServiceMethods.ByName("ReapplyLabelRuleInTimeRange")), - connect.WithHandlerOptions(opts...), - ) - return "/changes.LabelService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case LabelServiceListLabelRulesProcedure: - labelServiceListLabelRulesHandler.ServeHTTP(w, r) - case LabelServiceCreateLabelRuleProcedure: - labelServiceCreateLabelRuleHandler.ServeHTTP(w, r) - case LabelServiceGetLabelRuleProcedure: - labelServiceGetLabelRuleHandler.ServeHTTP(w, r) - case LabelServiceUpdateLabelRuleProcedure: - labelServiceUpdateLabelRuleHandler.ServeHTTP(w, r) - case LabelServiceDeleteLabelRuleProcedure: - labelServiceDeleteLabelRuleHandler.ServeHTTP(w, r) - case LabelServiceTestLabelRuleProcedure: - labelServiceTestLabelRuleHandler.ServeHTTP(w, r) - case LabelServiceReapplyLabelRuleInTimeRangeProcedure: - labelServiceReapplyLabelRuleInTimeRangeHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedLabelServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedLabelServiceHandler struct{} - -func (UnimplementedLabelServiceHandler) ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.ListLabelRules is not implemented")) -} - -func (UnimplementedLabelServiceHandler) CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.CreateLabelRule is not implemented")) -} - -func (UnimplementedLabelServiceHandler) GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.GetLabelRule is not implemented")) -} - -func (UnimplementedLabelServiceHandler) UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.UpdateLabelRule is not implemented")) -} - -func (UnimplementedLabelServiceHandler) DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.DeleteLabelRule is not implemented")) -} - -func (UnimplementedLabelServiceHandler) TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest], *connect.ServerStream[sdp_go.TestLabelRuleResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.TestLabelRule is not implemented")) -} - -func (UnimplementedLabelServiceHandler) ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.ReapplyLabelRuleInTimeRange is not implemented")) -} diff --git a/sdp-go/sdpconnect/cli.connect.go b/sdp-go/sdpconnect/cli.connect.go deleted file mode 100644 index f4910743..00000000 --- a/sdp-go/sdpconnect/cli.connect.go +++ /dev/null @@ -1,136 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: cli.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // ConfigServiceName is the fully-qualified name of the ConfigService service. - ConfigServiceName = "cli.ConfigService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // ConfigServiceGetConfigProcedure is the fully-qualified name of the ConfigService's GetConfig RPC. - ConfigServiceGetConfigProcedure = "/cli.ConfigService/GetConfig" - // ConfigServiceSetConfigProcedure is the fully-qualified name of the ConfigService's SetConfig RPC. - ConfigServiceSetConfigProcedure = "/cli.ConfigService/SetConfig" -) - -// ConfigServiceClient is a client for the cli.ConfigService service. -type ConfigServiceClient interface { - GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) - SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) -} - -// NewConfigServiceClient constructs a client for the cli.ConfigService service. By default, it uses -// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewConfigServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConfigServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - configServiceMethods := sdp_go.File_cli_proto.Services().ByName("ConfigService").Methods() - return &configServiceClient{ - getConfig: connect.NewClient[sdp_go.GetConfigRequest, sdp_go.GetConfigResponse]( - httpClient, - baseURL+ConfigServiceGetConfigProcedure, - connect.WithSchema(configServiceMethods.ByName("GetConfig")), - connect.WithClientOptions(opts...), - ), - setConfig: connect.NewClient[sdp_go.SetConfigRequest, sdp_go.SetConfigResponse]( - httpClient, - baseURL+ConfigServiceSetConfigProcedure, - connect.WithSchema(configServiceMethods.ByName("SetConfig")), - connect.WithClientOptions(opts...), - ), - } -} - -// configServiceClient implements ConfigServiceClient. -type configServiceClient struct { - getConfig *connect.Client[sdp_go.GetConfigRequest, sdp_go.GetConfigResponse] - setConfig *connect.Client[sdp_go.SetConfigRequest, sdp_go.SetConfigResponse] -} - -// GetConfig calls cli.ConfigService.GetConfig. -func (c *configServiceClient) GetConfig(ctx context.Context, req *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) { - return c.getConfig.CallUnary(ctx, req) -} - -// SetConfig calls cli.ConfigService.SetConfig. -func (c *configServiceClient) SetConfig(ctx context.Context, req *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) { - return c.setConfig.CallUnary(ctx, req) -} - -// ConfigServiceHandler is an implementation of the cli.ConfigService service. -type ConfigServiceHandler interface { - GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) - SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) -} - -// NewConfigServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewConfigServiceHandler(svc ConfigServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - configServiceMethods := sdp_go.File_cli_proto.Services().ByName("ConfigService").Methods() - configServiceGetConfigHandler := connect.NewUnaryHandler( - ConfigServiceGetConfigProcedure, - svc.GetConfig, - connect.WithSchema(configServiceMethods.ByName("GetConfig")), - connect.WithHandlerOptions(opts...), - ) - configServiceSetConfigHandler := connect.NewUnaryHandler( - ConfigServiceSetConfigProcedure, - svc.SetConfig, - connect.WithSchema(configServiceMethods.ByName("SetConfig")), - connect.WithHandlerOptions(opts...), - ) - return "/cli.ConfigService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case ConfigServiceGetConfigProcedure: - configServiceGetConfigHandler.ServeHTTP(w, r) - case ConfigServiceSetConfigProcedure: - configServiceSetConfigHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedConfigServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedConfigServiceHandler struct{} - -func (UnimplementedConfigServiceHandler) GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("cli.ConfigService.GetConfig is not implemented")) -} - -func (UnimplementedConfigServiceHandler) SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("cli.ConfigService.SetConfig is not implemented")) -} diff --git a/sdp-go/sdpconnect/config.connect.go b/sdp-go/sdpconnect/config.connect.go deleted file mode 100644 index 9ae09c45..00000000 --- a/sdp-go/sdpconnect/config.connect.go +++ /dev/null @@ -1,399 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: config.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // ConfigurationServiceName is the fully-qualified name of the ConfigurationService service. - ConfigurationServiceName = "config.ConfigurationService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // ConfigurationServiceGetAccountConfigProcedure is the fully-qualified name of the - // ConfigurationService's GetAccountConfig RPC. - ConfigurationServiceGetAccountConfigProcedure = "/config.ConfigurationService/GetAccountConfig" - // ConfigurationServiceUpdateAccountConfigProcedure is the fully-qualified name of the - // ConfigurationService's UpdateAccountConfig RPC. - ConfigurationServiceUpdateAccountConfigProcedure = "/config.ConfigurationService/UpdateAccountConfig" - // ConfigurationServiceCreateHcpConfigProcedure is the fully-qualified name of the - // ConfigurationService's CreateHcpConfig RPC. - ConfigurationServiceCreateHcpConfigProcedure = "/config.ConfigurationService/CreateHcpConfig" - // ConfigurationServiceGetHcpConfigProcedure is the fully-qualified name of the - // ConfigurationService's GetHcpConfig RPC. - ConfigurationServiceGetHcpConfigProcedure = "/config.ConfigurationService/GetHcpConfig" - // ConfigurationServiceDeleteHcpConfigProcedure is the fully-qualified name of the - // ConfigurationService's DeleteHcpConfig RPC. - ConfigurationServiceDeleteHcpConfigProcedure = "/config.ConfigurationService/DeleteHcpConfig" - // ConfigurationServiceGetSignalConfigProcedure is the fully-qualified name of the - // ConfigurationService's GetSignalConfig RPC. - ConfigurationServiceGetSignalConfigProcedure = "/config.ConfigurationService/GetSignalConfig" - // ConfigurationServiceUpdateSignalConfigProcedure is the fully-qualified name of the - // ConfigurationService's UpdateSignalConfig RPC. - ConfigurationServiceUpdateSignalConfigProcedure = "/config.ConfigurationService/UpdateSignalConfig" - // ConfigurationServiceGetGithubAppInformationProcedure is the fully-qualified name of the - // ConfigurationService's GetGithubAppInformation RPC. - ConfigurationServiceGetGithubAppInformationProcedure = "/config.ConfigurationService/GetGithubAppInformation" - // ConfigurationServiceRegenerateGithubAppProfileProcedure is the fully-qualified name of the - // ConfigurationService's RegenerateGithubAppProfile RPC. - ConfigurationServiceRegenerateGithubAppProfileProcedure = "/config.ConfigurationService/RegenerateGithubAppProfile" - // ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure is the fully-qualified - // name of the ConfigurationService's DeleteGithubAppProfileAndGithubInstallationID RPC. - ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure = "/config.ConfigurationService/DeleteGithubAppProfileAndGithubInstallationID" -) - -// ConfigurationServiceClient is a client for the config.ConfigurationService service. -type ConfigurationServiceClient interface { - // Get the account config for the user's account - GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) - // Update the account config for the user's account - UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) - // Create a new HCP Terraform config for the user's account. This follows - // the same flow as CreateAPIKey, to create a new API key that is then used - // for the HCP Terraform endpoint URL. - CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) - // Get the existing HCP Terraform config for the user's account. - GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) - // Remove the existing HCP Terraform config from the user's account. - DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) - // Get the signal config for the account - GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) - // Update the signal config for the account - UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) - // Github app - // we will be displaying app installation information for this account on the github integrations page - GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) - // regenerate the github app profile, this information is used for signal processing - RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) - // remove the github app installation id and github organisation profile from the signal config - // this will not uninstall the app from github, that must be done manually by the user - DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) -} - -// NewConfigurationServiceClient constructs a client for the config.ConfigurationService service. By -// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, -// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewConfigurationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConfigurationServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - configurationServiceMethods := sdp_go.File_config_proto.Services().ByName("ConfigurationService").Methods() - return &configurationServiceClient{ - getAccountConfig: connect.NewClient[sdp_go.GetAccountConfigRequest, sdp_go.GetAccountConfigResponse]( - httpClient, - baseURL+ConfigurationServiceGetAccountConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("GetAccountConfig")), - connect.WithClientOptions(opts...), - ), - updateAccountConfig: connect.NewClient[sdp_go.UpdateAccountConfigRequest, sdp_go.UpdateAccountConfigResponse]( - httpClient, - baseURL+ConfigurationServiceUpdateAccountConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("UpdateAccountConfig")), - connect.WithClientOptions(opts...), - ), - createHcpConfig: connect.NewClient[sdp_go.CreateHcpConfigRequest, sdp_go.CreateHcpConfigResponse]( - httpClient, - baseURL+ConfigurationServiceCreateHcpConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("CreateHcpConfig")), - connect.WithClientOptions(opts...), - ), - getHcpConfig: connect.NewClient[sdp_go.GetHcpConfigRequest, sdp_go.GetHcpConfigResponse]( - httpClient, - baseURL+ConfigurationServiceGetHcpConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("GetHcpConfig")), - connect.WithClientOptions(opts...), - ), - deleteHcpConfig: connect.NewClient[sdp_go.DeleteHcpConfigRequest, sdp_go.DeleteHcpConfigResponse]( - httpClient, - baseURL+ConfigurationServiceDeleteHcpConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), - connect.WithClientOptions(opts...), - ), - getSignalConfig: connect.NewClient[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse]( - httpClient, - baseURL+ConfigurationServiceGetSignalConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("GetSignalConfig")), - connect.WithClientOptions(opts...), - ), - updateSignalConfig: connect.NewClient[sdp_go.UpdateSignalConfigRequest, sdp_go.UpdateSignalConfigResponse]( - httpClient, - baseURL+ConfigurationServiceUpdateSignalConfigProcedure, - connect.WithSchema(configurationServiceMethods.ByName("UpdateSignalConfig")), - connect.WithClientOptions(opts...), - ), - getGithubAppInformation: connect.NewClient[sdp_go.GetGithubAppInformationRequest, sdp_go.GetGithubAppInformationResponse]( - httpClient, - baseURL+ConfigurationServiceGetGithubAppInformationProcedure, - connect.WithSchema(configurationServiceMethods.ByName("GetGithubAppInformation")), - connect.WithClientOptions(opts...), - ), - regenerateGithubAppProfile: connect.NewClient[sdp_go.RegenerateGithubAppProfileRequest, sdp_go.RegenerateGithubAppProfileResponse]( - httpClient, - baseURL+ConfigurationServiceRegenerateGithubAppProfileProcedure, - connect.WithSchema(configurationServiceMethods.ByName("RegenerateGithubAppProfile")), - connect.WithClientOptions(opts...), - ), - deleteGithubAppProfileAndGithubInstallationID: connect.NewClient[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest, sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse]( - httpClient, - baseURL+ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure, - connect.WithSchema(configurationServiceMethods.ByName("DeleteGithubAppProfileAndGithubInstallationID")), - connect.WithClientOptions(opts...), - ), - } -} - -// configurationServiceClient implements ConfigurationServiceClient. -type configurationServiceClient struct { - getAccountConfig *connect.Client[sdp_go.GetAccountConfigRequest, sdp_go.GetAccountConfigResponse] - updateAccountConfig *connect.Client[sdp_go.UpdateAccountConfigRequest, sdp_go.UpdateAccountConfigResponse] - createHcpConfig *connect.Client[sdp_go.CreateHcpConfigRequest, sdp_go.CreateHcpConfigResponse] - getHcpConfig *connect.Client[sdp_go.GetHcpConfigRequest, sdp_go.GetHcpConfigResponse] - deleteHcpConfig *connect.Client[sdp_go.DeleteHcpConfigRequest, sdp_go.DeleteHcpConfigResponse] - getSignalConfig *connect.Client[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse] - updateSignalConfig *connect.Client[sdp_go.UpdateSignalConfigRequest, sdp_go.UpdateSignalConfigResponse] - getGithubAppInformation *connect.Client[sdp_go.GetGithubAppInformationRequest, sdp_go.GetGithubAppInformationResponse] - regenerateGithubAppProfile *connect.Client[sdp_go.RegenerateGithubAppProfileRequest, sdp_go.RegenerateGithubAppProfileResponse] - deleteGithubAppProfileAndGithubInstallationID *connect.Client[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest, sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse] -} - -// GetAccountConfig calls config.ConfigurationService.GetAccountConfig. -func (c *configurationServiceClient) GetAccountConfig(ctx context.Context, req *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) { - return c.getAccountConfig.CallUnary(ctx, req) -} - -// UpdateAccountConfig calls config.ConfigurationService.UpdateAccountConfig. -func (c *configurationServiceClient) UpdateAccountConfig(ctx context.Context, req *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) { - return c.updateAccountConfig.CallUnary(ctx, req) -} - -// CreateHcpConfig calls config.ConfigurationService.CreateHcpConfig. -func (c *configurationServiceClient) CreateHcpConfig(ctx context.Context, req *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) { - return c.createHcpConfig.CallUnary(ctx, req) -} - -// GetHcpConfig calls config.ConfigurationService.GetHcpConfig. -func (c *configurationServiceClient) GetHcpConfig(ctx context.Context, req *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) { - return c.getHcpConfig.CallUnary(ctx, req) -} - -// DeleteHcpConfig calls config.ConfigurationService.DeleteHcpConfig. -func (c *configurationServiceClient) DeleteHcpConfig(ctx context.Context, req *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) { - return c.deleteHcpConfig.CallUnary(ctx, req) -} - -// GetSignalConfig calls config.ConfigurationService.GetSignalConfig. -func (c *configurationServiceClient) GetSignalConfig(ctx context.Context, req *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { - return c.getSignalConfig.CallUnary(ctx, req) -} - -// UpdateSignalConfig calls config.ConfigurationService.UpdateSignalConfig. -func (c *configurationServiceClient) UpdateSignalConfig(ctx context.Context, req *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) { - return c.updateSignalConfig.CallUnary(ctx, req) -} - -// GetGithubAppInformation calls config.ConfigurationService.GetGithubAppInformation. -func (c *configurationServiceClient) GetGithubAppInformation(ctx context.Context, req *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) { - return c.getGithubAppInformation.CallUnary(ctx, req) -} - -// RegenerateGithubAppProfile calls config.ConfigurationService.RegenerateGithubAppProfile. -func (c *configurationServiceClient) RegenerateGithubAppProfile(ctx context.Context, req *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) { - return c.regenerateGithubAppProfile.CallUnary(ctx, req) -} - -// DeleteGithubAppProfileAndGithubInstallationID calls -// config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID. -func (c *configurationServiceClient) DeleteGithubAppProfileAndGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) { - return c.deleteGithubAppProfileAndGithubInstallationID.CallUnary(ctx, req) -} - -// ConfigurationServiceHandler is an implementation of the config.ConfigurationService service. -type ConfigurationServiceHandler interface { - // Get the account config for the user's account - GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) - // Update the account config for the user's account - UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) - // Create a new HCP Terraform config for the user's account. This follows - // the same flow as CreateAPIKey, to create a new API key that is then used - // for the HCP Terraform endpoint URL. - CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) - // Get the existing HCP Terraform config for the user's account. - GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) - // Remove the existing HCP Terraform config from the user's account. - DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) - // Get the signal config for the account - GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) - // Update the signal config for the account - UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) - // Github app - // we will be displaying app installation information for this account on the github integrations page - GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) - // regenerate the github app profile, this information is used for signal processing - RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) - // remove the github app installation id and github organisation profile from the signal config - // this will not uninstall the app from github, that must be done manually by the user - DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) -} - -// NewConfigurationServiceHandler builds an HTTP handler from the service implementation. It returns -// the path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewConfigurationServiceHandler(svc ConfigurationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - configurationServiceMethods := sdp_go.File_config_proto.Services().ByName("ConfigurationService").Methods() - configurationServiceGetAccountConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceGetAccountConfigProcedure, - svc.GetAccountConfig, - connect.WithSchema(configurationServiceMethods.ByName("GetAccountConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceUpdateAccountConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceUpdateAccountConfigProcedure, - svc.UpdateAccountConfig, - connect.WithSchema(configurationServiceMethods.ByName("UpdateAccountConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceCreateHcpConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceCreateHcpConfigProcedure, - svc.CreateHcpConfig, - connect.WithSchema(configurationServiceMethods.ByName("CreateHcpConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceGetHcpConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceGetHcpConfigProcedure, - svc.GetHcpConfig, - connect.WithSchema(configurationServiceMethods.ByName("GetHcpConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceDeleteHcpConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceDeleteHcpConfigProcedure, - svc.DeleteHcpConfig, - connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceGetSignalConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceGetSignalConfigProcedure, - svc.GetSignalConfig, - connect.WithSchema(configurationServiceMethods.ByName("GetSignalConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceUpdateSignalConfigHandler := connect.NewUnaryHandler( - ConfigurationServiceUpdateSignalConfigProcedure, - svc.UpdateSignalConfig, - connect.WithSchema(configurationServiceMethods.ByName("UpdateSignalConfig")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceGetGithubAppInformationHandler := connect.NewUnaryHandler( - ConfigurationServiceGetGithubAppInformationProcedure, - svc.GetGithubAppInformation, - connect.WithSchema(configurationServiceMethods.ByName("GetGithubAppInformation")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceRegenerateGithubAppProfileHandler := connect.NewUnaryHandler( - ConfigurationServiceRegenerateGithubAppProfileProcedure, - svc.RegenerateGithubAppProfile, - connect.WithSchema(configurationServiceMethods.ByName("RegenerateGithubAppProfile")), - connect.WithHandlerOptions(opts...), - ) - configurationServiceDeleteGithubAppProfileAndGithubInstallationIDHandler := connect.NewUnaryHandler( - ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure, - svc.DeleteGithubAppProfileAndGithubInstallationID, - connect.WithSchema(configurationServiceMethods.ByName("DeleteGithubAppProfileAndGithubInstallationID")), - connect.WithHandlerOptions(opts...), - ) - return "/config.ConfigurationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case ConfigurationServiceGetAccountConfigProcedure: - configurationServiceGetAccountConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceUpdateAccountConfigProcedure: - configurationServiceUpdateAccountConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceCreateHcpConfigProcedure: - configurationServiceCreateHcpConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceGetHcpConfigProcedure: - configurationServiceGetHcpConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceDeleteHcpConfigProcedure: - configurationServiceDeleteHcpConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceGetSignalConfigProcedure: - configurationServiceGetSignalConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceUpdateSignalConfigProcedure: - configurationServiceUpdateSignalConfigHandler.ServeHTTP(w, r) - case ConfigurationServiceGetGithubAppInformationProcedure: - configurationServiceGetGithubAppInformationHandler.ServeHTTP(w, r) - case ConfigurationServiceRegenerateGithubAppProfileProcedure: - configurationServiceRegenerateGithubAppProfileHandler.ServeHTTP(w, r) - case ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure: - configurationServiceDeleteGithubAppProfileAndGithubInstallationIDHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedConfigurationServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedConfigurationServiceHandler struct{} - -func (UnimplementedConfigurationServiceHandler) GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetAccountConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.UpdateAccountConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.CreateHcpConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetHcpConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.DeleteHcpConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetSignalConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.UpdateSignalConfig is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetGithubAppInformation is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.RegenerateGithubAppProfile is not implemented")) -} - -func (UnimplementedConfigurationServiceHandler) DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID is not implemented")) -} diff --git a/sdp-go/sdpconnect/invites.connect.go b/sdp-go/sdpconnect/invites.connect.go deleted file mode 100644 index 189a9d71..00000000 --- a/sdp-go/sdpconnect/invites.connect.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: invites.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // InviteServiceName is the fully-qualified name of the InviteService service. - InviteServiceName = "invites.InviteService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // InviteServiceCreateInviteProcedure is the fully-qualified name of the InviteService's - // CreateInvite RPC. - InviteServiceCreateInviteProcedure = "/invites.InviteService/CreateInvite" - // InviteServiceListInvitesProcedure is the fully-qualified name of the InviteService's ListInvites - // RPC. - InviteServiceListInvitesProcedure = "/invites.InviteService/ListInvites" - // InviteServiceRevokeInviteProcedure is the fully-qualified name of the InviteService's - // RevokeInvite RPC. - InviteServiceRevokeInviteProcedure = "/invites.InviteService/RevokeInvite" - // InviteServiceResendInviteProcedure is the fully-qualified name of the InviteService's - // ResendInvite RPC. - InviteServiceResendInviteProcedure = "/invites.InviteService/ResendInvite" -) - -// InviteServiceClient is a client for the invites.InviteService service. -type InviteServiceClient interface { - CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) - ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) - RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) - ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) -} - -// NewInviteServiceClient constructs a client for the invites.InviteService service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewInviteServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) InviteServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - inviteServiceMethods := sdp_go.File_invites_proto.Services().ByName("InviteService").Methods() - return &inviteServiceClient{ - createInvite: connect.NewClient[sdp_go.CreateInviteRequest, sdp_go.CreateInviteResponse]( - httpClient, - baseURL+InviteServiceCreateInviteProcedure, - connect.WithSchema(inviteServiceMethods.ByName("CreateInvite")), - connect.WithClientOptions(opts...), - ), - listInvites: connect.NewClient[sdp_go.ListInvitesRequest, sdp_go.ListInvitesResponse]( - httpClient, - baseURL+InviteServiceListInvitesProcedure, - connect.WithSchema(inviteServiceMethods.ByName("ListInvites")), - connect.WithClientOptions(opts...), - ), - revokeInvite: connect.NewClient[sdp_go.RevokeInviteRequest, sdp_go.RevokeInviteResponse]( - httpClient, - baseURL+InviteServiceRevokeInviteProcedure, - connect.WithSchema(inviteServiceMethods.ByName("RevokeInvite")), - connect.WithClientOptions(opts...), - ), - resendInvite: connect.NewClient[sdp_go.ResendInviteRequest, sdp_go.ResendInviteResponse]( - httpClient, - baseURL+InviteServiceResendInviteProcedure, - connect.WithSchema(inviteServiceMethods.ByName("ResendInvite")), - connect.WithClientOptions(opts...), - ), - } -} - -// inviteServiceClient implements InviteServiceClient. -type inviteServiceClient struct { - createInvite *connect.Client[sdp_go.CreateInviteRequest, sdp_go.CreateInviteResponse] - listInvites *connect.Client[sdp_go.ListInvitesRequest, sdp_go.ListInvitesResponse] - revokeInvite *connect.Client[sdp_go.RevokeInviteRequest, sdp_go.RevokeInviteResponse] - resendInvite *connect.Client[sdp_go.ResendInviteRequest, sdp_go.ResendInviteResponse] -} - -// CreateInvite calls invites.InviteService.CreateInvite. -func (c *inviteServiceClient) CreateInvite(ctx context.Context, req *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) { - return c.createInvite.CallUnary(ctx, req) -} - -// ListInvites calls invites.InviteService.ListInvites. -func (c *inviteServiceClient) ListInvites(ctx context.Context, req *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) { - return c.listInvites.CallUnary(ctx, req) -} - -// RevokeInvite calls invites.InviteService.RevokeInvite. -func (c *inviteServiceClient) RevokeInvite(ctx context.Context, req *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) { - return c.revokeInvite.CallUnary(ctx, req) -} - -// ResendInvite calls invites.InviteService.ResendInvite. -func (c *inviteServiceClient) ResendInvite(ctx context.Context, req *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) { - return c.resendInvite.CallUnary(ctx, req) -} - -// InviteServiceHandler is an implementation of the invites.InviteService service. -type InviteServiceHandler interface { - CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) - ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) - RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) - ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) -} - -// NewInviteServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewInviteServiceHandler(svc InviteServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - inviteServiceMethods := sdp_go.File_invites_proto.Services().ByName("InviteService").Methods() - inviteServiceCreateInviteHandler := connect.NewUnaryHandler( - InviteServiceCreateInviteProcedure, - svc.CreateInvite, - connect.WithSchema(inviteServiceMethods.ByName("CreateInvite")), - connect.WithHandlerOptions(opts...), - ) - inviteServiceListInvitesHandler := connect.NewUnaryHandler( - InviteServiceListInvitesProcedure, - svc.ListInvites, - connect.WithSchema(inviteServiceMethods.ByName("ListInvites")), - connect.WithHandlerOptions(opts...), - ) - inviteServiceRevokeInviteHandler := connect.NewUnaryHandler( - InviteServiceRevokeInviteProcedure, - svc.RevokeInvite, - connect.WithSchema(inviteServiceMethods.ByName("RevokeInvite")), - connect.WithHandlerOptions(opts...), - ) - inviteServiceResendInviteHandler := connect.NewUnaryHandler( - InviteServiceResendInviteProcedure, - svc.ResendInvite, - connect.WithSchema(inviteServiceMethods.ByName("ResendInvite")), - connect.WithHandlerOptions(opts...), - ) - return "/invites.InviteService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case InviteServiceCreateInviteProcedure: - inviteServiceCreateInviteHandler.ServeHTTP(w, r) - case InviteServiceListInvitesProcedure: - inviteServiceListInvitesHandler.ServeHTTP(w, r) - case InviteServiceRevokeInviteProcedure: - inviteServiceRevokeInviteHandler.ServeHTTP(w, r) - case InviteServiceResendInviteProcedure: - inviteServiceResendInviteHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedInviteServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedInviteServiceHandler struct{} - -func (UnimplementedInviteServiceHandler) CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.CreateInvite is not implemented")) -} - -func (UnimplementedInviteServiceHandler) ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.ListInvites is not implemented")) -} - -func (UnimplementedInviteServiceHandler) RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.RevokeInvite is not implemented")) -} - -func (UnimplementedInviteServiceHandler) ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.ResendInvite is not implemented")) -} diff --git a/sdp-go/sdpconnect/logs.connect.go b/sdp-go/sdpconnect/logs.connect.go deleted file mode 100644 index 16f403e5..00000000 --- a/sdp-go/sdpconnect/logs.connect.go +++ /dev/null @@ -1,117 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: logs.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // LogsServiceName is the fully-qualified name of the LogsService service. - LogsServiceName = "logs.LogsService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // LogsServiceGetLogRecordsProcedure is the fully-qualified name of the LogsService's GetLogRecords - // RPC. - LogsServiceGetLogRecordsProcedure = "/logs.LogsService/GetLogRecords" -) - -// LogsServiceClient is a client for the logs.LogsService service. -type LogsServiceClient interface { - // GetLogRecords returns a stream of log records from the upstream API. The - // source is expected to use sane defaults within the limits of the - // underlying API and SDP capabilities (message size, etc). Each chunk is - // roughly a page of the upstream APIs pagination. - GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest]) (*connect.ServerStreamForClient[sdp_go.GetLogRecordsResponse], error) -} - -// NewLogsServiceClient constructs a client for the logs.LogsService service. By default, it uses -// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewLogsServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) LogsServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - logsServiceMethods := sdp_go.File_logs_proto.Services().ByName("LogsService").Methods() - return &logsServiceClient{ - getLogRecords: connect.NewClient[sdp_go.GetLogRecordsRequest, sdp_go.GetLogRecordsResponse]( - httpClient, - baseURL+LogsServiceGetLogRecordsProcedure, - connect.WithSchema(logsServiceMethods.ByName("GetLogRecords")), - connect.WithClientOptions(opts...), - ), - } -} - -// logsServiceClient implements LogsServiceClient. -type logsServiceClient struct { - getLogRecords *connect.Client[sdp_go.GetLogRecordsRequest, sdp_go.GetLogRecordsResponse] -} - -// GetLogRecords calls logs.LogsService.GetLogRecords. -func (c *logsServiceClient) GetLogRecords(ctx context.Context, req *connect.Request[sdp_go.GetLogRecordsRequest]) (*connect.ServerStreamForClient[sdp_go.GetLogRecordsResponse], error) { - return c.getLogRecords.CallServerStream(ctx, req) -} - -// LogsServiceHandler is an implementation of the logs.LogsService service. -type LogsServiceHandler interface { - // GetLogRecords returns a stream of log records from the upstream API. The - // source is expected to use sane defaults within the limits of the - // underlying API and SDP capabilities (message size, etc). Each chunk is - // roughly a page of the upstream APIs pagination. - GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest], *connect.ServerStream[sdp_go.GetLogRecordsResponse]) error -} - -// NewLogsServiceHandler builds an HTTP handler from the service implementation. It returns the path -// on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewLogsServiceHandler(svc LogsServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - logsServiceMethods := sdp_go.File_logs_proto.Services().ByName("LogsService").Methods() - logsServiceGetLogRecordsHandler := connect.NewServerStreamHandler( - LogsServiceGetLogRecordsProcedure, - svc.GetLogRecords, - connect.WithSchema(logsServiceMethods.ByName("GetLogRecords")), - connect.WithHandlerOptions(opts...), - ) - return "/logs.LogsService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case LogsServiceGetLogRecordsProcedure: - logsServiceGetLogRecordsHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedLogsServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedLogsServiceHandler struct{} - -func (UnimplementedLogsServiceHandler) GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest], *connect.ServerStream[sdp_go.GetLogRecordsResponse]) error { - return connect.NewError(connect.CodeUnimplemented, errors.New("logs.LogsService.GetLogRecords is not implemented")) -} diff --git a/sdp-go/sdpconnect/revlink.connect.go b/sdp-go/sdpconnect/revlink.connect.go deleted file mode 100644 index 673a48f9..00000000 --- a/sdp-go/sdpconnect/revlink.connect.go +++ /dev/null @@ -1,189 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: revlink.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // RevlinkServiceName is the fully-qualified name of the RevlinkService service. - RevlinkServiceName = "revlink.RevlinkService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // RevlinkServiceGetReverseEdgesProcedure is the fully-qualified name of the RevlinkService's - // GetReverseEdges RPC. - RevlinkServiceGetReverseEdgesProcedure = "/revlink.RevlinkService/GetReverseEdges" - // RevlinkServiceIngestGatewayResponsesProcedure is the fully-qualified name of the RevlinkService's - // IngestGatewayResponses RPC. - RevlinkServiceIngestGatewayResponsesProcedure = "/revlink.RevlinkService/IngestGatewayResponses" - // RevlinkServiceCheckpointProcedure is the fully-qualified name of the RevlinkService's Checkpoint - // RPC. - RevlinkServiceCheckpointProcedure = "/revlink.RevlinkService/Checkpoint" -) - -// RevlinkServiceClient is a client for the revlink.RevlinkService service. -type RevlinkServiceClient interface { - // Gets reverse edges for a given item - GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) - // Ingests a stream of gateway responses - IngestGatewayResponses(context.Context) *connect.ClientStreamForClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] - // Waits until all currently submitted gateway responses are committed to - // the database. This is primarily intended for tests to ensure that setup - // was completed. - // - // Note that this does only count the first try of each insertion; retries - // are not considered. - // - // Note2 that this is implemented in memory, so there is no guarantee - // that this will work in a distributed environment. - Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) -} - -// NewRevlinkServiceClient constructs a client for the revlink.RevlinkService service. By default, -// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and -// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() -// or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewRevlinkServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RevlinkServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - revlinkServiceMethods := sdp_go.File_revlink_proto.Services().ByName("RevlinkService").Methods() - return &revlinkServiceClient{ - getReverseEdges: connect.NewClient[sdp_go.GetReverseEdgesRequest, sdp_go.GetReverseEdgesResponse]( - httpClient, - baseURL+RevlinkServiceGetReverseEdgesProcedure, - connect.WithSchema(revlinkServiceMethods.ByName("GetReverseEdges")), - connect.WithClientOptions(opts...), - ), - ingestGatewayResponses: connect.NewClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse]( - httpClient, - baseURL+RevlinkServiceIngestGatewayResponsesProcedure, - connect.WithSchema(revlinkServiceMethods.ByName("IngestGatewayResponses")), - connect.WithClientOptions(opts...), - ), - checkpoint: connect.NewClient[sdp_go.CheckpointRequest, sdp_go.CheckpointResponse]( - httpClient, - baseURL+RevlinkServiceCheckpointProcedure, - connect.WithSchema(revlinkServiceMethods.ByName("Checkpoint")), - connect.WithClientOptions(opts...), - ), - } -} - -// revlinkServiceClient implements RevlinkServiceClient. -type revlinkServiceClient struct { - getReverseEdges *connect.Client[sdp_go.GetReverseEdgesRequest, sdp_go.GetReverseEdgesResponse] - ingestGatewayResponses *connect.Client[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] - checkpoint *connect.Client[sdp_go.CheckpointRequest, sdp_go.CheckpointResponse] -} - -// GetReverseEdges calls revlink.RevlinkService.GetReverseEdges. -func (c *revlinkServiceClient) GetReverseEdges(ctx context.Context, req *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) { - return c.getReverseEdges.CallUnary(ctx, req) -} - -// IngestGatewayResponses calls revlink.RevlinkService.IngestGatewayResponses. -func (c *revlinkServiceClient) IngestGatewayResponses(ctx context.Context) *connect.ClientStreamForClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] { - return c.ingestGatewayResponses.CallClientStream(ctx) -} - -// Checkpoint calls revlink.RevlinkService.Checkpoint. -func (c *revlinkServiceClient) Checkpoint(ctx context.Context, req *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) { - return c.checkpoint.CallUnary(ctx, req) -} - -// RevlinkServiceHandler is an implementation of the revlink.RevlinkService service. -type RevlinkServiceHandler interface { - // Gets reverse edges for a given item - GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) - // Ingests a stream of gateway responses - IngestGatewayResponses(context.Context, *connect.ClientStream[sdp_go.IngestGatewayResponseRequest]) (*connect.Response[sdp_go.IngestGatewayResponsesResponse], error) - // Waits until all currently submitted gateway responses are committed to - // the database. This is primarily intended for tests to ensure that setup - // was completed. - // - // Note that this does only count the first try of each insertion; retries - // are not considered. - // - // Note2 that this is implemented in memory, so there is no guarantee - // that this will work in a distributed environment. - Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) -} - -// NewRevlinkServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewRevlinkServiceHandler(svc RevlinkServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - revlinkServiceMethods := sdp_go.File_revlink_proto.Services().ByName("RevlinkService").Methods() - revlinkServiceGetReverseEdgesHandler := connect.NewUnaryHandler( - RevlinkServiceGetReverseEdgesProcedure, - svc.GetReverseEdges, - connect.WithSchema(revlinkServiceMethods.ByName("GetReverseEdges")), - connect.WithHandlerOptions(opts...), - ) - revlinkServiceIngestGatewayResponsesHandler := connect.NewClientStreamHandler( - RevlinkServiceIngestGatewayResponsesProcedure, - svc.IngestGatewayResponses, - connect.WithSchema(revlinkServiceMethods.ByName("IngestGatewayResponses")), - connect.WithHandlerOptions(opts...), - ) - revlinkServiceCheckpointHandler := connect.NewUnaryHandler( - RevlinkServiceCheckpointProcedure, - svc.Checkpoint, - connect.WithSchema(revlinkServiceMethods.ByName("Checkpoint")), - connect.WithHandlerOptions(opts...), - ) - return "/revlink.RevlinkService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case RevlinkServiceGetReverseEdgesProcedure: - revlinkServiceGetReverseEdgesHandler.ServeHTTP(w, r) - case RevlinkServiceIngestGatewayResponsesProcedure: - revlinkServiceIngestGatewayResponsesHandler.ServeHTTP(w, r) - case RevlinkServiceCheckpointProcedure: - revlinkServiceCheckpointHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedRevlinkServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedRevlinkServiceHandler struct{} - -func (UnimplementedRevlinkServiceHandler) GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.GetReverseEdges is not implemented")) -} - -func (UnimplementedRevlinkServiceHandler) IngestGatewayResponses(context.Context, *connect.ClientStream[sdp_go.IngestGatewayResponseRequest]) (*connect.Response[sdp_go.IngestGatewayResponsesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.IngestGatewayResponses is not implemented")) -} - -func (UnimplementedRevlinkServiceHandler) Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.Checkpoint is not implemented")) -} diff --git a/sdp-go/sdpconnect/signal.connect.go b/sdp-go/sdpconnect/signal.connect.go deleted file mode 100644 index 41df42f1..00000000 --- a/sdp-go/sdpconnect/signal.connect.go +++ /dev/null @@ -1,330 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: signal.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // SignalServiceName is the fully-qualified name of the SignalService service. - SignalServiceName = "signal.SignalService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // SignalServiceAddSignalProcedure is the fully-qualified name of the SignalService's AddSignal RPC. - SignalServiceAddSignalProcedure = "/signal.SignalService/AddSignal" - // SignalServiceGetSignalsByChangeExternalIDProcedure is the fully-qualified name of the - // SignalService's GetSignalsByChangeExternalID RPC. - SignalServiceGetSignalsByChangeExternalIDProcedure = "/signal.SignalService/GetSignalsByChangeExternalID" - // SignalServiceGetChangeOverviewSignalsProcedure is the fully-qualified name of the SignalService's - // GetChangeOverviewSignals RPC. - SignalServiceGetChangeOverviewSignalsProcedure = "/signal.SignalService/GetChangeOverviewSignals" - // SignalServiceGetItemSignalsProcedure is the fully-qualified name of the SignalService's - // GetItemSignals RPC. - SignalServiceGetItemSignalsProcedure = "/signal.SignalService/GetItemSignals" - // SignalServiceGetItemSignalsV2Procedure is the fully-qualified name of the SignalService's - // GetItemSignalsV2 RPC. - SignalServiceGetItemSignalsV2Procedure = "/signal.SignalService/GetItemSignalsV2" - // SignalServiceGetCustomSignalsByCategoryProcedure is the fully-qualified name of the - // SignalService's GetCustomSignalsByCategory RPC. - SignalServiceGetCustomSignalsByCategoryProcedure = "/signal.SignalService/GetCustomSignalsByCategory" - // SignalServiceGetItemSignalDetailsProcedure is the fully-qualified name of the SignalService's - // GetItemSignalDetails RPC. - SignalServiceGetItemSignalDetailsProcedure = "/signal.SignalService/GetItemSignalDetails" -) - -// SignalServiceClient is a client for the signal.SignalService service. -type SignalServiceClient interface { - // This is an external API to add a signals to a change. - // It will be used by the CLI, the web UI, and other clients. - // It expects the user to provide the properties of the signal, such as name, value, description, and category. - // And it returns the signal that was added, including the machine-generated metadata such as UUID and aggregation ID. - // DOES THIS NEED TO BE UPDATED TO SPECIFY THE LEVEL OF THE SIGNAL? - AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) - // This is an API to get all signals associated with a change by its external ID. It is not used by the frontend. - // It returns a slice/array of Signals. Which includes both the user-facing properties of the signal, and the machine-generated metadata. - // Look at the Signal message for more details. - GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) - // NB for the following we do not need to specify the icons for the Items, as they are derived by the Frontend separately. - // Get all top-level signals for a change. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) - // Get item-level signals for all items in a change. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) - // Get a slice of items, sorted by their aggregate signal value, ascending. - // for each item include - // - an aggregated value for the item, calculated by AggregateSignalScores. - // - a friendly item ref, also before and after - // - a slice of signals for the item, sorted by the signal value, ascending. - // - the status of the item, e.g. "added", "modified", "deleted". - GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) - // Get all custom signals for a change by its external ID and category. They are NOT associated with any item. - // There is no score aggregation or description aggregation at this level. They are aggregated at the GetChangeOverviewSignals level. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) - // Get all signals for attributes/modifications of an item. This will only be used for routineness to start with. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) -} - -// NewSignalServiceClient constructs a client for the signal.SignalService service. By default, it -// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends -// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or -// connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewSignalServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SignalServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - signalServiceMethods := sdp_go.File_signal_proto.Services().ByName("SignalService").Methods() - return &signalServiceClient{ - addSignal: connect.NewClient[sdp_go.AddSignalRequest, sdp_go.AddSignalResponse]( - httpClient, - baseURL+SignalServiceAddSignalProcedure, - connect.WithSchema(signalServiceMethods.ByName("AddSignal")), - connect.WithClientOptions(opts...), - ), - getSignalsByChangeExternalID: connect.NewClient[sdp_go.GetSignalsByChangeExternalIDRequest, sdp_go.GetSignalsByChangeExternalIDResponse]( - httpClient, - baseURL+SignalServiceGetSignalsByChangeExternalIDProcedure, - connect.WithSchema(signalServiceMethods.ByName("GetSignalsByChangeExternalID")), - connect.WithClientOptions(opts...), - ), - getChangeOverviewSignals: connect.NewClient[sdp_go.GetChangeOverviewSignalsRequest, sdp_go.GetChangeOverviewSignalsResponse]( - httpClient, - baseURL+SignalServiceGetChangeOverviewSignalsProcedure, - connect.WithSchema(signalServiceMethods.ByName("GetChangeOverviewSignals")), - connect.WithClientOptions(opts...), - ), - getItemSignals: connect.NewClient[sdp_go.GetItemSignalsRequest, sdp_go.GetItemSignalsResponse]( - httpClient, - baseURL+SignalServiceGetItemSignalsProcedure, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignals")), - connect.WithClientOptions(opts...), - ), - getItemSignalsV2: connect.NewClient[sdp_go.GetItemSignalsRequestV2, sdp_go.GetItemSignalsResponseV2]( - httpClient, - baseURL+SignalServiceGetItemSignalsV2Procedure, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignalsV2")), - connect.WithClientOptions(opts...), - ), - getCustomSignalsByCategory: connect.NewClient[sdp_go.GetCustomSignalsByCategoryRequest, sdp_go.GetCustomSignalsByCategoryResponse]( - httpClient, - baseURL+SignalServiceGetCustomSignalsByCategoryProcedure, - connect.WithSchema(signalServiceMethods.ByName("GetCustomSignalsByCategory")), - connect.WithClientOptions(opts...), - ), - getItemSignalDetails: connect.NewClient[sdp_go.GetItemSignalDetailsRequest, sdp_go.GetItemSignalDetailsResponse]( - httpClient, - baseURL+SignalServiceGetItemSignalDetailsProcedure, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignalDetails")), - connect.WithClientOptions(opts...), - ), - } -} - -// signalServiceClient implements SignalServiceClient. -type signalServiceClient struct { - addSignal *connect.Client[sdp_go.AddSignalRequest, sdp_go.AddSignalResponse] - getSignalsByChangeExternalID *connect.Client[sdp_go.GetSignalsByChangeExternalIDRequest, sdp_go.GetSignalsByChangeExternalIDResponse] - getChangeOverviewSignals *connect.Client[sdp_go.GetChangeOverviewSignalsRequest, sdp_go.GetChangeOverviewSignalsResponse] - getItemSignals *connect.Client[sdp_go.GetItemSignalsRequest, sdp_go.GetItemSignalsResponse] - getItemSignalsV2 *connect.Client[sdp_go.GetItemSignalsRequestV2, sdp_go.GetItemSignalsResponseV2] - getCustomSignalsByCategory *connect.Client[sdp_go.GetCustomSignalsByCategoryRequest, sdp_go.GetCustomSignalsByCategoryResponse] - getItemSignalDetails *connect.Client[sdp_go.GetItemSignalDetailsRequest, sdp_go.GetItemSignalDetailsResponse] -} - -// AddSignal calls signal.SignalService.AddSignal. -func (c *signalServiceClient) AddSignal(ctx context.Context, req *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) { - return c.addSignal.CallUnary(ctx, req) -} - -// GetSignalsByChangeExternalID calls signal.SignalService.GetSignalsByChangeExternalID. -func (c *signalServiceClient) GetSignalsByChangeExternalID(ctx context.Context, req *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) { - return c.getSignalsByChangeExternalID.CallUnary(ctx, req) -} - -// GetChangeOverviewSignals calls signal.SignalService.GetChangeOverviewSignals. -func (c *signalServiceClient) GetChangeOverviewSignals(ctx context.Context, req *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) { - return c.getChangeOverviewSignals.CallUnary(ctx, req) -} - -// GetItemSignals calls signal.SignalService.GetItemSignals. -func (c *signalServiceClient) GetItemSignals(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) { - return c.getItemSignals.CallUnary(ctx, req) -} - -// GetItemSignalsV2 calls signal.SignalService.GetItemSignalsV2. -func (c *signalServiceClient) GetItemSignalsV2(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) { - return c.getItemSignalsV2.CallUnary(ctx, req) -} - -// GetCustomSignalsByCategory calls signal.SignalService.GetCustomSignalsByCategory. -func (c *signalServiceClient) GetCustomSignalsByCategory(ctx context.Context, req *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) { - return c.getCustomSignalsByCategory.CallUnary(ctx, req) -} - -// GetItemSignalDetails calls signal.SignalService.GetItemSignalDetails. -func (c *signalServiceClient) GetItemSignalDetails(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) { - return c.getItemSignalDetails.CallUnary(ctx, req) -} - -// SignalServiceHandler is an implementation of the signal.SignalService service. -type SignalServiceHandler interface { - // This is an external API to add a signals to a change. - // It will be used by the CLI, the web UI, and other clients. - // It expects the user to provide the properties of the signal, such as name, value, description, and category. - // And it returns the signal that was added, including the machine-generated metadata such as UUID and aggregation ID. - // DOES THIS NEED TO BE UPDATED TO SPECIFY THE LEVEL OF THE SIGNAL? - AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) - // This is an API to get all signals associated with a change by its external ID. It is not used by the frontend. - // It returns a slice/array of Signals. Which includes both the user-facing properties of the signal, and the machine-generated metadata. - // Look at the Signal message for more details. - GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) - // NB for the following we do not need to specify the icons for the Items, as they are derived by the Frontend separately. - // Get all top-level signals for a change. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) - // Get item-level signals for all items in a change. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) - // Get a slice of items, sorted by their aggregate signal value, ascending. - // for each item include - // - an aggregated value for the item, calculated by AggregateSignalScores. - // - a friendly item ref, also before and after - // - a slice of signals for the item, sorted by the signal value, ascending. - // - the status of the item, e.g. "added", "modified", "deleted". - GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) - // Get all custom signals for a change by its external ID and category. They are NOT associated with any item. - // There is no score aggregation or description aggregation at this level. They are aggregated at the GetChangeOverviewSignals level. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) - // Get all signals for attributes/modifications of an item. This will only be used for routineness to start with. - // They are sorted by the signal value, ascending. From minus 5 to plus 5. - GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) -} - -// NewSignalServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewSignalServiceHandler(svc SignalServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - signalServiceMethods := sdp_go.File_signal_proto.Services().ByName("SignalService").Methods() - signalServiceAddSignalHandler := connect.NewUnaryHandler( - SignalServiceAddSignalProcedure, - svc.AddSignal, - connect.WithSchema(signalServiceMethods.ByName("AddSignal")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetSignalsByChangeExternalIDHandler := connect.NewUnaryHandler( - SignalServiceGetSignalsByChangeExternalIDProcedure, - svc.GetSignalsByChangeExternalID, - connect.WithSchema(signalServiceMethods.ByName("GetSignalsByChangeExternalID")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetChangeOverviewSignalsHandler := connect.NewUnaryHandler( - SignalServiceGetChangeOverviewSignalsProcedure, - svc.GetChangeOverviewSignals, - connect.WithSchema(signalServiceMethods.ByName("GetChangeOverviewSignals")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetItemSignalsHandler := connect.NewUnaryHandler( - SignalServiceGetItemSignalsProcedure, - svc.GetItemSignals, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignals")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetItemSignalsV2Handler := connect.NewUnaryHandler( - SignalServiceGetItemSignalsV2Procedure, - svc.GetItemSignalsV2, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignalsV2")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetCustomSignalsByCategoryHandler := connect.NewUnaryHandler( - SignalServiceGetCustomSignalsByCategoryProcedure, - svc.GetCustomSignalsByCategory, - connect.WithSchema(signalServiceMethods.ByName("GetCustomSignalsByCategory")), - connect.WithHandlerOptions(opts...), - ) - signalServiceGetItemSignalDetailsHandler := connect.NewUnaryHandler( - SignalServiceGetItemSignalDetailsProcedure, - svc.GetItemSignalDetails, - connect.WithSchema(signalServiceMethods.ByName("GetItemSignalDetails")), - connect.WithHandlerOptions(opts...), - ) - return "/signal.SignalService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case SignalServiceAddSignalProcedure: - signalServiceAddSignalHandler.ServeHTTP(w, r) - case SignalServiceGetSignalsByChangeExternalIDProcedure: - signalServiceGetSignalsByChangeExternalIDHandler.ServeHTTP(w, r) - case SignalServiceGetChangeOverviewSignalsProcedure: - signalServiceGetChangeOverviewSignalsHandler.ServeHTTP(w, r) - case SignalServiceGetItemSignalsProcedure: - signalServiceGetItemSignalsHandler.ServeHTTP(w, r) - case SignalServiceGetItemSignalsV2Procedure: - signalServiceGetItemSignalsV2Handler.ServeHTTP(w, r) - case SignalServiceGetCustomSignalsByCategoryProcedure: - signalServiceGetCustomSignalsByCategoryHandler.ServeHTTP(w, r) - case SignalServiceGetItemSignalDetailsProcedure: - signalServiceGetItemSignalDetailsHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedSignalServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedSignalServiceHandler struct{} - -func (UnimplementedSignalServiceHandler) AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.AddSignal is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetSignalsByChangeExternalID is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetChangeOverviewSignals is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignals is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignalsV2 is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetCustomSignalsByCategory is not implemented")) -} - -func (UnimplementedSignalServiceHandler) GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignalDetails is not implemented")) -} diff --git a/sdp-go/sdpconnect/snapshots.connect.go b/sdp-go/sdpconnect/snapshots.connect.go deleted file mode 100644 index f787552c..00000000 --- a/sdp-go/sdpconnect/snapshots.connect.go +++ /dev/null @@ -1,254 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: snapshots.proto - -package sdpconnect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - sdp_go "github.com/overmindtech/cli/sdp-go" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // SnapshotsServiceName is the fully-qualified name of the SnapshotsService service. - SnapshotsServiceName = "snapshots.SnapshotsService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // SnapshotsServiceListSnapshotsProcedure is the fully-qualified name of the SnapshotsService's - // ListSnapshots RPC. - SnapshotsServiceListSnapshotsProcedure = "/snapshots.SnapshotsService/ListSnapshots" - // SnapshotsServiceCreateSnapshotProcedure is the fully-qualified name of the SnapshotsService's - // CreateSnapshot RPC. - SnapshotsServiceCreateSnapshotProcedure = "/snapshots.SnapshotsService/CreateSnapshot" - // SnapshotsServiceGetSnapshotProcedure is the fully-qualified name of the SnapshotsService's - // GetSnapshot RPC. - SnapshotsServiceGetSnapshotProcedure = "/snapshots.SnapshotsService/GetSnapshot" - // SnapshotsServiceUpdateSnapshotProcedure is the fully-qualified name of the SnapshotsService's - // UpdateSnapshot RPC. - SnapshotsServiceUpdateSnapshotProcedure = "/snapshots.SnapshotsService/UpdateSnapshot" - // SnapshotsServiceDeleteSnapshotProcedure is the fully-qualified name of the SnapshotsService's - // DeleteSnapshot RPC. - SnapshotsServiceDeleteSnapshotProcedure = "/snapshots.SnapshotsService/DeleteSnapshot" - // SnapshotsServiceListSnapshotByGUNProcedure is the fully-qualified name of the SnapshotsService's - // ListSnapshotByGUN RPC. - SnapshotsServiceListSnapshotByGUNProcedure = "/snapshots.SnapshotsService/ListSnapshotByGUN" -) - -// SnapshotsServiceClient is a client for the snapshots.SnapshotsService service. -type SnapshotsServiceClient interface { - ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) - CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) - GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) - UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) - DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) - ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) -} - -// NewSnapshotsServiceClient constructs a client for the snapshots.SnapshotsService service. By -// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, -// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewSnapshotsServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SnapshotsServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - snapshotsServiceMethods := sdp_go.File_snapshots_proto.Services().ByName("SnapshotsService").Methods() - return &snapshotsServiceClient{ - listSnapshots: connect.NewClient[sdp_go.ListSnapshotsRequest, sdp_go.ListSnapshotResponse]( - httpClient, - baseURL+SnapshotsServiceListSnapshotsProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshots")), - connect.WithClientOptions(opts...), - ), - createSnapshot: connect.NewClient[sdp_go.CreateSnapshotRequest, sdp_go.CreateSnapshotResponse]( - httpClient, - baseURL+SnapshotsServiceCreateSnapshotProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("CreateSnapshot")), - connect.WithClientOptions(opts...), - ), - getSnapshot: connect.NewClient[sdp_go.GetSnapshotRequest, sdp_go.GetSnapshotResponse]( - httpClient, - baseURL+SnapshotsServiceGetSnapshotProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("GetSnapshot")), - connect.WithClientOptions(opts...), - ), - updateSnapshot: connect.NewClient[sdp_go.UpdateSnapshotRequest, sdp_go.UpdateSnapshotResponse]( - httpClient, - baseURL+SnapshotsServiceUpdateSnapshotProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("UpdateSnapshot")), - connect.WithClientOptions(opts...), - ), - deleteSnapshot: connect.NewClient[sdp_go.DeleteSnapshotRequest, sdp_go.DeleteSnapshotResponse]( - httpClient, - baseURL+SnapshotsServiceDeleteSnapshotProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("DeleteSnapshot")), - connect.WithClientOptions(opts...), - ), - listSnapshotByGUN: connect.NewClient[sdp_go.ListSnapshotsByGUNRequest, sdp_go.ListSnapshotsByGUNResponse]( - httpClient, - baseURL+SnapshotsServiceListSnapshotByGUNProcedure, - connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshotByGUN")), - connect.WithClientOptions(opts...), - ), - } -} - -// snapshotsServiceClient implements SnapshotsServiceClient. -type snapshotsServiceClient struct { - listSnapshots *connect.Client[sdp_go.ListSnapshotsRequest, sdp_go.ListSnapshotResponse] - createSnapshot *connect.Client[sdp_go.CreateSnapshotRequest, sdp_go.CreateSnapshotResponse] - getSnapshot *connect.Client[sdp_go.GetSnapshotRequest, sdp_go.GetSnapshotResponse] - updateSnapshot *connect.Client[sdp_go.UpdateSnapshotRequest, sdp_go.UpdateSnapshotResponse] - deleteSnapshot *connect.Client[sdp_go.DeleteSnapshotRequest, sdp_go.DeleteSnapshotResponse] - listSnapshotByGUN *connect.Client[sdp_go.ListSnapshotsByGUNRequest, sdp_go.ListSnapshotsByGUNResponse] -} - -// ListSnapshots calls snapshots.SnapshotsService.ListSnapshots. -func (c *snapshotsServiceClient) ListSnapshots(ctx context.Context, req *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) { - return c.listSnapshots.CallUnary(ctx, req) -} - -// CreateSnapshot calls snapshots.SnapshotsService.CreateSnapshot. -func (c *snapshotsServiceClient) CreateSnapshot(ctx context.Context, req *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) { - return c.createSnapshot.CallUnary(ctx, req) -} - -// GetSnapshot calls snapshots.SnapshotsService.GetSnapshot. -func (c *snapshotsServiceClient) GetSnapshot(ctx context.Context, req *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) { - return c.getSnapshot.CallUnary(ctx, req) -} - -// UpdateSnapshot calls snapshots.SnapshotsService.UpdateSnapshot. -func (c *snapshotsServiceClient) UpdateSnapshot(ctx context.Context, req *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) { - return c.updateSnapshot.CallUnary(ctx, req) -} - -// DeleteSnapshot calls snapshots.SnapshotsService.DeleteSnapshot. -func (c *snapshotsServiceClient) DeleteSnapshot(ctx context.Context, req *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) { - return c.deleteSnapshot.CallUnary(ctx, req) -} - -// ListSnapshotByGUN calls snapshots.SnapshotsService.ListSnapshotByGUN. -func (c *snapshotsServiceClient) ListSnapshotByGUN(ctx context.Context, req *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) { - return c.listSnapshotByGUN.CallUnary(ctx, req) -} - -// SnapshotsServiceHandler is an implementation of the snapshots.SnapshotsService service. -type SnapshotsServiceHandler interface { - ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) - CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) - GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) - UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) - DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) - ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) -} - -// NewSnapshotsServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewSnapshotsServiceHandler(svc SnapshotsServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - snapshotsServiceMethods := sdp_go.File_snapshots_proto.Services().ByName("SnapshotsService").Methods() - snapshotsServiceListSnapshotsHandler := connect.NewUnaryHandler( - SnapshotsServiceListSnapshotsProcedure, - svc.ListSnapshots, - connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshots")), - connect.WithHandlerOptions(opts...), - ) - snapshotsServiceCreateSnapshotHandler := connect.NewUnaryHandler( - SnapshotsServiceCreateSnapshotProcedure, - svc.CreateSnapshot, - connect.WithSchema(snapshotsServiceMethods.ByName("CreateSnapshot")), - connect.WithHandlerOptions(opts...), - ) - snapshotsServiceGetSnapshotHandler := connect.NewUnaryHandler( - SnapshotsServiceGetSnapshotProcedure, - svc.GetSnapshot, - connect.WithSchema(snapshotsServiceMethods.ByName("GetSnapshot")), - connect.WithHandlerOptions(opts...), - ) - snapshotsServiceUpdateSnapshotHandler := connect.NewUnaryHandler( - SnapshotsServiceUpdateSnapshotProcedure, - svc.UpdateSnapshot, - connect.WithSchema(snapshotsServiceMethods.ByName("UpdateSnapshot")), - connect.WithHandlerOptions(opts...), - ) - snapshotsServiceDeleteSnapshotHandler := connect.NewUnaryHandler( - SnapshotsServiceDeleteSnapshotProcedure, - svc.DeleteSnapshot, - connect.WithSchema(snapshotsServiceMethods.ByName("DeleteSnapshot")), - connect.WithHandlerOptions(opts...), - ) - snapshotsServiceListSnapshotByGUNHandler := connect.NewUnaryHandler( - SnapshotsServiceListSnapshotByGUNProcedure, - svc.ListSnapshotByGUN, - connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshotByGUN")), - connect.WithHandlerOptions(opts...), - ) - return "/snapshots.SnapshotsService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case SnapshotsServiceListSnapshotsProcedure: - snapshotsServiceListSnapshotsHandler.ServeHTTP(w, r) - case SnapshotsServiceCreateSnapshotProcedure: - snapshotsServiceCreateSnapshotHandler.ServeHTTP(w, r) - case SnapshotsServiceGetSnapshotProcedure: - snapshotsServiceGetSnapshotHandler.ServeHTTP(w, r) - case SnapshotsServiceUpdateSnapshotProcedure: - snapshotsServiceUpdateSnapshotHandler.ServeHTTP(w, r) - case SnapshotsServiceDeleteSnapshotProcedure: - snapshotsServiceDeleteSnapshotHandler.ServeHTTP(w, r) - case SnapshotsServiceListSnapshotByGUNProcedure: - snapshotsServiceListSnapshotByGUNHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedSnapshotsServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedSnapshotsServiceHandler struct{} - -func (UnimplementedSnapshotsServiceHandler) ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.ListSnapshots is not implemented")) -} - -func (UnimplementedSnapshotsServiceHandler) CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.CreateSnapshot is not implemented")) -} - -func (UnimplementedSnapshotsServiceHandler) GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.GetSnapshot is not implemented")) -} - -func (UnimplementedSnapshotsServiceHandler) UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.UpdateSnapshot is not implemented")) -} - -func (UnimplementedSnapshotsServiceHandler) DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.DeleteSnapshot is not implemented")) -} - -func (UnimplementedSnapshotsServiceHandler) ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.ListSnapshotByGUN is not implemented")) -} diff --git a/sdp-go/sdpws/client.go b/sdp-go/sdpws/client.go deleted file mode 100644 index 90d61608..00000000 --- a/sdp-go/sdpws/client.go +++ /dev/null @@ -1,525 +0,0 @@ -package sdpws - -import ( - "bytes" - "context" - "errors" - "fmt" - "net/http" - "slices" - "sync" - - "github.com/coder/websocket" - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "google.golang.org/protobuf/proto" -) - -// Client is the main driver for all interactions with a SDP/Gateway websocket. -// -// Internally it holds a map of all active requests, which are identified by a -// UUID, to multiplex incoming responses to the correct caller. Note that the -// request methods block until the response is received, so to send multiple -// requests in parallel, call requestor methods in goroutines, e.g. using a conc -// Pool: -// -// ``` -// -// pool := pool.New().WithContext(ctx).WithCancelOnError().WithFirstError() -// pool.Go(func() error { -// items, err := client.Query(ctx, q) -// if err != nil { -// return err -// } -// // do something with items -// } -// // ... -// pool.Wait() -// -// ``` -// -// Alternatively, pass in a GatewayMessageHandler to receive all messages as -// they come in and send messages directly using `Send()` and then `Wait()` for -// all request IDs. -type Client struct { - conn *websocket.Conn - - handler GatewayMessageHandler - - requestMap map[uuid.UUID]chan *sdp.GatewayResponse - requestMapMu sync.RWMutex - - finishedRequestMap map[uuid.UUID]bool - finishedRequestMapCond *sync.Cond - finishedRequestMapMu sync.Mutex - - err error - errMu sync.Mutex - - closed bool - closedMu sync.Mutex - - // receiveCtx is the context for the receive goroutine - // receiveCancel cancels the receive context - // receiveDone signals when receive has finished - receiveCtx context.Context - receiveCancel context.CancelFunc - receiveDone sync.WaitGroup -} - -// Dial connects to the given URL and returns a new Client. Pass nil as handler -// if you do not need per-message callbacks. -// -// To stop the client, cancel the provided context: -// -// ``` -// ctx, cancel := context.WithCancel(context.Background()) -// defer cancel() -// client, err := sdpws.Dial(ctx, gatewayUrl, NewAuthenticatedClient(ctx, otelhttp.DefaultClient), nil) -// ``` -func Dial(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler) (*Client, error) { - return dialImpl(ctx, u, httpClient, handler, true) -} - -// DialBatch connects to the given URL and returns a new Client. Pass nil as -// handler if you do not need per-message callbacks. This method is intended for -// batch processing and sets up opentelemetry propagation. Otherwise this -// equivalent to `Dial()` -func DialBatch(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler) (*Client, error) { - return dialImpl(ctx, u, httpClient, handler, false) -} - -func dialImpl(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler, interactive bool) (*Client, error) { - if httpClient == nil { - httpClient = tracing.HTTPClient() - } - options := &websocket.DialOptions{ - HTTPClient: httpClient, - } - if !interactive { - options.HTTPHeader = http.Header{ - "X-overmind-interactive": []string{"false"}, - } - } - - //nolint: bodyclose // github.com/coder/websocket reads the body internally - conn, _, err := websocket.Dial(ctx, u, options) - if err != nil { - return nil, err - } - - // the default, 32kB is too small for cert bundles and rds-db-cluster-parameter-groups - conn.SetReadLimit(2 * 1024 * 1024) - - c := &Client{ - conn: conn, - handler: handler, - requestMap: make(map[uuid.UUID]chan *sdp.GatewayResponse), - finishedRequestMap: make(map[uuid.UUID]bool), - } - c.finishedRequestMapCond = sync.NewCond(&c.finishedRequestMapMu) - - // Create a dedicated context for receive() that we can cancel independently - c.receiveCtx, c.receiveCancel = context.WithCancel(ctx) - c.receiveDone.Add(1) - go func() { - defer c.receiveDone.Done() - c.receive(c.receiveCtx) - }() - - return c, nil -} - -func (c *Client) receive(ctx context.Context) { - defer tracing.LogRecoverToReturn(ctx, "sdpws.Client.receive") - for { - // Check if context is cancelled before attempting to read - // This prevents lock acquisition failures when context is cancelled during Close() - if ctx.Err() != nil { - // Context is cancelled - exit gracefully without calling abort - // This prevents "failed to acquire lock" errors when Close() is called - // with a cancelled context. The abort() will be called by Close() itself. - return - } - - // Check if client is already closed before attempting to read - // This prevents errors when Close() is called from another goroutine - if c.Closed() { - return - } - - msg := &sdp.GatewayResponse{} - - typ, r, err := c.conn.Reader(ctx) - if err != nil { - // If context is cancelled, this is expected during Close() and we should - // exit gracefully without calling abort to avoid lock contention. - // The abort() will be called by Close() itself. - if ctx.Err() != nil { - // Context cancelled - exit gracefully - // Don't call abort() here as it may already be closing, which could - // cause "failed to acquire lock" errors - return - } - // Check if this is a normal closure from the remote side - var ce websocket.CloseError - if errors.As(err, &ce) && ce.Code == websocket.StatusNormalClosure { - // Normal closure from remote - exit gracefully - // Call abort() with nil to properly set the closed state - // abort() will handle normal closure gracefully (no error logged) - c.abort(ctx, nil) - return - } - // For other errors, abort normally - c.abort(ctx, fmt.Errorf("failed to initialise websocket reader: %w", err)) - return - } - if typ != websocket.MessageBinary { - c.conn.Close(websocket.StatusUnsupportedData, "expected binary message") - c.abort(ctx, fmt.Errorf("expected binary message for protobuf but got: %v", typ)) - return - } - - b := new(bytes.Buffer) - _, err = b.ReadFrom(r) - if err != nil { - c.abort(ctx, fmt.Errorf("failed to read from websocket: %w", err)) - return - } - - err = proto.Unmarshal(b.Bytes(), msg) - if err != nil { - c.abort(ctx, fmt.Errorf("error unmarshalling message: %w", err)) - return - } - - switch msg.GetResponseType().(type) { - case *sdp.GatewayResponse_NewItem: - item := msg.GetNewItem() - if c.handler != nil { - c.handler.NewItem(ctx, item) - } - u, err := uuid.FromBytes(item.GetMetadata().GetSourceQuery().GetUUID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_NewEdge: - edge := msg.GetNewEdge() - if c.handler != nil { - c.handler.NewEdge(ctx, edge) - } - // TODO: edges are not attached to a specific query, so we can't send them to a request channel - // maybe that's not a problem anyways? - // c, ok := c.getRequestChan(uuid.UUID(edge.Metadata.SourceQuery.UUID)) - // if ok { - // c <- msg - // } - - case *sdp.GatewayResponse_Status: - status := msg.GetStatus() - if c.handler != nil { - c.handler.Status(ctx, status) - } - - case *sdp.GatewayResponse_QueryError: - qe := msg.GetQueryError() - if c.handler != nil { - c.handler.QueryError(ctx, qe) - } - u, err := uuid.FromBytes(qe.GetUUID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_DeleteItem: - item := msg.GetDeleteItem() - if c.handler != nil { - c.handler.DeleteItem(ctx, item) - } - - case *sdp.GatewayResponse_DeleteEdge: - edge := msg.GetDeleteEdge() - if c.handler != nil { - c.handler.DeleteEdge(ctx, edge) - } - - case *sdp.GatewayResponse_UpdateItem: - item := msg.GetUpdateItem() - if c.handler != nil { - c.handler.UpdateItem(ctx, item) - } - - case *sdp.GatewayResponse_SnapshotStoreResult: - result := msg.GetSnapshotStoreResult() - if c.handler != nil { - c.handler.SnapshotStoreResult(ctx, result) - } - u, err := uuid.FromBytes(result.GetMsgID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_SnapshotLoadResult: - result := msg.GetSnapshotLoadResult() - if c.handler != nil { - c.handler.SnapshotLoadResult(ctx, result) - } - u, err := uuid.FromBytes(result.GetMsgID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_BookmarkStoreResult: - result := msg.GetBookmarkStoreResult() - if c.handler != nil { - c.handler.BookmarkStoreResult(ctx, result) - } - u, err := uuid.FromBytes(result.GetMsgID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_BookmarkLoadResult: - result := msg.GetBookmarkLoadResult() - if c.handler != nil { - c.handler.BookmarkLoadResult(ctx, result) - } - u, err := uuid.FromBytes(result.GetMsgID()) - if err == nil { - c.postRequestChan(u, msg) - } - - case *sdp.GatewayResponse_QueryStatus: - qs := msg.GetQueryStatus() - if c.handler != nil { - c.handler.QueryStatus(ctx, qs) - } - u, err := uuid.FromBytes(qs.GetUUID()) - if err == nil { - c.postRequestChan(u, msg) - } - - switch qs.GetStatus() { //nolint: exhaustive // ignore sdp.QueryStatus_UNSPECIFIED, sdp.QueryStatus_STARTED - case sdp.QueryStatus_FINISHED, sdp.QueryStatus_CANCELLED, sdp.QueryStatus_ERRORED: - c.finishRequestChan(u) - } - - case *sdp.GatewayResponse_ChatResponse: - chatResponse := msg.GetChatResponse() - if c.handler != nil { - c.handler.ChatResponse(ctx, chatResponse) - } - c.postRequestChan(uuid.Nil, msg) - - case *sdp.GatewayResponse_ToolStart: - toolStart := msg.GetToolStart() - if c.handler != nil { - c.handler.ToolStart(ctx, toolStart) - } - c.postRequestChan(uuid.Nil, msg) - - case *sdp.GatewayResponse_ToolFinish: - toolFinish := msg.GetToolFinish() - if c.handler != nil { - c.handler.ToolFinish(ctx, toolFinish) - } - c.postRequestChan(uuid.Nil, msg) - - default: - log.WithContext(ctx).WithField("response", msg).WithField("responseType", fmt.Sprintf("%T", msg.GetResponseType())).Warn("unexpected response") - } - } -} - -func (c *Client) send(ctx context.Context, msg *sdp.GatewayRequest) error { - buf, err := proto.Marshal(msg) - if err != nil { - log.WithContext(ctx).WithError(err).WithField("request", msg).Trace("error marshaling request") - c.abort(ctx, err) - return err - } - - err = c.conn.Write(ctx, websocket.MessageBinary, buf) - if err != nil { - log.WithContext(ctx).WithError(err).WithField("request", msg).Trace("error writing request to websocket") - c.abort(ctx, err) - return err - } - return nil -} - -// Wait blocks until all specified requests have been finished. Waiting on a -// closed client returns immediately with no error. -func (c *Client) Wait(ctx context.Context, reqIDs uuid.UUIDs) error { - for { - if c.Closed() { - return nil - } - - // check for context cancellation - if ctx.Err() != nil { - return ctx.Err() - } - - // wrap this in a function so defers can be called (otherwise the lock is held for all loop iterations) - finished := func() bool { - c.finishedRequestMapMu.Lock() - defer c.finishedRequestMapMu.Unlock() - - // remove all finished requests from the list of requests to wait for - reqIDs = slices.DeleteFunc(reqIDs, func(reqID uuid.UUID) bool { - _, ok := c.finishedRequestMap[reqID] - return ok - }) - if len(reqIDs) == 0 { - return true - } - - c.finishedRequestMapCond.Wait() - return false - }() - - if finished { - return nil - } - } -} - -// abort stores the specified error and closes the connection. -func (c *Client) abort(ctx context.Context, err error) { - c.closedMu.Lock() - if c.closed { - c.closedMu.Unlock() - return - } - c.closed = true - c.closedMu.Unlock() - - isNormalClosure := false - var ce websocket.CloseError - if errors.As(err, &ce) { - // tear down the connection without a new error if this is a regular close - isNormalClosure = ce.Code == websocket.StatusNormalClosure - } - - if err != nil && !isNormalClosure { - log.WithContext(ctx).WithError(err).Error("aborting client") - } - c.errMu.Lock() - c.err = errors.Join(c.err, err) - c.errMu.Unlock() - - // Cancel the receive context to stop receive() from reading more messages. - if c.receiveCancel != nil { - c.receiveCancel() - } - - // call this outside of the lock to avoid deadlock should other parts of the - // code try to call abort() when crashing out of a read or write - // Only close the connection if it exists (may be nil in test scenarios) - var closeErr error - if c.conn != nil { - closeErr = c.conn.Close(websocket.StatusNormalClosure, "normal closure") - } - - c.errMu.Lock() - c.err = errors.Join(c.err, closeErr) - c.errMu.Unlock() - - c.closeAllRequestChans() -} - -// Close closes the connection and returns any errors from the underlying connection. -func (c *Client) Close(ctx context.Context) error { - // Cancel the receive context first to stop receive() from reading more messages - if c.receiveCancel != nil { - c.receiveCancel() - } - - // Wait for receive() to finish reading/sending its last message before closing channels. - // This prevents race conditions where we close channels while receive() is still trying - // to send to them. We do this before calling abort() to ensure receive() has finished. - if c.receiveCancel != nil { - c.receiveDone.Wait() - } - - c.abort(ctx, nil) - - c.errMu.Lock() - defer c.errMu.Unlock() - return c.err -} - -func (c *Client) Closed() bool { - c.closedMu.Lock() - defer c.closedMu.Unlock() - return c.closed -} - -func (c *Client) createRequestChan(u uuid.UUID) chan *sdp.GatewayResponse { - r := make(chan *sdp.GatewayResponse, 1) - c.requestMapMu.Lock() - defer c.requestMapMu.Unlock() - c.requestMap[u] = r - return r -} - -func (c *Client) postRequestChan(u uuid.UUID, msg *sdp.GatewayResponse) { - c.requestMapMu.RLock() - r, ok := c.requestMap[u] - c.requestMapMu.RUnlock() - - if !ok { - return - } - - // Check if client is closed before sending. If closed, channels may be closed, - // so we should not attempt to send. With proper context handling, receive() will - // have finished before channels are closed, but we check here as a safety measure. - if c.Closed() { - return - } - - // Use select with receive context to avoid blocking when context is cancelled. - // This prevents deadlock where receive() is blocked on send while Close() is waiting for it. - // During normal operation (context not cancelled), the send case will be selected, - // ensuring no messages are dropped. When context is cancelled, we use non-blocking send. - select { - case <-c.receiveCtx.Done(): - return - case r <- msg: - // Successfully sent (normal operation - blocking until receiver reads) - return - } -} - -func (c *Client) finishRequestChan(u uuid.UUID) { - c.requestMapMu.Lock() - defer c.requestMapMu.Unlock() - - c.finishedRequestMapMu.Lock() - defer c.finishedRequestMapMu.Unlock() - - delete(c.requestMap, u) - c.finishedRequestMap[u] = true - c.finishedRequestMapCond.Broadcast() -} - -func (c *Client) closeAllRequestChans() { - c.requestMapMu.Lock() - defer c.requestMapMu.Unlock() - - c.finishedRequestMapMu.Lock() - defer c.finishedRequestMapMu.Unlock() - - for k, v := range c.requestMap { - close(v) - c.finishedRequestMap[k] = true - } - // clear the map to free up memory - c.requestMap = map[uuid.UUID]chan *sdp.GatewayResponse{} - c.finishedRequestMapCond.Broadcast() -} diff --git a/sdp-go/sdpws/client_test.go b/sdp-go/sdpws/client_test.go deleted file mode 100644 index f4bde01a..00000000 --- a/sdp-go/sdpws/client_test.go +++ /dev/null @@ -1,1064 +0,0 @@ -package sdpws - -import ( - "bytes" - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "sync" - "testing" - "time" - - "github.com/coder/websocket" - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "go.uber.org/goleak" - "google.golang.org/protobuf/proto" -) - -// Helper function to check if a slice contains a string -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -// TestServer is a test server for the websocket client. Note that this can only -// handle a single connection at a time. -type testServer struct { - url string - - conn *websocket.Conn - connMu sync.Mutex - - requests []*sdp.GatewayRequest - requestsMu sync.Mutex -} - -func newTestServer(_ context.Context, t *testing.T) (*testServer, func()) { - ts := &testServer{ - requests: make([]*sdp.GatewayRequest, 0), - } - - serveMux := http.NewServeMux() - serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return - } - defer func() { - _ = c.Close(websocket.StatusNormalClosure, "") - }() - - ts.connMu.Lock() - ts.conn = c - ts.connMu.Unlock() - - // ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) - // defer cancel() - - for { - msg := &sdp.GatewayRequest{} - - typ, reader, err := c.Reader(r.Context()) - if err != nil { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("failed to initialise websocket reader: %v", err)) - return - } - if typ != websocket.MessageBinary { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("expected binary message for protobuf but got: %v", typ)) - t.Fatalf("expected binary message for protobuf but got: %v", typ) - return - } - - b := new(bytes.Buffer) - _, err = b.ReadFrom(reader) - if err != nil { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("failed to read from websocket: %v", err)) - t.Fatalf("failed to read from websocket: %v", err) - return - } - - err = proto.Unmarshal(b.Bytes(), msg) - if err != nil { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error un marshaling message: %v", err)) - t.Fatalf("error un marshaling message: %v", err) - return - } - - ts.requestsMu.Lock() - ts.requests = append(ts.requests, msg) - ts.requestsMu.Unlock() - } - }) - - s := httptest.NewServer(serveMux) - ts.url = s.URL - - return ts, func() { - s.Close() - } -} - -func (ts *testServer) inject(ctx context.Context, msg *sdp.GatewayResponse) { - ts.connMu.Lock() - c := ts.conn - ts.connMu.Unlock() - - buf, err := proto.Marshal(msg) - if err != nil { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error marshaling message: %v", err)) - return - } - - err = c.Write(ctx, websocket.MessageBinary, buf) - if err != nil { - c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error writing message: %v", err)) - return - } -} - -func TestClient(t *testing.T) { - defer goleak.VerifyNone(t) - - t.Run("Query", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - u := uuid.New() - - q := &sdp.Query{ - UUID: u[:], - Type: "", - Method: 0, - Query: "", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "", - IgnoreCache: false, - } - - go func() { - time.Sleep(100 * time.Millisecond) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: u[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - }() - - // this will block until the above goroutine has injected the response - _, err = c.QueryOne(ctx, q) - if err != nil { - t.Fatal(err) - } - err = c.Wait(ctx, uuid.UUIDs{u}) - if err != nil { - t.Fatal(err) - } - - ts.requestsMu.Lock() - defer ts.requestsMu.Unlock() - - if len(ts.requests) != 1 { - t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) - } - - recvQ, ok := ts.requests[0].GetRequestType().(*sdp.GatewayRequest_Query) - if !ok || uuid.UUID(recvQ.Query.GetUUID()) != u { - t.Fatalf("expected query, got %v", ts.requests[0]) - } - }) - - t.Run("QueryNotFound", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - u := uuid.New() - - q := &sdp.Query{ - UUID: u[:], - Type: "", - Method: 0, - Query: "", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "", - IgnoreCache: false, - } - - go func() { - time.Sleep(100 * time.Millisecond) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: u[:], - Status: sdp.QueryStatus_STARTED, - }, - }, - }) - time.Sleep(100 * time.Millisecond) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryError{ - QueryError: &sdp.QueryError{ - UUID: u[:], - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "not found", - Scope: "scope", - SourceName: "src name", - ItemType: "item type", - ResponderName: "responder name", - }, - }, - }) - time.Sleep(100 * time.Millisecond) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: u[:], - Status: sdp.QueryStatus_ERRORED, - }, - }, - }) - }() - - // this will block until the above goroutine has injected the response - _, err = c.QueryOne(ctx, q) - if err != nil { - t.Fatal(err) - } - err = c.Wait(ctx, uuid.UUIDs{u}) - if err != nil { - t.Fatal(err) - } - - ts.requestsMu.Lock() - defer ts.requestsMu.Unlock() - - if len(ts.requests) != 1 { - t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) - } - - recvQ, ok := ts.requests[0].GetRequestType().(*sdp.GatewayRequest_Query) - if !ok || uuid.UUID(recvQ.Query.GetUUID()) != u { - t.Fatalf("expected query, got %v", ts.requests[0]) - } - }) - - t.Run("StoreSnapshot", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - u := uuid.New() - - go func() { - time.Sleep(100 * time.Millisecond) - ts.requestsMu.Lock() - msgID := ts.requests[0].GetStoreSnapshot().GetMsgID() - ts.requestsMu.Unlock() - - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_SnapshotStoreResult{ - SnapshotStoreResult: &sdp.SnapshotStoreResult{ - Success: true, - ErrorMessage: "", - MsgID: msgID, - SnapshotID: u[:], - }, - }, - }) - }() - - // this will block until the above goroutine has injected the response - snapu, err := c.StoreSnapshot(ctx, "name", "description") - if err != nil { - t.Fatal(err) - } - if snapu != u { - t.Errorf("expected snapshot id %v, got %v", u, snapu) - } - - ts.requestsMu.Lock() - defer ts.requestsMu.Unlock() - - if len(ts.requests) != 1 { - t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) - } - }) - - t.Run("StoreBookmark", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - u := uuid.New() - - go func() { - time.Sleep(100 * time.Millisecond) - ts.requestsMu.Lock() - msgID := ts.requests[0].GetStoreBookmark().GetMsgID() - ts.requestsMu.Unlock() - - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_BookmarkStoreResult{ - BookmarkStoreResult: &sdp.BookmarkStoreResult{ - Success: true, - ErrorMessage: "", - MsgID: msgID, - BookmarkID: u[:], - }, - }, - }) - }() - - // this will block until the above goroutine has injected the response - snapu, err := c.StoreBookmark(ctx, "name", "description", true) - if err != nil { - t.Fatal(err) - } - if snapu != u { - t.Errorf("expected bookmark id %v, got %v", u, snapu) - } - - ts.requestsMu.Lock() - defer ts.requestsMu.Unlock() - - if len(ts.requests) != 1 { - t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) - } - }) - - t.Run("ConcurrentQueries", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - // Create multiple queries with different UUIDs - numQueries := 5 - queries := make([]*sdp.Query, numQueries) - expectedItems := make(map[string]*sdp.Item) - - for i := range numQueries { - u := uuid.New() - queries[i] = &sdp.Query{ - UUID: u[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: fmt.Sprintf("query-%d", i), - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - // Create expected items that should be returned for each query - expectedItems[u.String()] = &sdp.Item{ - Type: "test", - UniqueAttribute: fmt.Sprintf("item-%d", i), - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: queries[i], - }, - } - } - - // Inject responses in a different order than queries to test proper routing - go func() { - time.Sleep(50 * time.Millisecond) - - // Send responses in reverse order to test UUID-based routing - for i := numQueries - 1; i >= 0; i-- { - u := uuid.UUID(queries[i].GetUUID()) - - // Send an item response first - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: expectedItems[u.String()], - }, - }) - - // Then send the completion status - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: u[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - - // Add a small delay between responses to make race conditions more likely - time.Sleep(10 * time.Millisecond) - } - }() - - // Execute all queries concurrently - type queryResult struct { - index int - items []*sdp.Item - err error - } - - results := make([]queryResult, numQueries) - var wg sync.WaitGroup - - for i := range numQueries { - wg.Add(1) - go func(index int) { - defer wg.Done() - items, err := c.QueryOne(ctx, queries[index]) - results[index] = queryResult{ - index: index, - items: items, - err: err, - } - }(i) - } - - wg.Wait() - - // Verify that each query got the correct response - for i, result := range results { - if result.err != nil { - t.Errorf("Query %d failed: %v", i, result.err) - continue - } - - if len(result.items) != 1 { - t.Errorf("Query %d: expected 1 item, got %d", i, len(result.items)) - continue - } - - receivedItem := result.items[0] - expectedUniqueAttr := fmt.Sprintf("item-%d", i) - - if receivedItem.GetUniqueAttribute() != expectedUniqueAttr { - t.Errorf("Query %d: expected item with unique attribute %s, got %s", - i, expectedUniqueAttr, receivedItem.GetUniqueAttribute()) - } - - // Verify the item's metadata contains the correct source query - if receivedItem.GetMetadata() == nil || receivedItem.GetMetadata().GetSourceQuery() == nil { - t.Errorf("Query %d: item missing metadata or source query", i) - continue - } - - sourceQueryUUID := uuid.UUID(receivedItem.GetMetadata().GetSourceQuery().GetUUID()) - expectedUUID := uuid.UUID(queries[i].GetUUID()) - - if sourceQueryUUID != expectedUUID { - t.Errorf("Query %d: expected source query UUID %s, got %s", - i, expectedUUID, sourceQueryUUID) - } - } - - // Verify that the server received all queries - ts.requestsMu.Lock() - defer ts.requestsMu.Unlock() - - if len(ts.requests) != numQueries { - t.Fatalf("expected %d requests, got %d", numQueries, len(ts.requests)) - } - }) - - t.Run("ResponseMixupPrevention", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - // Create two queries with different UUIDs - query1UUID := uuid.New() - query2UUID := uuid.New() - - query1 := &sdp.Query{ - UUID: query1UUID[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: "query-1", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - query2 := &sdp.Query{ - UUID: query2UUID[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: "query-2", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - // Items that should be returned for each query - item1 := &sdp.Item{ - Type: "test", - UniqueAttribute: "item-for-query-1", - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: query1, - }, - } - - item2 := &sdp.Item{ - Type: "test", - UniqueAttribute: "item-for-query-2", - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: query2, - }, - } - - // Inject responses in a way that could cause mixup if UUIDs aren't handled correctly - go func() { - time.Sleep(50 * time.Millisecond) - - // Send responses for query2 first, then query1 - // If the client doesn't properly route by UUID, responses could get mixed up - - // Send multiple items for query2 - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: item2, - }, - }) - - // Send an item for query1 - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: item1, - }, - }) - - // Send another item for query2 to test multiple items per query - item2_duplicate := &sdp.Item{ - Type: "test", - UniqueAttribute: "item-for-query-2-duplicate", - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: query2, - }, - } - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: item2_duplicate, - }, - }) - - // Complete query1 first (even though we sent its response second) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: query1UUID[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - - // Complete query2 after query1 - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: query2UUID[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - }() - - // Execute both queries concurrently - type result struct { - items []*sdp.Item - err error - } - - var wg sync.WaitGroup - results := make([]result, 2) - - wg.Add(1) - go func() { - defer wg.Done() - items, err := c.QueryOne(ctx, query1) - results[0] = result{items: items, err: err} - }() - - wg.Add(1) - go func() { - defer wg.Done() - items, err := c.QueryOne(ctx, query2) - results[1] = result{items: items, err: err} - }() - - wg.Wait() - - // Verify query1 got the correct response - if results[0].err != nil { - t.Errorf("Query 1 failed: %v", results[0].err) - } else { - if len(results[0].items) != 1 { - t.Errorf("Query 1: expected 1 item, got %d", len(results[0].items)) - } else if results[0].items[0].GetUniqueAttribute() != "item-for-query-1" { - t.Errorf("Query 1: got wrong item: %s", results[0].items[0].GetUniqueAttribute()) - } - } - - // Verify query2 got the correct responses - if results[1].err != nil { - t.Errorf("Query 2 failed: %v", results[1].err) - } else { - if len(results[1].items) != 2 { - t.Errorf("Query 2: expected 2 items, got %d", len(results[1].items)) - } else { - // Check that both items are for query2 - for i, item := range results[1].items { - if !contains([]string{"item-for-query-2", "item-for-query-2-duplicate"}, item.GetUniqueAttribute()) { - t.Errorf("Query 2, item %d: got wrong item: %s", i, item.GetUniqueAttribute()) - } - } - } - } - }) - - t.Run("UUIDRoutingValidation", func(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := context.Background() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - // This test validates that responses are properly routed by UUID - // If the client were reading responses in order (FIFO) instead of by UUID, - // this test would fail because we send responses out of order - - queryA_UUID := uuid.New() - queryB_UUID := uuid.New() - - queryA := &sdp.Query{ - UUID: queryA_UUID[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: "query-A", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - queryB := &sdp.Query{ - UUID: queryB_UUID[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: "query-B", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - // Items that should be returned for each query - itemA := &sdp.Item{ - Type: "test", - UniqueAttribute: "item-A", - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: queryA, - }, - } - - itemB := &sdp.Item{ - Type: "test", - UniqueAttribute: "item-B", - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: queryB, - }, - } - - // Inject responses deliberately out of order - go func() { - time.Sleep(50 * time.Millisecond) - - // Send itemB first (for queryB), then itemA (for queryA) - // If the client doesn't route by UUID, queryA might get itemB - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: itemB, - }, - }) - - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: itemA, - }, - }) - - // Complete queryA first (even though itemA was sent second) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: queryA_UUID[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - - // Complete queryB second - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: queryB_UUID[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - }() - - // Execute queryA - it should get itemA despite itemB being sent first - var wg sync.WaitGroup - type result struct { - items []*sdp.Item - err error - } - - resultsA := make([]result, 1) - resultsB := make([]result, 1) - - wg.Add(1) - go func() { - defer wg.Done() - items, err := c.QueryOne(ctx, queryA) - resultsA[0] = result{items: items, err: err} - }() - - wg.Add(1) - go func() { - defer wg.Done() - items, err := c.QueryOne(ctx, queryB) - resultsB[0] = result{items: items, err: err} - }() - - wg.Wait() - - // Verify queryA got the correct item - if resultsA[0].err != nil { - t.Fatalf("Query A failed: %v", resultsA[0].err) - } - - if len(resultsA[0].items) != 1 { - t.Fatalf("Query A: expected 1 item, got %d", len(resultsA[0].items)) - } - - if resultsA[0].items[0].GetUniqueAttribute() != "item-A" { - t.Errorf("Query A got wrong item: expected 'item-A', got '%s'", resultsA[0].items[0].GetUniqueAttribute()) - } - - // Verify queryB got the correct item - if resultsB[0].err != nil { - t.Fatalf("Query B failed: %v", resultsB[0].err) - } - - if len(resultsB[0].items) != 1 { - t.Fatalf("Query B: expected 1 item, got %d", len(resultsB[0].items)) - } - - if resultsB[0].items[0].GetUniqueAttribute() != "item-B" { - t.Errorf("Query B got wrong item: expected 'item-B', got '%s'", resultsB[0].items[0].GetUniqueAttribute()) - } - }) -} - -// TestRaceConditionOnClose stresses the shutdown path around the historical -// "send on closed channel" bug. Originally, there was a race where: -// 1. postRequestChan is called and acquires the read lock -// 2. postRequestChan looks up a channel and prepares to send -// 3. Another goroutine calls closeAllRequestChans(), which closes all channels -// 4. postRequestChan tries to send on the now-closed channel and panics -// -// The implementation has since been fixed by eliminating this race via proper -// synchronization (e.g. context cancellation followed by waiting for the -// relevant goroutines to finish) instead of relying on recover(). This test -// verifies that no "send on closed channel" panics escape to the caller under -// concurrent activity and that the shutdown logic behaves correctly. -// -// This test is expected to pass cleanly when run with the Go race detector -// enabled (go test -race). It may be run multiple times to increase stress: -// go test -run TestRaceConditionOnClose -race -v -count=100 -func TestRaceConditionOnClose(t *testing.T) { - // Skip on CI to avoid flaky tests in automated environments - if os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" { - t.Skip("Skipping race condition test in CI environment") - } - - // Skip goleak for this stress test as we're intentionally testing race conditions - - ctx := t.Context() - - // Run many iterations to increase probability of hitting the race condition - // The race happens when postRequestChan and closeAllRequestChans run concurrently - iterations := 1000 - panics := 0 - - for iteration := range iterations { - func() { - // Create a minimal client with just the necessary fields - c := &Client{ - requestMap: make(map[uuid.UUID]chan *sdp.GatewayResponse), - finishedRequestMap: make(map[uuid.UUID]bool), - } - c.finishedRequestMapCond = sync.NewCond(&c.finishedRequestMapMu) - - // Initialize context handling to properly test the mechanism - // This simulates what Dial() does - c.receiveCtx, c.receiveCancel = context.WithCancel(ctx) - - // Create multiple request channels to increase race probability - numChannels := 10 - uuids := make([]uuid.UUID, numChannels) - for i := range numChannels { - uuids[i] = uuid.New() - ch := make(chan *sdp.GatewayResponse, 1) - c.requestMapMu.Lock() - c.requestMap[uuids[i]] = ch - c.requestMapMu.Unlock() - } - - // Create a message to send - msg := &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: &sdp.Item{ - Type: "test", - UniqueAttribute: fmt.Sprintf("item-%d", iteration), - Scope: "test", - }, - }, - } - - // Use a wait group to coordinate concurrent operations - var wg sync.WaitGroup - panicChan := make(chan bool, numChannels*2) - - // Start a simulated receive() goroutine that will call postRequestChan - // This simulates the real receive() behavior where it processes messages - // and calls postRequestChan() until the context is cancelled - c.receiveDone.Add(1) - go func() { - defer c.receiveDone.Done() - // Simulate receive() processing messages and calling postRequestChan - // It will be cancelled by Close() and should stop before channels are closed - for i := range 1000 { - // Check context before processing each message (like real receive() does) - if c.receiveCtx.Err() != nil { - // Context cancelled - exit gracefully (simulating receive() behavior) - return - } - // Simulate receive() processing a message and calling postRequestChan - // Use a random UUID to simulate different queries - // Wrap in recover() to catch any panics (shouldn't happen with proper context handling) - func() { - defer func() { - if r := recover(); r != nil { - panicChan <- true - } - }() - u := uuids[i%numChannels] - c.postRequestChan(u, msg) - }() - time.Sleep(time.Nanosecond) - } - }() - - // Start a goroutine that calls Close() concurrently - // Close() will cancel the receive context, wait for receive() to finish, - // and then close channels. This ensures receive() stops before channels are closed. - wg.Add(1) - go func() { - defer wg.Done() - // Wait a tiny bit to let some postRequestChan calls start - time.Sleep(time.Microsecond * 10) - // Use Close() which properly cancels receive context and waits before closing channels - _ = c.Close(ctx) - }() - - // Wait for all goroutines to complete - wg.Wait() - close(panicChan) - - // Count panics - for range panicChan { - panics++ - } - }() - } - - t.Logf("Successfully completed all %d iterations", iterations) - if panics > 0 { - t.Errorf("Detected %d panics - with proper context handling, receive() should stop before channels are closed, preventing panics. Panics indicate the context handling is not working correctly.", panics) - } else { - t.Logf("No panics detected - context handling is working correctly. receive() stops before channels are closed, preventing 'send on closed channel' panics") - } -} - -// TestNoMessageDroppingDuringNormalOperation verifies that messages are not -// dropped during normal operation when items arrive faster than they can be read. -// This test sends many items rapidly and verifies that all items are received. -func TestNoMessageDroppingDuringNormalOperation(t *testing.T) { - defer goleak.VerifyNone(t) - ctx := t.Context() - - ts, closeFn := newTestServer(ctx, t) - defer closeFn() - - c, err := Dial(ctx, ts.url, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = c.Close(ctx) - }() - - u := uuid.New() - q := &sdp.Query{ - UUID: u[:], - Type: "test", - Method: sdp.QueryMethod_GET, - Query: "query", - RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, - Scope: "test", - IgnoreCache: false, - } - - // Send many items rapidly - more than the channel buffer size (1) - // to test that blocking send works correctly and no messages are dropped - numItems := 100 - expectedItems := make([]*sdp.Item, numItems) - for i := range numItems { - expectedItems[i] = &sdp.Item{ - Type: "test", - UniqueAttribute: fmt.Sprintf("item-%d", i), - Scope: "test", - Metadata: &sdp.Metadata{ - SourceQuery: q, - }, - } - } - - // Inject all items rapidly, then the completion status - go func() { - time.Sleep(50 * time.Millisecond) - // Send all items as fast as possible - for i := range numItems { - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_NewItem{ - NewItem: expectedItems[i], - }, - }) - } - // Then send the completion status - time.Sleep(10 * time.Millisecond) - ts.inject(ctx, &sdp.GatewayResponse{ - ResponseType: &sdp.GatewayResponse_QueryStatus{ - QueryStatus: &sdp.QueryStatus{ - UUID: u[:], - Status: sdp.QueryStatus_FINISHED, - }, - }, - }) - }() - - // QueryOne should receive all items, not drop any - items, err := c.QueryOne(ctx, q) - if err != nil { - t.Fatalf("QueryOne failed: %v", err) - } - - if len(items) != numItems { - t.Errorf("Expected %d items, got %d. Messages were dropped!", numItems, len(items)) - } - - // Verify we got all the expected items - receivedAttrs := make(map[string]bool) - for _, item := range items { - receivedAttrs[item.GetUniqueAttribute()] = true - } - - for i := range numItems { - expectedAttr := fmt.Sprintf("item-%d", i) - if !receivedAttrs[expectedAttr] { - t.Errorf("Missing expected item: %s", expectedAttr) - } - } -} diff --git a/sdp-go/sdpws/messagehandler.go b/sdp-go/sdpws/messagehandler.go deleted file mode 100644 index f4c52f61..00000000 --- a/sdp-go/sdpws/messagehandler.go +++ /dev/null @@ -1,191 +0,0 @@ -package sdpws - -import ( - "context" - - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" -) - -// GatewayMessageHandler is an interface that can be implemented to handle -// messages from the gateway. The individual methods are called when the sdpws -// client receives a message from the gateway. Methods are called in the same -// order as the messages are received from the gateway. The sdpws client -// guarantees that the methods are called in a single thread, so no locking is -// needed. -type GatewayMessageHandler interface { - NewItem(context.Context, *sdp.Item) - NewEdge(context.Context, *sdp.Edge) - Status(context.Context, *sdp.GatewayRequestStatus) - Error(context.Context, string) - QueryError(context.Context, *sdp.QueryError) - DeleteItem(context.Context, *sdp.Reference) - DeleteEdge(context.Context, *sdp.Edge) - UpdateItem(context.Context, *sdp.Item) - SnapshotStoreResult(context.Context, *sdp.SnapshotStoreResult) - SnapshotLoadResult(context.Context, *sdp.SnapshotLoadResult) - BookmarkStoreResult(context.Context, *sdp.BookmarkStoreResult) - BookmarkLoadResult(context.Context, *sdp.BookmarkLoadResult) - QueryStatus(context.Context, *sdp.QueryStatus) - ChatResponse(context.Context, *sdp.ChatResponse) - ToolStart(context.Context, *sdp.ToolStart) - ToolFinish(context.Context, *sdp.ToolFinish) -} - -type LoggingGatewayMessageHandler struct { - Level log.Level -} - -// assert that LoggingGatewayMessageHandler implements GatewayMessageHandler -var _ GatewayMessageHandler = (*LoggingGatewayMessageHandler)(nil) - -func (l *LoggingGatewayMessageHandler) NewItem(ctx context.Context, item *sdp.Item) { - log.WithContext(ctx).WithField("item", item).Log(l.Level, "received new item") -} - -func (l *LoggingGatewayMessageHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { - log.WithContext(ctx).WithField("edge", edge).Log(l.Level, "received new edge") -} - -func (l *LoggingGatewayMessageHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { - log.WithContext(ctx).WithField("status", status.GetSummary()).Log(l.Level, "received status") -} - -func (l *LoggingGatewayMessageHandler) Error(ctx context.Context, errorMessage string) { - log.WithContext(ctx).WithField("errorMessage", errorMessage).Log(l.Level, "received error") -} - -func (l *LoggingGatewayMessageHandler) QueryError(ctx context.Context, queryError *sdp.QueryError) { - log.WithContext(ctx).WithField("queryError", queryError).Log(l.Level, "received query error") -} - -func (l *LoggingGatewayMessageHandler) DeleteItem(ctx context.Context, reference *sdp.Reference) { - log.WithContext(ctx).WithField("reference", reference).Log(l.Level, "received delete item") -} - -func (l *LoggingGatewayMessageHandler) DeleteEdge(ctx context.Context, edge *sdp.Edge) { - log.WithContext(ctx).WithField("edge", edge).Log(l.Level, "received delete edge") -} - -func (l *LoggingGatewayMessageHandler) UpdateItem(ctx context.Context, item *sdp.Item) { - log.WithContext(ctx).WithField("item", item).Log(l.Level, "received updated item") -} - -func (l *LoggingGatewayMessageHandler) SnapshotStoreResult(ctx context.Context, result *sdp.SnapshotStoreResult) { - log.WithContext(ctx).WithField("result", result).Log(l.Level, "received snapshot store result") -} - -func (l *LoggingGatewayMessageHandler) SnapshotLoadResult(ctx context.Context, result *sdp.SnapshotLoadResult) { - log.WithContext(ctx).WithField("result", result).Log(l.Level, "received snapshot load result") -} - -func (l *LoggingGatewayMessageHandler) BookmarkStoreResult(ctx context.Context, result *sdp.BookmarkStoreResult) { - log.WithContext(ctx).WithField("result", result).Log(l.Level, "received bookmark store result") -} - -func (l *LoggingGatewayMessageHandler) BookmarkLoadResult(ctx context.Context, result *sdp.BookmarkLoadResult) { - log.WithContext(ctx).WithField("result", result).Log(l.Level, "received bookmark load result") -} - -func (l *LoggingGatewayMessageHandler) QueryStatus(ctx context.Context, status *sdp.QueryStatus) { - log.WithContext(ctx).WithField("status", status).WithField("uuid", status.GetUUIDParsed()).Log(l.Level, "received query status") -} - -func (l *LoggingGatewayMessageHandler) ChatResponse(ctx context.Context, chatResponse *sdp.ChatResponse) { - log.WithContext(ctx).WithField("chatResponse", chatResponse).Log(l.Level, "received chat response") -} - -func (l *LoggingGatewayMessageHandler) ToolStart(ctx context.Context, toolStart *sdp.ToolStart) { - log.WithContext(ctx).WithField("toolStart", toolStart).Log(l.Level, "received tool start") -} - -func (l *LoggingGatewayMessageHandler) ToolFinish(ctx context.Context, toolFinish *sdp.ToolFinish) { - log.WithContext(ctx).WithField("toolFinish", toolFinish).Log(l.Level, "received tool finish") -} - -type NoopGatewayMessageHandler struct{} - -// assert that NoopGatewayMessageHandler implements GatewayMessageHandler -var _ GatewayMessageHandler = (*NoopGatewayMessageHandler)(nil) - -func (l *NoopGatewayMessageHandler) NewItem(ctx context.Context, item *sdp.Item) { -} - -func (l *NoopGatewayMessageHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { -} - -func (l *NoopGatewayMessageHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { -} - -func (l *NoopGatewayMessageHandler) Error(ctx context.Context, errorMessage string) { -} - -func (l *NoopGatewayMessageHandler) QueryError(ctx context.Context, queryError *sdp.QueryError) { -} - -func (l *NoopGatewayMessageHandler) DeleteItem(ctx context.Context, reference *sdp.Reference) { -} - -func (l *NoopGatewayMessageHandler) DeleteEdge(ctx context.Context, edge *sdp.Edge) { -} - -func (l *NoopGatewayMessageHandler) UpdateItem(ctx context.Context, item *sdp.Item) { -} - -func (l *NoopGatewayMessageHandler) SnapshotStoreResult(ctx context.Context, result *sdp.SnapshotStoreResult) { -} - -func (l *NoopGatewayMessageHandler) SnapshotLoadResult(ctx context.Context, result *sdp.SnapshotLoadResult) { -} - -func (l *NoopGatewayMessageHandler) BookmarkStoreResult(ctx context.Context, result *sdp.BookmarkStoreResult) { -} - -func (l *NoopGatewayMessageHandler) BookmarkLoadResult(ctx context.Context, result *sdp.BookmarkLoadResult) { -} - -func (l *NoopGatewayMessageHandler) QueryStatus(ctx context.Context, status *sdp.QueryStatus) { -} - -func (l *NoopGatewayMessageHandler) ChatResponse(ctx context.Context, chatMessageResult *sdp.ChatResponse) { -} - -func (l *NoopGatewayMessageHandler) ToolStart(ctx context.Context, toolStart *sdp.ToolStart) { -} - -func (l *NoopGatewayMessageHandler) ToolFinish(ctx context.Context, toolFinish *sdp.ToolFinish) { -} - -var _ GatewayMessageHandler = (*StoreEverythingHandler)(nil) - -// A handler that stores all the items and edges it receives -type StoreEverythingHandler struct { - Items []*sdp.Item - Edges []*sdp.Edge - - NoopGatewayMessageHandler -} - -func (s *StoreEverythingHandler) NewItem(ctx context.Context, item *sdp.Item) { - s.Items = append(s.Items, item) -} - -func (s *StoreEverythingHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { - s.Edges = append(s.Edges, edge) -} - -var _ GatewayMessageHandler = (*WaitForAllQueriesHandler)(nil) - -// A Handler that waits for all queries to be done then calls a callback -type WaitForAllQueriesHandler struct { - // A callback that will be called when all queries are done - DoneCallback func() - - StoreEverythingHandler -} - -func (w *WaitForAllQueriesHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { - if status.Done() { - w.DoneCallback() - } -} diff --git a/sdp-go/sdpws/utils.go b/sdp-go/sdpws/utils.go deleted file mode 100644 index 6bdc737b..00000000 --- a/sdp-go/sdpws/utils.go +++ /dev/null @@ -1,343 +0,0 @@ -package sdpws - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/types/known/durationpb" -) - -// Sends a query on the websocket connection without waiting for responses. Use -// the `Wait()` method to wait for completion of requests based on their UUID -func (c *Client) SendQuery(ctx context.Context, q *sdp.Query) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("query", q).Trace("writing query to websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_Query{ - Query: q, - }, - MinStatusInterval: durationpb.New(time.Second), - }) - if err != nil { - // c.send already aborts - // c.abort(ctx, err) - return fmt.Errorf("error sending query: %w", err) - } - return nil -} - -// QueryOne runs a query and waits for it to complete, returning only the items -// that were found as direct results to the top-level query. -func (c *Client) QueryOne(ctx context.Context, q *sdp.Query) ([]*sdp.Item, error) { - if c.Closed() { - return nil, errors.New("client closed") - } - - u := uuid.UUID(q.GetUUID()) - - r := c.createRequestChan(u) - defer c.finishRequestChan(u) - - err := c.SendQuery(ctx, q) - if err != nil { - // c.SendQuery already aborts - // c.abort(ctx, err) - return nil, err - } - - items := make([]*sdp.Item, 0) - - var otherErr *sdp.QueryError -readLoop: - for { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context canceled: %w", ctx.Err()) - case resp, more := <-r: - if !more { - break readLoop - } - switch resp.GetResponseType().(type) { - case *sdp.GatewayResponse_NewItem: - item := resp.GetNewItem() - log.WithContext(ctx).WithField("query", q).WithField("item", item).Trace("received item") - items = append(items, item) - case *sdp.GatewayResponse_QueryError: - qe := resp.GetQueryError() - log.WithContext(ctx).WithField("query", q).WithField("queryError", qe).Trace("received query error") - switch qe.GetErrorType() { - case sdp.QueryError_OTHER, sdp.QueryError_TIMEOUT, sdp.QueryError_NOSCOPE: - // record that we received an error, but continue reading - // if we receive any item, mapping was still successful - otherErr = qe - continue readLoop - case sdp.QueryError_NOTFOUND: - // never record not found as an error - continue readLoop - } - case *sdp.GatewayResponse_QueryStatus: - qs := resp.GetQueryStatus() - span := trace.SpanFromContext(ctx) - span.SetAttributes(attribute.String("ovm.sdp.lastQueryStatus", qs.String())) - log.WithContext(ctx).WithField("query", q).WithField("queryStatus", qs).Trace("received query status") - switch qs.GetStatus() { //nolint:exhaustive // we dont care about sdp.QueryStatus_UNSPECIFIED, sdp.QueryStatus_STARTED - case sdp.QueryStatus_FINISHED: - break readLoop - case sdp.QueryStatus_CANCELLED: - return nil, errors.New("query cancelled") - case sdp.QueryStatus_ERRORED: - // if we already received items, we can ignore the error - if len(items) == 0 && otherErr != nil { - err = fmt.Errorf("query errored: %w", otherErr) - // query errors should not abort the connection - // c.abort(ctx, err) - return nil, err - } - break readLoop - } - default: - log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") - } - } - } - - return items, nil -} - -// TODO: CancelQuery -// TODO: Expand - -// Sends a LoadSnapshot request on the websocket connection without waiting for -// a response. -func (c *Client) SendLoadSnapshot(ctx context.Context, s *sdp.LoadSnapshot) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("snapshot", s).Trace("loading snapshot via websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_LoadSnapshot{ - LoadSnapshot: s, - }, - }) - if err != nil { - return fmt.Errorf("error sending load snapshot: %w", err) - } - return nil -} - -// Load a snapshot and wait for it to complete. This will return the -// SnapshotLoadResult from the gateway. A separate error is only returned when -// there is a communication error. Logic errors from the gateway are reported -// through the returned SnapshotLoadResult. -func (c *Client) LoadSnapshot(ctx context.Context, id uuid.UUID) (*sdp.SnapshotLoadResult, error) { - if c.Closed() { - return nil, errors.New("client closed") - } - - u := uuid.New() - s := &sdp.LoadSnapshot{ - UUID: id[:], - MsgID: u[:], - } - r := c.createRequestChan(u) - - err := c.SendLoadSnapshot(ctx, s) - if err != nil { - return nil, err - } - - for { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("context canceled: %w", ctx.Err()) - case resp, more := <-r: - if !more { - return nil, errors.New("request channel closed") - } - switch resp.GetResponseType().(type) { - case *sdp.GatewayResponse_SnapshotLoadResult: - slr := resp.GetSnapshotLoadResult() - log.WithContext(ctx).WithField("snapshot", s).WithField("snapshotLoadResult", slr).Trace("received snapshot load result") - return slr, nil - default: - log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") - return nil, errors.New("unexpected response") - } - } - } -} - -// Sends a StoreSnapshot request on the websocket connection without waiting for -// a response. -func (c *Client) SendStoreSnapshot(ctx context.Context, s *sdp.StoreSnapshot) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("snapshot", s).Trace("storing snapshot via websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_StoreSnapshot{ - StoreSnapshot: s, - }, - }) - if err != nil { - return fmt.Errorf("error sending store snapshot: %w", err) - } - return nil -} - -// Store a snapshot and wait for it to complete, returning the UUID of the -// snapshot that was created. -func (c *Client) StoreSnapshot(ctx context.Context, name, description string) (uuid.UUID, error) { - if c.Closed() { - return uuid.UUID{}, errors.New("client closed") - } - - u := uuid.New() - s := &sdp.StoreSnapshot{ - Name: name, - Description: description, - MsgID: u[:], - } - r := c.createRequestChan(u) - - err := c.SendStoreSnapshot(ctx, s) - if err != nil { - return uuid.UUID{}, err - } - - for { - select { - case <-ctx.Done(): - return uuid.UUID{}, fmt.Errorf("context canceled: %w", ctx.Err()) - case resp, more := <-r: - if !more { - return uuid.UUID{}, errors.New("request channel closed") - } - switch resp.GetResponseType().(type) { - case *sdp.GatewayResponse_SnapshotStoreResult: - ssr := resp.GetSnapshotStoreResult() - log.WithContext(ctx).WithField("Snapshot", s).WithField("snapshotStoreResult", ssr).Trace("received snapshot store result") - if ssr.GetSuccess() { - return uuid.UUID(ssr.GetSnapshotID()), nil - } - return uuid.UUID{}, fmt.Errorf("snapshot store failed: %v", ssr.GetErrorMessage()) - default: - log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") - return uuid.UUID{}, errors.New("unexpected response") - } - } - } -} - -func (c *Client) SendLoadBookmark(ctx context.Context, b *sdp.LoadBookmark) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("bookmark", b).Trace("loading bookmark via websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_LoadBookmark{ - LoadBookmark: b, - }, - }) - if err != nil { - return fmt.Errorf("error sending load bookmark: %w", err) - } - return nil -} - -// Sends a StoreBookmark request on the websocket connection without waiting for -// a response. -func (c *Client) SendStoreBookmark(ctx context.Context, b *sdp.StoreBookmark) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("bookmark", b).Trace("storing bookmark via websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_StoreBookmark{ - StoreBookmark: b, - }, - }) - if err != nil { - return fmt.Errorf("error sending store bookmark: %w", err) - } - return nil -} - -// Store a bookmark and wait for it to complete, returning the UUID of the -// bookmark that was created. -func (c *Client) StoreBookmark(ctx context.Context, name, description string, isSystem bool) (uuid.UUID, error) { - if c.Closed() { - return uuid.UUID{}, errors.New("client closed") - } - - u := uuid.New() - b := &sdp.StoreBookmark{ - Name: name, - Description: description, - MsgID: u[:], - IsSystem: true, - } - r := c.createRequestChan(u) - - err := c.SendStoreBookmark(ctx, b) - if err != nil { - return uuid.UUID{}, err - } - - for { - select { - case <-ctx.Done(): - return uuid.UUID{}, fmt.Errorf("context canceled: %w", ctx.Err()) - case resp, more := <-r: - if !more { - return uuid.UUID{}, errors.New("request channel closed") - } - switch resp.GetResponseType().(type) { - case *sdp.GatewayResponse_BookmarkStoreResult: - bsr := resp.GetBookmarkStoreResult() - log.WithContext(ctx).WithField("bookmark", b).WithField("bookmarkStoreResult", bsr).Trace("received bookmark store result") - if bsr.GetSuccess() { - return uuid.UUID(bsr.GetBookmarkID()), nil - } - return uuid.UUID{}, fmt.Errorf("bookmark store failed: %v", bsr.GetErrorMessage()) - default: - log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") - return uuid.UUID{}, errors.New("unexpected response") - } - } - } -} - -// TODO: LoadBookmark - -// send chatMessage to the assistant -func (c *Client) SendChatMessage(ctx context.Context, m *sdp.ChatMessage) error { - if c.Closed() { - return errors.New("client closed") - } - - log.WithContext(ctx).WithField("message", m).Trace("sending chat message via websocket") - err := c.send(ctx, &sdp.GatewayRequest{ - RequestType: &sdp.GatewayRequest_ChatMessage{ - ChatMessage: m, - }, - }) - if err != nil { - return fmt.Errorf("error sending chat message: %w", err) - } - return nil -} diff --git a/sdp-go/signal.pb.go b/sdp-go/signal.pb.go deleted file mode 100644 index 4866cabd..00000000 --- a/sdp-go/signal.pb.go +++ /dev/null @@ -1,1286 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: signal.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type AddSignalRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The user facing properties of the signal - Properties *SignalProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - // UUID of the change this signal is associated with - ChangeUUID []byte `protobuf:"bytes,2,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AddSignalRequest) Reset() { - *x = AddSignalRequest{} - mi := &file_signal_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AddSignalRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AddSignalRequest) ProtoMessage() {} - -func (x *AddSignalRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[0] - 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 AddSignalRequest.ProtoReflect.Descriptor instead. -func (*AddSignalRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{0} -} - -func (x *AddSignalRequest) GetProperties() *SignalProperties { - if x != nil { - return x.Properties - } - return nil -} - -func (x *AddSignalRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type AddSignalResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signal *Signal `protobuf:"bytes,1,opt,name=signal,proto3" json:"signal,omitempty"` // The signal that was added - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AddSignalResponse) Reset() { - *x = AddSignalResponse{} - mi := &file_signal_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AddSignalResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AddSignalResponse) ProtoMessage() {} - -func (x *AddSignalResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[1] - 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 AddSignalResponse.ProtoReflect.Descriptor instead. -func (*AddSignalResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{1} -} - -func (x *AddSignalResponse) GetSignal() *Signal { - if x != nil { - return x.Signal - } - return nil -} - -type GetSignalsByChangeExternalIDRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` // UUID of the change - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSignalsByChangeExternalIDRequest) Reset() { - *x = GetSignalsByChangeExternalIDRequest{} - mi := &file_signal_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSignalsByChangeExternalIDRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSignalsByChangeExternalIDRequest) ProtoMessage() {} - -func (x *GetSignalsByChangeExternalIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[2] - 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 GetSignalsByChangeExternalIDRequest.ProtoReflect.Descriptor instead. -func (*GetSignalsByChangeExternalIDRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{2} -} - -func (x *GetSignalsByChangeExternalIDRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetSignalsByChangeExternalIDResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // List of all signals associated with the change - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSignalsByChangeExternalIDResponse) Reset() { - *x = GetSignalsByChangeExternalIDResponse{} - mi := &file_signal_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSignalsByChangeExternalIDResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSignalsByChangeExternalIDResponse) ProtoMessage() {} - -func (x *GetSignalsByChangeExternalIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[3] - 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 GetSignalsByChangeExternalIDResponse.ProtoReflect.Descriptor instead. -func (*GetSignalsByChangeExternalIDResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{3} -} - -func (x *GetSignalsByChangeExternalIDResponse) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -type GetChangeOverviewSignalsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeOverviewSignalsRequest) Reset() { - *x = GetChangeOverviewSignalsRequest{} - mi := &file_signal_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeOverviewSignalsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeOverviewSignalsRequest) ProtoMessage() {} - -func (x *GetChangeOverviewSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[4] - 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 GetChangeOverviewSignalsRequest.ProtoReflect.Descriptor instead. -func (*GetChangeOverviewSignalsRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{4} -} - -func (x *GetChangeOverviewSignalsRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetChangeOverviewSignalsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` - // The aggregated value for all categories in the change, calculated by AggregateSignalScores. - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetChangeOverviewSignalsResponse) Reset() { - *x = GetChangeOverviewSignalsResponse{} - mi := &file_signal_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetChangeOverviewSignalsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetChangeOverviewSignalsResponse) ProtoMessage() {} - -func (x *GetChangeOverviewSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[5] - 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 GetChangeOverviewSignalsResponse.ProtoReflect.Descriptor instead. -func (*GetChangeOverviewSignalsResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{5} -} - -func (x *GetChangeOverviewSignalsResponse) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -func (x *GetChangeOverviewSignalsResponse) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -type ItemAggregation struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A sorted list of signals for this item, by category. - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // Corresponds to []*sdp.Signal - // The aggregated value for this item, calculated by AggregateSignalScores. - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ItemAggregation) Reset() { - *x = ItemAggregation{} - mi := &file_signal_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ItemAggregation) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ItemAggregation) ProtoMessage() {} - -func (x *ItemAggregation) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[6] - 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 ItemAggregation.ProtoReflect.Descriptor instead. -func (*ItemAggregation) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{6} -} - -func (x *ItemAggregation) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -func (x *ItemAggregation) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -type GetItemSignalsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalsRequest) Reset() { - *x = GetItemSignalsRequest{} - mi := &file_signal_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalsRequest) ProtoMessage() {} - -func (x *GetItemSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[7] - 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 GetItemSignalsRequest.ProtoReflect.Descriptor instead. -func (*GetItemSignalsRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{7} -} - -func (x *GetItemSignalsRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type GetItemSignalsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A map of Globally Unique Names (GUNs) of items to their aggregation of signals. - // These are by category, sorted by the signal value, ascending. - // We also include a value for this GUN, which is calculated by AggregateSignalScores - ItemAggregations map[string]*ItemAggregation `protobuf:"bytes,1,rep,name=itemAggregations,proto3" json:"itemAggregations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalsResponse) Reset() { - *x = GetItemSignalsResponse{} - mi := &file_signal_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalsResponse) ProtoMessage() {} - -func (x *GetItemSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[8] - 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 GetItemSignalsResponse.ProtoReflect.Descriptor instead. -func (*GetItemSignalsResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{8} -} - -func (x *GetItemSignalsResponse) GetItemAggregations() map[string]*ItemAggregation { - if x != nil { - return x.ItemAggregations - } - return nil -} - -type GetItemSignalsRequestV2 struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalsRequestV2) Reset() { - *x = GetItemSignalsRequestV2{} - mi := &file_signal_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalsRequestV2) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalsRequestV2) ProtoMessage() {} - -func (x *GetItemSignalsRequestV2) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[9] - 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 GetItemSignalsRequestV2.ProtoReflect.Descriptor instead. -func (*GetItemSignalsRequestV2) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{9} -} - -func (x *GetItemSignalsRequestV2) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -type ItemAggregationV2 struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A sorted list of signals for this item, by category. - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // Corresponds to []*sdp.Signal - // The aggregated value for this item, calculated by AggregateSignalScores. - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 - // It is the friendly item reference, taken from the resolve mapping queries. - // for handiness we wall back to the afterRef if the mappedRef is not available. - MappedRef *Reference `protobuf:"bytes,3,opt,name=mappedRef,proto3" json:"mappedRef,omitempty"` - // This it the item reference from after, it is used to look up the item in GetItemSignalDetailsRequest. - AfterRef *Reference `protobuf:"bytes,4,opt,name=afterRef,proto3" json:"afterRef,omitempty"` - // status is the status of the item, e.g. "added", "modified", "deleted". - Status ItemDiffStatus `protobuf:"varint,5,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ItemAggregationV2) Reset() { - *x = ItemAggregationV2{} - mi := &file_signal_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ItemAggregationV2) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ItemAggregationV2) ProtoMessage() {} - -func (x *ItemAggregationV2) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[10] - 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 ItemAggregationV2.ProtoReflect.Descriptor instead. -func (*ItemAggregationV2) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{10} -} - -func (x *ItemAggregationV2) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -func (x *ItemAggregationV2) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *ItemAggregationV2) GetMappedRef() *Reference { - if x != nil { - return x.MappedRef - } - return nil -} - -func (x *ItemAggregationV2) GetAfterRef() *Reference { - if x != nil { - return x.AfterRef - } - return nil -} - -func (x *ItemAggregationV2) GetStatus() ItemDiffStatus { - if x != nil { - return x.Status - } - return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED -} - -type GetItemSignalsResponseV2 struct { - state protoimpl.MessageState `protogen:"open.v1"` - // A map of Globally Unique Names (GUNs) of items to their aggregation of signals. - // These are by category, sorted by the signal value, ascending. - // We also include a value for this GUN, which is calculated by AggregateSignalScores - ItemAggregations []*ItemAggregationV2 `protobuf:"bytes,1,rep,name=itemAggregations,proto3" json:"itemAggregations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalsResponseV2) Reset() { - *x = GetItemSignalsResponseV2{} - mi := &file_signal_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalsResponseV2) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalsResponseV2) ProtoMessage() {} - -func (x *GetItemSignalsResponseV2) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[11] - 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 GetItemSignalsResponseV2.ProtoReflect.Descriptor instead. -func (*GetItemSignalsResponseV2) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{11} -} - -func (x *GetItemSignalsResponseV2) GetItemAggregations() []*ItemAggregationV2 { - if x != nil { - return x.ItemAggregations - } - return nil -} - -// Get all custom signals for a change by its external ID and category. They are NOT associated with any item. -type GetCustomSignalsByCategoryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCustomSignalsByCategoryRequest) Reset() { - *x = GetCustomSignalsByCategoryRequest{} - mi := &file_signal_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCustomSignalsByCategoryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCustomSignalsByCategoryRequest) ProtoMessage() {} - -func (x *GetCustomSignalsByCategoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[12] - 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 GetCustomSignalsByCategoryRequest.ProtoReflect.Descriptor instead. -func (*GetCustomSignalsByCategoryRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{12} -} - -func (x *GetCustomSignalsByCategoryRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -func (x *GetCustomSignalsByCategoryRequest) GetCategory() string { - if x != nil { - return x.Category - } - return "" -} - -// array of signals -type GetCustomSignalsByCategoryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCustomSignalsByCategoryResponse) Reset() { - *x = GetCustomSignalsByCategoryResponse{} - mi := &file_signal_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCustomSignalsByCategoryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCustomSignalsByCategoryResponse) ProtoMessage() {} - -func (x *GetCustomSignalsByCategoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCustomSignalsByCategoryResponse.ProtoReflect.Descriptor instead. -func (*GetCustomSignalsByCategoryResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{13} -} - -func (x *GetCustomSignalsByCategoryResponse) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -type GetItemSignalDetailsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The item for which we want to get the details of the signals. - // it is the reference of the terraform item before/after. - // NB it is not the lookup item from resolve mapping queries. - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // The UUID of the change this item is associated with. - ChangeUUID []byte `protobuf:"bytes,2,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` - // The category of the signals we want to get. This is used to filter the signals by category. - Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalDetailsRequest) Reset() { - *x = GetItemSignalDetailsRequest{} - mi := &file_signal_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalDetailsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalDetailsRequest) ProtoMessage() {} - -func (x *GetItemSignalDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetItemSignalDetailsRequest.ProtoReflect.Descriptor instead. -func (*GetItemSignalDetailsRequest) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{14} -} - -func (x *GetItemSignalDetailsRequest) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -func (x *GetItemSignalDetailsRequest) GetChangeUUID() []byte { - if x != nil { - return x.ChangeUUID - } - return nil -} - -func (x *GetItemSignalDetailsRequest) GetCategory() string { - if x != nil { - return x.Category - } - return "" -} - -type GetItemSignalDetailsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetItemSignalDetailsResponse) Reset() { - *x = GetItemSignalDetailsResponse{} - mi := &file_signal_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetItemSignalDetailsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetItemSignalDetailsResponse) ProtoMessage() {} - -func (x *GetItemSignalDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[15] - 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 GetItemSignalDetailsResponse.ProtoReflect.Descriptor instead. -func (*GetItemSignalDetailsResponse) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{15} -} - -func (x *GetItemSignalDetailsResponse) GetSignals() []*Signal { - if x != nil { - return x.Signals - } - return nil -} - -type SignalMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SignalMetadata) Reset() { - *x = SignalMetadata{} - mi := &file_signal_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SignalMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SignalMetadata) ProtoMessage() {} - -func (x *SignalMetadata) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[16] - 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 SignalMetadata.ProtoReflect.Descriptor instead. -func (*SignalMetadata) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{16} -} - -type SignalProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // user-supplied properties of this signal - // A short name for the signal - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // This is float64 value, representing the signal's value. -5 to +5. +5 is very high / strong, 0 is neutral, -5 is very low / weak. - Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` - // A one sentence description of the signal, it could be activity in routineness, or another - // descriptive text - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - // A category for the signal, e.g. "routineness", "anomaly", etc. How it will be grouped in the UI. - Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"` - // This is poorly named, this is the after item reference, equivalent to afterRef in ItemAggregationV2 - // in the signals table this is the afterRef column. It is not the mappedRef / friendly item reference. - Item *Reference `protobuf:"bytes,5,opt,name=item,proto3,oneof" json:"item,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SignalProperties) Reset() { - *x = SignalProperties{} - mi := &file_signal_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SignalProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SignalProperties) ProtoMessage() {} - -func (x *SignalProperties) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SignalProperties.ProtoReflect.Descriptor instead. -func (*SignalProperties) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{17} -} - -func (x *SignalProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SignalProperties) GetValue() float64 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *SignalProperties) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *SignalProperties) GetCategory() string { - if x != nil { - return x.Category - } - return "" -} - -func (x *SignalProperties) GetItem() *Reference { - if x != nil { - return x.Item - } - return nil -} - -// we mimic the layout of the changes object here, because there are 2 parts -// to a signal: the machine-generated metadata and the user-supplied properties. -type Signal struct { - state protoimpl.MessageState `protogen:"open.v1"` - // machine-generated metadata of this signal - Metadata *SignalMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // user-supplied properties of this signal - Properties *SignalProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Signal) Reset() { - *x = Signal{} - mi := &file_signal_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Signal) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Signal) ProtoMessage() {} - -func (x *Signal) ProtoReflect() protoreflect.Message { - mi := &file_signal_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Signal.ProtoReflect.Descriptor instead. -func (*Signal) Descriptor() ([]byte, []int) { - return file_signal_proto_rawDescGZIP(), []int{18} -} - -func (x *Signal) GetMetadata() *SignalMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Signal) GetProperties() *SignalProperties { - if x != nil { - return x.Properties - } - 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 = "" + - "\n" + - "\fsignal.proto\x12\x06signal\x1a\rchanges.proto\x1a\vitems.proto\"l\n" + - "\x10AddSignalRequest\x128\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x18.signal.SignalPropertiesR\n" + - "properties\x12\x1e\n" + - "\n" + - "changeUUID\x18\x02 \x01(\fR\n" + - "changeUUID\";\n" + - "\x11AddSignalResponse\x12&\n" + - "\x06signal\x18\x01 \x01(\v2\x0e.signal.SignalR\x06signal\"E\n" + - "#GetSignalsByChangeExternalIDRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"P\n" + - "$GetSignalsByChangeExternalIDResponse\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"A\n" + - "\x1fGetChangeOverviewSignalsRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"b\n" + - " GetChangeOverviewSignalsResponse\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + - "\x05value\x18\x02 \x01(\x01R\x05value\"Q\n" + - "\x0fItemAggregation\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + - "\x05value\x18\x02 \x01(\x01R\x05value\"7\n" + - "\x15GetItemSignalsRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"\xd8\x01\n" + - "\x16GetItemSignalsResponse\x12`\n" + - "\x10itemAggregations\x18\x01 \x03(\v24.signal.GetItemSignalsResponse.ItemAggregationsEntryR\x10itemAggregations\x1a\\\n" + - "\x15ItemAggregationsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12-\n" + - "\x05value\x18\x02 \x01(\v2\x17.signal.ItemAggregationR\x05value:\x028\x01\"9\n" + - "\x17GetItemSignalsRequestV2\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\"\xd6\x01\n" + - "\x11ItemAggregationV2\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + - "\x05value\x18\x02 \x01(\x01R\x05value\x12(\n" + - "\tmappedRef\x18\x03 \x01(\v2\n" + - ".ReferenceR\tmappedRef\x12&\n" + - "\bafterRef\x18\x04 \x01(\v2\n" + - ".ReferenceR\bafterRef\x12/\n" + - "\x06status\x18\x05 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\"a\n" + - "\x18GetItemSignalsResponseV2\x12E\n" + - "\x10itemAggregations\x18\x01 \x03(\v2\x19.signal.ItemAggregationV2R\x10itemAggregations\"_\n" + - "!GetCustomSignalsByCategoryRequest\x12\x1e\n" + - "\n" + - "changeUUID\x18\x01 \x01(\fR\n" + - "changeUUID\x12\x1a\n" + - "\bcategory\x18\x02 \x01(\tR\bcategory\"N\n" + - "\"GetCustomSignalsByCategoryResponse\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"y\n" + - "\x1bGetItemSignalDetailsRequest\x12\x1e\n" + - "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12\x1e\n" + - "\n" + - "changeUUID\x18\x02 \x01(\fR\n" + - "changeUUID\x12\x1a\n" + - "\bcategory\x18\x03 \x01(\tR\bcategory\"H\n" + - "\x1cGetItemSignalDetailsResponse\x12(\n" + - "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"\x10\n" + - "\x0eSignalMetadata\"\xa8\x01\n" + - "\x10SignalProperties\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05value\x18\x02 \x01(\x01R\x05value\x12 \n" + - "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1a\n" + - "\bcategory\x18\x04 \x01(\tR\bcategory\x12#\n" + - "\x04item\x18\x05 \x01(\v2\n" + - ".ReferenceH\x00R\x04item\x88\x01\x01B\a\n" + - "\x05_item\"v\n" + - "\x06Signal\x122\n" + - "\bmetadata\x18\x01 \x01(\v2\x16.signal.SignalMetadataR\bmetadata\x128\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x18.signal.SignalPropertiesR\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" + - "\x18GetChangeOverviewSignals\x12'.signal.GetChangeOverviewSignalsRequest\x1a(.signal.GetChangeOverviewSignalsResponse\x12O\n" + - "\x0eGetItemSignals\x12\x1d.signal.GetItemSignalsRequest\x1a\x1e.signal.GetItemSignalsResponse\x12U\n" + - "\x10GetItemSignalsV2\x12\x1f.signal.GetItemSignalsRequestV2\x1a .signal.GetItemSignalsResponseV2\x12s\n" + - "\x1aGetCustomSignalsByCategory\x12).signal.GetCustomSignalsByCategoryRequest\x1a*.signal.GetCustomSignalsByCategoryResponse\x12a\n" + - "\x14GetItemSignalDetails\x12#.signal.GetItemSignalDetailsRequest\x1a$.signal.GetItemSignalDetailsResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_signal_proto_rawDescOnce sync.Once - file_signal_proto_rawDescData []byte -) - -func file_signal_proto_rawDescGZIP() []byte { - file_signal_proto_rawDescOnce.Do(func() { - file_signal_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_signal_proto_rawDesc), len(file_signal_proto_rawDesc))) - }) - return file_signal_proto_rawDescData -} - -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 - (*GetSignalsByChangeExternalIDRequest)(nil), // 2: signal.GetSignalsByChangeExternalIDRequest - (*GetSignalsByChangeExternalIDResponse)(nil), // 3: signal.GetSignalsByChangeExternalIDResponse - (*GetChangeOverviewSignalsRequest)(nil), // 4: signal.GetChangeOverviewSignalsRequest - (*GetChangeOverviewSignalsResponse)(nil), // 5: signal.GetChangeOverviewSignalsResponse - (*ItemAggregation)(nil), // 6: signal.ItemAggregation - (*GetItemSignalsRequest)(nil), // 7: signal.GetItemSignalsRequest - (*GetItemSignalsResponse)(nil), // 8: signal.GetItemSignalsResponse - (*GetItemSignalsRequestV2)(nil), // 9: signal.GetItemSignalsRequestV2 - (*ItemAggregationV2)(nil), // 10: signal.ItemAggregationV2 - (*GetItemSignalsResponseV2)(nil), // 11: signal.GetItemSignalsResponseV2 - (*GetCustomSignalsByCategoryRequest)(nil), // 12: signal.GetCustomSignalsByCategoryRequest - (*GetCustomSignalsByCategoryResponse)(nil), // 13: signal.GetCustomSignalsByCategoryResponse - (*GetItemSignalDetailsRequest)(nil), // 14: signal.GetItemSignalDetailsRequest - (*GetItemSignalDetailsResponse)(nil), // 15: signal.GetItemSignalDetailsResponse - (*SignalMetadata)(nil), // 16: signal.SignalMetadata - (*SignalProperties)(nil), // 17: signal.SignalProperties - (*Signal)(nil), // 18: signal.Signal - (*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 - 18, // 1: signal.AddSignalResponse.signal:type_name -> signal.Signal - 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 - 20, // 5: signal.GetItemSignalsResponse.itemAggregations:type_name -> signal.GetItemSignalsResponse.ItemAggregationsEntry - 18, // 6: signal.ItemAggregationV2.signals:type_name -> signal.Signal - 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 - 21, // 12: signal.GetItemSignalDetailsRequest.item:type_name -> Reference - 18, // 13: signal.GetItemSignalDetailsResponse.signals:type_name -> signal.Signal - 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 - 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() } -func file_signal_proto_init() { - if File_signal_proto != nil { - return - } - file_changes_proto_init() - file_items_proto_init() - file_signal_proto_msgTypes[17].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_signal_proto_rawDesc), len(file_signal_proto_rawDesc)), - NumEnums: 0, - NumMessages: 21, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_signal_proto_goTypes, - DependencyIndexes: file_signal_proto_depIdxs, - MessageInfos: file_signal_proto_msgTypes, - }.Build() - File_signal_proto = out.File - file_signal_proto_goTypes = nil - file_signal_proto_depIdxs = nil -} diff --git a/sdp-go/signals.go b/sdp-go/signals.go deleted file mode 100644 index bd884cd0..00000000 --- a/sdp-go/signals.go +++ /dev/null @@ -1,10 +0,0 @@ -package sdp - -type SignalCategoryName string - -// SignalCategoryName constants represent the predefined categories for signals. -// if you add a new category, please also update the cli command "submit-signal" @ cli/cmd/changes_submit_signal.go -const ( - SignalCategoryNameCustom SignalCategoryName = "Custom" - SignalCategoryNameRoutine SignalCategoryName = "Routine" -) diff --git a/sdp-go/snapshots.go b/sdp-go/snapshots.go deleted file mode 100644 index d6e45c71..00000000 --- a/sdp-go/snapshots.go +++ /dev/null @@ -1,37 +0,0 @@ -package sdp - -import "github.com/google/uuid" - -func (s *Snapshot) ToMap() map[string]any { - return map[string]any{ - "metadata": s.GetMetadata().ToMap(), - "properties": s.GetProperties().ToMap(), - } -} - -func (sm *SnapshotMetadata) ToMap() map[string]any { - return map[string]any{ - "UUID": stringFromUuidBytes(sm.GetUUID()), - "created": sm.GetCreated().AsTime(), - } -} - -func (sm *SnapshotMetadata) GetUUIDParsed() *uuid.UUID { - if sm == nil { - return nil - } - u, err := uuid.FromBytes(sm.GetUUID()) - if err != nil { - return nil - } - return &u -} - -func (sp *SnapshotProperties) ToMap() map[string]any { - return map[string]any{ - "name": sp.GetName(), - "description": sp.GetDescription(), - "queries": sp.GetQueries(), - "Items": sp.GetItems(), - } -} diff --git a/sdp-go/snapshots.pb.go b/sdp-go/snapshots.pb.go deleted file mode 100644 index edd6d6ec..00000000 --- a/sdp-go/snapshots.pb.go +++ /dev/null @@ -1,1011 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: snapshots.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Snapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *SnapshotMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Properties *SnapshotProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Snapshot) Reset() { - *x = Snapshot{} - mi := &file_snapshots_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Snapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Snapshot) ProtoMessage() {} - -func (x *Snapshot) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[0] - 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 Snapshot.ProtoReflect.Descriptor instead. -func (*Snapshot) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{0} -} - -func (x *Snapshot) GetMetadata() *SnapshotMetadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Snapshot) GetProperties() *SnapshotProperties { - if x != nil { - return x.Properties - } - return nil -} - -type SnapshotProperties struct { - state protoimpl.MessageState `protogen:"open.v1"` - // name of this snapshot - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // description of this snapshot - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - // queries that make up the snapshot - Queries []*Query `protobuf:"bytes,3,rep,name=queries,proto3" json:"queries,omitempty"` - // items stored in the snapshot - Items []*Item `protobuf:"bytes,5,rep,name=items,proto3" json:"items,omitempty"` - // edges stored in the snapshot - Edges []*Edge `protobuf:"bytes,6,rep,name=edges,proto3" json:"edges,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SnapshotProperties) Reset() { - *x = SnapshotProperties{} - mi := &file_snapshots_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SnapshotProperties) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SnapshotProperties) ProtoMessage() {} - -func (x *SnapshotProperties) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[1] - 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 SnapshotProperties.ProtoReflect.Descriptor instead. -func (*SnapshotProperties) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{1} -} - -func (x *SnapshotProperties) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SnapshotProperties) GetDescription() string { - if x != nil { - return x.Description - } - return "" -} - -func (x *SnapshotProperties) GetQueries() []*Query { - if x != nil { - return x.Queries - } - return nil -} - -func (x *SnapshotProperties) GetItems() []*Item { - if x != nil { - return x.Items - } - return nil -} - -func (x *SnapshotProperties) GetEdges() []*Edge { - if x != nil { - return x.Edges - } - return nil -} - -type SnapshotMetadata struct { - state protoimpl.MessageState `protogen:"open.v1"` - // unique id to identify this snapshot - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - // timestamp when this snapshot was created - Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SnapshotMetadata) Reset() { - *x = SnapshotMetadata{} - mi := &file_snapshots_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SnapshotMetadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SnapshotMetadata) ProtoMessage() {} - -func (x *SnapshotMetadata) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[2] - 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 SnapshotMetadata.ProtoReflect.Descriptor instead. -func (*SnapshotMetadata) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{2} -} - -func (x *SnapshotMetadata) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *SnapshotMetadata) GetCreated() *timestamppb.Timestamp { - if x != nil { - return x.Created - } - return nil -} - -// lists all snapshots -type ListSnapshotsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSnapshotsRequest) Reset() { - *x = ListSnapshotsRequest{} - mi := &file_snapshots_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSnapshotsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSnapshotsRequest) ProtoMessage() {} - -func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[3] - 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 ListSnapshotsRequest.ProtoReflect.Descriptor instead. -func (*ListSnapshotsRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{3} -} - -type ListSnapshotResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the list of all snapshots - Snapshots []*Snapshot `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSnapshotResponse) Reset() { - *x = ListSnapshotResponse{} - mi := &file_snapshots_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSnapshotResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSnapshotResponse) ProtoMessage() {} - -func (x *ListSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[4] - 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 ListSnapshotResponse.ProtoReflect.Descriptor instead. -func (*ListSnapshotResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{4} -} - -func (x *ListSnapshotResponse) GetSnapshots() []*Snapshot { - if x != nil { - return x.Snapshots - } - return nil -} - -// creates a new snapshot -type CreateSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // properties of the new snapshot - Properties *SnapshotProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateSnapshotRequest) Reset() { - *x = CreateSnapshotRequest{} - mi := &file_snapshots_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateSnapshotRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateSnapshotRequest) ProtoMessage() {} - -func (x *CreateSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[5] - 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 CreateSnapshotRequest.ProtoReflect.Descriptor instead. -func (*CreateSnapshotRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{5} -} - -func (x *CreateSnapshotRequest) GetProperties() *SnapshotProperties { - if x != nil { - return x.Properties - } - return nil -} - -type CreateSnapshotResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the newly created snapshot - Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateSnapshotResponse) Reset() { - *x = CreateSnapshotResponse{} - mi := &file_snapshots_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateSnapshotResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateSnapshotResponse) ProtoMessage() {} - -func (x *CreateSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[6] - 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 CreateSnapshotResponse.ProtoReflect.Descriptor instead. -func (*CreateSnapshotResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{6} -} - -func (x *CreateSnapshotResponse) GetSnapshot() *Snapshot { - if x != nil { - return x.Snapshot - } - return nil -} - -// get the details of a specific snapshot -type GetSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSnapshotRequest) Reset() { - *x = GetSnapshotRequest{} - mi := &file_snapshots_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSnapshotRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSnapshotRequest) ProtoMessage() {} - -func (x *GetSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[7] - 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 GetSnapshotRequest.ProtoReflect.Descriptor instead. -func (*GetSnapshotRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{7} -} - -func (x *GetSnapshotRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type GetSnapshotResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSnapshotResponse) Reset() { - *x = GetSnapshotResponse{} - mi := &file_snapshots_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSnapshotResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSnapshotResponse) ProtoMessage() {} - -func (x *GetSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[8] - 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 GetSnapshotResponse.ProtoReflect.Descriptor instead. -func (*GetSnapshotResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{8} -} - -func (x *GetSnapshotResponse) GetSnapshot() *Snapshot { - if x != nil { - return x.Snapshot - } - return nil -} - -// updates the properties of an existing snapshot -type UpdateSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - Properties *SnapshotProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSnapshotRequest) Reset() { - *x = UpdateSnapshotRequest{} - mi := &file_snapshots_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSnapshotRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSnapshotRequest) ProtoMessage() {} - -func (x *UpdateSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[9] - 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 UpdateSnapshotRequest.ProtoReflect.Descriptor instead. -func (*UpdateSnapshotRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{9} -} - -func (x *UpdateSnapshotRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -func (x *UpdateSnapshotRequest) GetProperties() *SnapshotProperties { - if x != nil { - return x.Properties - } - return nil -} - -type UpdateSnapshotResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // the updated version of the snapshot - Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSnapshotResponse) Reset() { - *x = UpdateSnapshotResponse{} - mi := &file_snapshots_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSnapshotResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSnapshotResponse) ProtoMessage() {} - -func (x *UpdateSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[10] - 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 UpdateSnapshotResponse.ProtoReflect.Descriptor instead. -func (*UpdateSnapshotResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{10} -} - -func (x *UpdateSnapshotResponse) GetSnapshot() *Snapshot { - if x != nil { - return x.Snapshot - } - return nil -} - -// deletes a given snapshot -type DeleteSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSnapshotRequest) Reset() { - *x = DeleteSnapshotRequest{} - mi := &file_snapshots_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSnapshotRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSnapshotRequest) ProtoMessage() {} - -func (x *DeleteSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[11] - 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 DeleteSnapshotRequest.ProtoReflect.Descriptor instead. -func (*DeleteSnapshotRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{11} -} - -func (x *DeleteSnapshotRequest) GetUUID() []byte { - if x != nil { - return x.UUID - } - return nil -} - -type DeleteSnapshotResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSnapshotResponse) Reset() { - *x = DeleteSnapshotResponse{} - mi := &file_snapshots_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSnapshotResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSnapshotResponse) ProtoMessage() {} - -func (x *DeleteSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[12] - 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 DeleteSnapshotResponse.ProtoReflect.Descriptor instead. -func (*DeleteSnapshotResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{12} -} - -// get the initial data -type GetInitialDataRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetInitialDataRequest) Reset() { - *x = GetInitialDataRequest{} - mi := &file_snapshots_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetInitialDataRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetInitialDataRequest) ProtoMessage() {} - -func (x *GetInitialDataRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetInitialDataRequest.ProtoReflect.Descriptor instead. -func (*GetInitialDataRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{13} -} - -type GetInitialDataResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - BlastRadiusSnapshot *Snapshot `protobuf:"bytes,1,opt,name=blastRadiusSnapshot,proto3" json:"blastRadiusSnapshot,omitempty"` - ChangingItemsBookmark *Bookmark `protobuf:"bytes,2,opt,name=changingItemsBookmark,proto3" json:"changingItemsBookmark,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetInitialDataResponse) Reset() { - *x = GetInitialDataResponse{} - mi := &file_snapshots_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetInitialDataResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetInitialDataResponse) ProtoMessage() {} - -func (x *GetInitialDataResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetInitialDataResponse.ProtoReflect.Descriptor instead. -func (*GetInitialDataResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{14} -} - -func (x *GetInitialDataResponse) GetBlastRadiusSnapshot() *Snapshot { - if x != nil { - return x.BlastRadiusSnapshot - } - return nil -} - -func (x *GetInitialDataResponse) GetChangingItemsBookmark() *Bookmark { - if x != nil { - return x.ChangingItemsBookmark - } - return nil -} - -type ListSnapshotsByGUNRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - GloballyUniqueName string `protobuf:"bytes,1,opt,name=globallyUniqueName,proto3" json:"globallyUniqueName,omitempty"` - Pagination *PaginationRequest `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSnapshotsByGUNRequest) Reset() { - *x = ListSnapshotsByGUNRequest{} - mi := &file_snapshots_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSnapshotsByGUNRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSnapshotsByGUNRequest) ProtoMessage() {} - -func (x *ListSnapshotsByGUNRequest) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[15] - 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 ListSnapshotsByGUNRequest.ProtoReflect.Descriptor instead. -func (*ListSnapshotsByGUNRequest) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{15} -} - -func (x *ListSnapshotsByGUNRequest) GetGloballyUniqueName() string { - if x != nil { - return x.GloballyUniqueName - } - return "" -} - -func (x *ListSnapshotsByGUNRequest) GetPagination() *PaginationRequest { - if x != nil { - return x.Pagination - } - return nil -} - -type ListSnapshotsByGUNResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - UUIDs [][]byte `protobuf:"bytes,1,rep,name=UUIDs,proto3" json:"UUIDs,omitempty"` - Pagination *PaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSnapshotsByGUNResponse) Reset() { - *x = ListSnapshotsByGUNResponse{} - mi := &file_snapshots_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSnapshotsByGUNResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSnapshotsByGUNResponse) ProtoMessage() {} - -func (x *ListSnapshotsByGUNResponse) ProtoReflect() protoreflect.Message { - mi := &file_snapshots_proto_msgTypes[16] - 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 ListSnapshotsByGUNResponse.ProtoReflect.Descriptor instead. -func (*ListSnapshotsByGUNResponse) Descriptor() ([]byte, []int) { - return file_snapshots_proto_rawDescGZIP(), []int{16} -} - -func (x *ListSnapshotsByGUNResponse) GetUUIDs() [][]byte { - if x != nil { - return x.UUIDs - } - return nil -} - -func (x *ListSnapshotsByGUNResponse) GetPagination() *PaginationResponse { - if x != nil { - return x.Pagination - } - return nil -} - -var File_snapshots_proto protoreflect.FileDescriptor - -const file_snapshots_proto_rawDesc = "" + - "\n" + - "\x0fsnapshots.proto\x12\tsnapshots\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fbookmarks.proto\x1a\vitems.proto\x1a\n" + - "util.proto\"\x82\x01\n" + - "\bSnapshot\x127\n" + - "\bmetadata\x18\x01 \x01(\v2\x1b.snapshots.SnapshotMetadataR\bmetadata\x12=\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + - "properties\"\xac\x01\n" + - "\x12SnapshotProperties\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12 \n" + - "\aqueries\x18\x03 \x03(\v2\x06.QueryR\aqueries\x12\x1b\n" + - "\x05items\x18\x05 \x03(\v2\x05.ItemR\x05items\x12\x1b\n" + - "\x05edges\x18\x06 \x03(\v2\x05.EdgeR\x05edgesJ\x04\b\x04\x10\x05\"\\\n" + - "\x10SnapshotMetadata\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + - "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\"\x16\n" + - "\x14ListSnapshotsRequest\"I\n" + - "\x14ListSnapshotResponse\x121\n" + - "\tsnapshots\x18\x01 \x03(\v2\x13.snapshots.SnapshotR\tsnapshots\"V\n" + - "\x15CreateSnapshotRequest\x12=\n" + - "\n" + - "properties\x18\x01 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + - "properties\"I\n" + - "\x16CreateSnapshotResponse\x12/\n" + - "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"(\n" + - "\x12GetSnapshotRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"F\n" + - "\x13GetSnapshotResponse\x12/\n" + - "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"j\n" + - "\x15UpdateSnapshotRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12=\n" + - "\n" + - "properties\x18\x02 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + - "properties\"I\n" + - "\x16UpdateSnapshotResponse\x12/\n" + - "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"+\n" + - "\x15DeleteSnapshotRequest\x12\x12\n" + - "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x18\n" + - "\x16DeleteSnapshotResponse\"\x17\n" + - "\x15GetInitialDataRequest\"\xaa\x01\n" + - "\x16GetInitialDataResponse\x12E\n" + - "\x13blastRadiusSnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\x13blastRadiusSnapshot\x12I\n" + - "\x15changingItemsBookmark\x18\x02 \x01(\v2\x13.bookmarks.BookmarkR\x15changingItemsBookmark\"\x7f\n" + - "\x19ListSnapshotsByGUNRequest\x12.\n" + - "\x12globallyUniqueName\x18\x01 \x01(\tR\x12globallyUniqueName\x122\n" + - "\n" + - "pagination\x18\x02 \x01(\v2\x12.PaginationRequestR\n" + - "pagination\"g\n" + - "\x1aListSnapshotsByGUNResponse\x12\x14\n" + - "\x05UUIDs\x18\x01 \x03(\fR\x05UUIDs\x123\n" + - "\n" + - "pagination\x18\x02 \x01(\v2\x13.PaginationResponseR\n" + - "pagination2\x9a\x04\n" + - "\x10SnapshotsService\x12Q\n" + - "\rListSnapshots\x12\x1f.snapshots.ListSnapshotsRequest\x1a\x1f.snapshots.ListSnapshotResponse\x12U\n" + - "\x0eCreateSnapshot\x12 .snapshots.CreateSnapshotRequest\x1a!.snapshots.CreateSnapshotResponse\x12L\n" + - "\vGetSnapshot\x12\x1d.snapshots.GetSnapshotRequest\x1a\x1e.snapshots.GetSnapshotResponse\x12U\n" + - "\x0eUpdateSnapshot\x12 .snapshots.UpdateSnapshotRequest\x1a!.snapshots.UpdateSnapshotResponse\x12U\n" + - "\x0eDeleteSnapshot\x12 .snapshots.DeleteSnapshotRequest\x1a!.snapshots.DeleteSnapshotResponse\x12`\n" + - "\x11ListSnapshotByGUN\x12$.snapshots.ListSnapshotsByGUNRequest\x1a%.snapshots.ListSnapshotsByGUNResponseB.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_snapshots_proto_rawDescOnce sync.Once - file_snapshots_proto_rawDescData []byte -) - -func file_snapshots_proto_rawDescGZIP() []byte { - file_snapshots_proto_rawDescOnce.Do(func() { - file_snapshots_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_snapshots_proto_rawDesc), len(file_snapshots_proto_rawDesc))) - }) - return file_snapshots_proto_rawDescData -} - -var file_snapshots_proto_msgTypes = make([]protoimpl.MessageInfo, 17) -var file_snapshots_proto_goTypes = []any{ - (*Snapshot)(nil), // 0: snapshots.Snapshot - (*SnapshotProperties)(nil), // 1: snapshots.SnapshotProperties - (*SnapshotMetadata)(nil), // 2: snapshots.SnapshotMetadata - (*ListSnapshotsRequest)(nil), // 3: snapshots.ListSnapshotsRequest - (*ListSnapshotResponse)(nil), // 4: snapshots.ListSnapshotResponse - (*CreateSnapshotRequest)(nil), // 5: snapshots.CreateSnapshotRequest - (*CreateSnapshotResponse)(nil), // 6: snapshots.CreateSnapshotResponse - (*GetSnapshotRequest)(nil), // 7: snapshots.GetSnapshotRequest - (*GetSnapshotResponse)(nil), // 8: snapshots.GetSnapshotResponse - (*UpdateSnapshotRequest)(nil), // 9: snapshots.UpdateSnapshotRequest - (*UpdateSnapshotResponse)(nil), // 10: snapshots.UpdateSnapshotResponse - (*DeleteSnapshotRequest)(nil), // 11: snapshots.DeleteSnapshotRequest - (*DeleteSnapshotResponse)(nil), // 12: snapshots.DeleteSnapshotResponse - (*GetInitialDataRequest)(nil), // 13: snapshots.GetInitialDataRequest - (*GetInitialDataResponse)(nil), // 14: snapshots.GetInitialDataResponse - (*ListSnapshotsByGUNRequest)(nil), // 15: snapshots.ListSnapshotsByGUNRequest - (*ListSnapshotsByGUNResponse)(nil), // 16: snapshots.ListSnapshotsByGUNResponse - (*Query)(nil), // 17: Query - (*Item)(nil), // 18: Item - (*Edge)(nil), // 19: Edge - (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp - (*Bookmark)(nil), // 21: bookmarks.Bookmark - (*PaginationRequest)(nil), // 22: PaginationRequest - (*PaginationResponse)(nil), // 23: PaginationResponse -} -var file_snapshots_proto_depIdxs = []int32{ - 2, // 0: snapshots.Snapshot.metadata:type_name -> snapshots.SnapshotMetadata - 1, // 1: snapshots.Snapshot.properties:type_name -> snapshots.SnapshotProperties - 17, // 2: snapshots.SnapshotProperties.queries:type_name -> Query - 18, // 3: snapshots.SnapshotProperties.items:type_name -> Item - 19, // 4: snapshots.SnapshotProperties.edges:type_name -> Edge - 20, // 5: snapshots.SnapshotMetadata.created:type_name -> google.protobuf.Timestamp - 0, // 6: snapshots.ListSnapshotResponse.snapshots:type_name -> snapshots.Snapshot - 1, // 7: snapshots.CreateSnapshotRequest.properties:type_name -> snapshots.SnapshotProperties - 0, // 8: snapshots.CreateSnapshotResponse.snapshot:type_name -> snapshots.Snapshot - 0, // 9: snapshots.GetSnapshotResponse.snapshot:type_name -> snapshots.Snapshot - 1, // 10: snapshots.UpdateSnapshotRequest.properties:type_name -> snapshots.SnapshotProperties - 0, // 11: snapshots.UpdateSnapshotResponse.snapshot:type_name -> snapshots.Snapshot - 0, // 12: snapshots.GetInitialDataResponse.blastRadiusSnapshot:type_name -> snapshots.Snapshot - 21, // 13: snapshots.GetInitialDataResponse.changingItemsBookmark:type_name -> bookmarks.Bookmark - 22, // 14: snapshots.ListSnapshotsByGUNRequest.pagination:type_name -> PaginationRequest - 23, // 15: snapshots.ListSnapshotsByGUNResponse.pagination:type_name -> PaginationResponse - 3, // 16: snapshots.SnapshotsService.ListSnapshots:input_type -> snapshots.ListSnapshotsRequest - 5, // 17: snapshots.SnapshotsService.CreateSnapshot:input_type -> snapshots.CreateSnapshotRequest - 7, // 18: snapshots.SnapshotsService.GetSnapshot:input_type -> snapshots.GetSnapshotRequest - 9, // 19: snapshots.SnapshotsService.UpdateSnapshot:input_type -> snapshots.UpdateSnapshotRequest - 11, // 20: snapshots.SnapshotsService.DeleteSnapshot:input_type -> snapshots.DeleteSnapshotRequest - 15, // 21: snapshots.SnapshotsService.ListSnapshotByGUN:input_type -> snapshots.ListSnapshotsByGUNRequest - 4, // 22: snapshots.SnapshotsService.ListSnapshots:output_type -> snapshots.ListSnapshotResponse - 6, // 23: snapshots.SnapshotsService.CreateSnapshot:output_type -> snapshots.CreateSnapshotResponse - 8, // 24: snapshots.SnapshotsService.GetSnapshot:output_type -> snapshots.GetSnapshotResponse - 10, // 25: snapshots.SnapshotsService.UpdateSnapshot:output_type -> snapshots.UpdateSnapshotResponse - 12, // 26: snapshots.SnapshotsService.DeleteSnapshot:output_type -> snapshots.DeleteSnapshotResponse - 16, // 27: snapshots.SnapshotsService.ListSnapshotByGUN:output_type -> snapshots.ListSnapshotsByGUNResponse - 22, // [22:28] is the sub-list for method output_type - 16, // [16:22] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name -} - -func init() { file_snapshots_proto_init() } -func file_snapshots_proto_init() { - if File_snapshots_proto != nil { - return - } - file_bookmarks_proto_init() - file_items_proto_init() - file_util_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_snapshots_proto_rawDesc), len(file_snapshots_proto_rawDesc)), - NumEnums: 0, - NumMessages: 17, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_snapshots_proto_goTypes, - DependencyIndexes: file_snapshots_proto_depIdxs, - MessageInfos: file_snapshots_proto_msgTypes, - }.Build() - File_snapshots_proto = out.File - file_snapshots_proto_goTypes = nil - file_snapshots_proto_depIdxs = nil -} diff --git a/sdp-go/test_utils.go b/sdp-go/test_utils.go deleted file mode 100644 index 0ca45d10..00000000 --- a/sdp-go/test_utils.go +++ /dev/null @@ -1,240 +0,0 @@ -package sdp - -import ( - "context" - "errors" - "fmt" - "math/rand" - "regexp" - "strings" - sync "sync" - - "github.com/nats-io/nats.go" - "google.golang.org/protobuf/proto" -) - -type ResponseMessage struct { - Subject string - V interface{} -} - -// TestConnection Used to mock a NATS connection for testing -type TestConnection struct { - Messages []ResponseMessage - MessagesMu sync.Mutex - - // If set, the test connection will not return ErrNoResponders if someone - // tries to publish a message to a subject with no responders - IgnoreNoResponders bool - - Subscriptions map[*regexp.Regexp][]nats.MsgHandler - subscriptionsMutex sync.RWMutex -} - -// assert interface implementation -var _ EncodedConnection = (*TestConnection)(nil) - -// Publish Test publish method, notes down the subject and the message -func (t *TestConnection) Publish(ctx context.Context, subj string, m proto.Message) error { - t.MessagesMu.Lock() - t.Messages = append(t.Messages, ResponseMessage{ - Subject: subj, - V: m, - }) - t.MessagesMu.Unlock() - - data, err := proto.Marshal(m) - if err != nil { - return err - } - msg := nats.Msg{ - Subject: subj, - Data: data, - } - return t.runHandlers(&msg) -} - -// PublishRequest Test publish method, notes down the subject and the message -func (t *TestConnection) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { - t.MessagesMu.Lock() - t.Messages = append(t.Messages, ResponseMessage{ - Subject: subj, - V: m, - }) - t.MessagesMu.Unlock() - - data, err := proto.Marshal(m) - if err != nil { - return err - } - msg := nats.Msg{ - Subject: subj, - Data: data, - Header: nats.Header{}, - } - msg.Header.Add("reply-to", replyTo) - return t.runHandlers(&msg) -} - -// PublishMsg Test publish method, notes down the subject and the message -func (t *TestConnection) PublishMsg(ctx context.Context, msg *nats.Msg) error { - t.MessagesMu.Lock() - t.Messages = append(t.Messages, ResponseMessage{ - Subject: msg.Subject, - V: msg.Data, - }) - t.MessagesMu.Unlock() - - err := t.runHandlers(msg) - if err != nil { - return err - } - - return nil -} - -func (t *TestConnection) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { - t.subscriptionsMutex.Lock() - defer t.subscriptionsMutex.Unlock() - - if t.Subscriptions == nil { - t.Subscriptions = make(map[*regexp.Regexp][]nats.MsgHandler) - } - - regex := t.subjectToRegexp(subj) - - t.Subscriptions[regex] = append(t.Subscriptions[regex], cb) - - return nil, nil -} - -func (t *TestConnection) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { - // TODO: implement queue groups here - return t.Subscribe(subj, cb) -} - -func (r *TestConnection) subjectToRegexp(subject string) *regexp.Regexp { - // If the subject contains a > then handle this - if strings.Contains(subject, ">") { - // Escape regex to literal - quoted := regexp.QuoteMeta(subject) - - // Replace > with .*$ - return regexp.MustCompile(strings.ReplaceAll(quoted, ">", ".*$")) - } - - if strings.Contains(subject, "*") { - // Escape regex to literal - quoted := regexp.QuoteMeta(subject) - - // Replace \* with \w+ - return regexp.MustCompile(strings.ReplaceAll(quoted, `\*`, `\w+`)) - } - - return regexp.MustCompile(regexp.QuoteMeta(subject)) -} - -// RequestMsg Simulates a request on the given subject, assigns a random -// response subject then calls the handler on the given subject, we are -// expecting the handler to be in the format: func(msg *nats.Msg) -func (t *TestConnection) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { - replySubject := randSeq(10) - msg.Reply = replySubject - replies := make(chan interface{}, 128) - - // Subscribe to the reply subject - _, err := t.Subscribe(replySubject, func(msg *nats.Msg) { - replies <- msg - }) - if err != nil { - return nil, err - } - // Run the handlers - err = t.runHandlers(msg) - if err != nil { - return nil, err - } - - // Return the first result - select { - case reply, ok := <-replies: - if ok { - if m, ok := reply.(*nats.Msg); ok { - return &nats.Msg{ - Subject: replySubject, - Data: m.Data, - }, nil - } else { - return nil, fmt.Errorf("reply was not a *nats.Msg, but a %T", reply) - } - } else { - return nil, errors.New("no replies") - } - case <-ctx.Done(): - return nil, ctx.Err() - } -} - -// Status Always returns nats.CONNECTED -func (n *TestConnection) Status() nats.Status { - return nats.CONNECTED -} - -// Stats Always returns empty/zero nats.Statistics -func (n *TestConnection) Stats() nats.Statistics { - return nats.Statistics{} -} - -// LastError Always returns nil -func (n *TestConnection) LastError() error { - return nil -} - -// Drain Always returns nil -func (n *TestConnection) Drain() error { - return nil -} - -// Close Does nothing -func (n *TestConnection) Close() {} - -// Underlying Always returns nil -func (n *TestConnection) Underlying() *nats.Conn { - return &nats.Conn{} -} - -// Drop Does nothing -func (n *TestConnection) Drop() {} - -// runHandlers Runs the handlers for a given subject -func (t *TestConnection) runHandlers(msg *nats.Msg) error { - t.subscriptionsMutex.RLock() - defer t.subscriptionsMutex.RUnlock() - - var hasResponder bool - - for subjectRegex, handlers := range t.Subscriptions { - if subjectRegex.MatchString(msg.Subject) { - for _, handler := range handlers { - hasResponder = true - handler(msg) - } - } - } - - if hasResponder || t.IgnoreNoResponders { - return nil - } else { - return nats.ErrNoResponders - } -} - -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randSeq(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] //nolint:gosec // This is not for security - } - return string(b) -} diff --git a/sdp-go/test_utils_test.go b/sdp-go/test_utils_test.go deleted file mode 100644 index 7eaf67a1..00000000 --- a/sdp-go/test_utils_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package sdp - -import ( - "context" - "testing" - - "github.com/nats-io/nats.go" - "google.golang.org/protobuf/proto" -) - -func TestRequest(t *testing.T) { - tc := TestConnection{} - - t.Run("with a regular subject", func(t *testing.T) { - // Create the responder - _, err := tc.Subscribe("test", func(msg *nats.Msg) { - err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ - ResponseType: &GatewayResponse_Error{ - Error: "testing", - }, - }) - if err2 != nil { - t.Error(err2) - } - }) - if err != nil { - t.Fatal(err) - } - - request := &GatewayRequest{} - - data, err := proto.Marshal(request) - if err != nil { - t.Fatal(err) - } - - msg := nats.Msg{ - Subject: "test", - Data: data, - } - replyMsg, err := tc.RequestMsg(context.Background(), &msg) - if err != nil { - t.Fatal(err) - } - - response := &GatewayResponse{} - err = proto.Unmarshal(replyMsg.Data, response) - if err != nil { - t.Error(err) - } - - if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { - t.Errorf("expected error to be 'testing', got '%v'", response) - } - }) - - t.Run("with a > wildcard subject", func(t *testing.T) { - // Create the responder - _, err := tc.Subscribe("test.>", func(msg *nats.Msg) { - err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ - ResponseType: &GatewayResponse_Error{ - Error: "testing", - }, - }) - if err2 != nil { - t.Error(err2) - } - }) - if err != nil { - t.Fatal(err) - } - - request := &GatewayRequest{} - - data, err := proto.Marshal(request) - if err != nil { - t.Fatal(err) - } - - msg := nats.Msg{ - Subject: "test.foo.bar", - Data: data, - } - replyMsg, err := tc.RequestMsg(context.Background(), &msg) - if err != nil { - t.Fatal(err) - } - - response := &GatewayResponse{} - err = proto.Unmarshal(replyMsg.Data, response) - if err != nil { - t.Error(err) - } - - if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { - t.Errorf("expected error to be 'testing', got '%v'", response) - } - }) - - t.Run("with a * wildcard subject", func(t *testing.T) { - // Create the responder - _, err := tc.Subscribe("test.*.bar", func(msg *nats.Msg) { - err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ - ResponseType: &GatewayResponse_Error{ - Error: "testing", - }, - }) - if err2 != nil { - t.Error(err2) - } - }) - if err != nil { - t.Fatal(err) - } - - request := &GatewayRequest{} - - data, err := proto.Marshal(request) - if err != nil { - t.Fatal(err) - } - - msg := nats.Msg{ - Subject: "test.foo.bar", - Data: data, - } - replyMsg, err := tc.RequestMsg(context.Background(), &msg) - if err != nil { - t.Fatal(err) - } - - response := &GatewayResponse{} - err = proto.Unmarshal(replyMsg.Data, response) - if err != nil { - t.Error(err) - } - - if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { - t.Errorf("expected error to be 'testing', got '%v'", response) - } - }) - -} diff --git a/sdp-go/tracing.go b/sdp-go/tracing.go deleted file mode 100644 index d9ad2a79..00000000 --- a/sdp-go/tracing.go +++ /dev/null @@ -1,106 +0,0 @@ -package sdp - -import ( - "context" - - "connectrpc.com/connect" - "github.com/getsentry/sentry-go" - "github.com/nats-io/nats.go" - "github.com/overmindtech/cli/tracing" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" -) - -type CtxMsgHandler func(ctx context.Context, msg *nats.Msg) - -func NewOtelExtractingHandler(spanName string, h CtxMsgHandler, t trace.Tracer, spanOpts ...trace.SpanStartOption) nats.MsgHandler { - if h == nil { - return nil - } - - return func(msg *nats.Msg) { - ctx := context.Background() - - ctx = otel.GetTextMapPropagator().Extract(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) - - // don't start a span when we have no spanName - if spanName != "" { - var span trace.Span - ctx, span = t.Start(ctx, spanName, spanOpts...) - defer span.End() - } - - h(ctx, msg) - } -} - -func NewAsyncOtelExtractingHandler(spanName string, h CtxMsgHandler, t trace.Tracer, spanOpts ...trace.SpanStartOption) nats.MsgHandler { - if h == nil { - return nil - } - - return func(msg *nats.Msg) { - go func() { - defer sentry.Recover() - - ctx := context.Background() - ctx = otel.GetTextMapPropagator().Extract(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) - - // don't start a span when we have no spanName - if spanName != "" { - var span trace.Span - ctx, span = t.Start(ctx, spanName, spanOpts...) - defer span.End() - } - - h(ctx, msg) - }() - } -} - -func InjectOtelTraceContext(ctx context.Context, msg *nats.Msg) { - if msg.Header == nil { - msg.Header = make(nats.Header) - } - - otel.GetTextMapPropagator().Inject(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) -} - -type sentryInterceptor struct{} - -// NewSentryInterceptor pass this to connect handlers as `connect.WithInterceptors(NewSentryInterceptor())` to recover from panics in the handler and report them to sentry. Otherwise panics get recovered by connect-go itself and do not get reported to sentry. -func NewSentryInterceptor() connect.Interceptor { - return &sentryInterceptor{} -} - -func (i *sentryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { - // Same as previous UnaryInterceptorFunc. - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - defer sentry.Recover() - return next(ctx, req) - }) -} - -func (*sentryInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { - return connect.StreamingClientFunc(func( - ctx context.Context, - spec connect.Spec, - ) connect.StreamingClientConn { - defer sentry.Recover() - conn := next(ctx, spec) - return conn - }) -} - -func (i *sentryInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { - return connect.StreamingHandlerFunc(func( - ctx context.Context, - conn connect.StreamingHandlerConn, - ) error { - defer sentry.Recover() - return next(ctx, conn) - }) -} diff --git a/sdp-go/tracing_test.go b/sdp-go/tracing_test.go deleted file mode 100644 index e3416629..00000000 --- a/sdp-go/tracing_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package sdp - -import ( - "context" - "testing" - - "github.com/nats-io/nats.go" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - sdktrace "go.opentelemetry.io/otel/sdk/trace" -) - -func TestTraceContextPropagation(t *testing.T) { - tp := sdktrace.NewTracerProvider() - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - - tc := TestConnection{ - Messages: make([]ResponseMessage, 0), - } - - outerCtx := context.Background() - outerCtx, outerSpan := tp.Tracer("outerTracer").Start(outerCtx, "outer span") - defer outerSpan.End() - // outerJson, err := outerSpan.SpanContext().MarshalJSON() - // if err != nil { - // t.Errorf("error marshalling outerSpan: %v", err) - // } else { - // if !bytes.Equal(outerJson, []byte("{\"TraceID\":\"00000000000000000000000000000000\",\"SpanID\":\"0000000000000000\",\"TraceFlags\":\"00\",\"TraceState\":\"\",\"Remote\":false}")) { - // t.Errorf("outer span has unexpected context: %v", string(outerJson)) - // } - // } - handlerCalled := make(chan struct{}) - _, err := tc.Subscribe("test.subject", NewOtelExtractingHandler("inner span", func(innerCtx context.Context, msg *nats.Msg) { - _, innerSpan := tp.Tracer("innerTracer").Start(innerCtx, "innerSpan") - // innerJson, err := innerSpan.SpanContext().MarshalJSON() - // if err != nil { - // t.Errorf("error marshalling innerSpan: %v", err) - // } else { - // if !bytes.Equal(innerJson, []byte("{\"TraceID\":\"00000000000000000000000000000000\",\"SpanID\":\"0000000000000000\",\"TraceFlags\":\"00\",\"TraceState\":\"\",\"Remote\":false}")) { - // t.Errorf("inner span has unexpected context: %v", string(innerJson)) - // } - // } - if innerSpan.SpanContext().TraceID() != outerSpan.SpanContext().TraceID() { - t.Error("inner span did not link up to outer span") - } - - // clean up - innerSpan.End() - - // finish the test - handlerCalled <- struct{}{} - }, tp.Tracer("providedTracer"))) - if err != nil { - t.Errorf("error subscribing: %v", err) - } - - m := &nats.Msg{ - Subject: "test.subject", - Data: make([]byte, 0), - } - - go func() { - InjectOtelTraceContext(outerCtx, m) - err = tc.PublishMsg(outerCtx, m) - if err != nil { - t.Errorf("error publishing message: %v", err) - } - }() - - // Wait for the handler to be called - <-handlerCalled -} diff --git a/sdp-go/util.go b/sdp-go/util.go deleted file mode 100644 index aac366c3..00000000 --- a/sdp-go/util.go +++ /dev/null @@ -1,156 +0,0 @@ -package sdp - -import ( - "fmt" - "math" - "strings" - - "github.com/google/uuid" -) - -// CalculatePaginationOffsetLimit Calculates the offset and limit for pagination -// in SQL queries, along with the current page and total pages that should be -// included in the response -// -// This also sets sane defaults for the page size if pagination is not provided. -// These defaults are page 1 with a page size of 10 -// -// NOTE: If there are no items, then this will return 0 for all values -func CalculatePaginationOffsetLimit(pagination *PaginationRequest, totalItems int32) (offset, limit, page, totalPages int32) { - if totalItems == 0 { - // If there are no items, there are no pages - return 0, 0, 0, 0 - } - - var requestedPageSize int32 - var requestedPage int32 - - if pagination == nil { - // Set sane defaults - requestedPageSize = 10 - requestedPage = 1 - } else { - requestedPageSize = pagination.GetPageSize() - requestedPage = pagination.GetPage() - } - - // pagesize is at least 10, at most 100 - limit = min(100, max(10, requestedPageSize)) - // calculate the total number of pages - totalPages = int32(math.Ceil(float64(totalItems) / float64(limit))) - - // page has to be at least 1, and at most totalPages - page = min(totalPages, requestedPage) - page = max(1, page) - - // calculate the offset - if totalPages == 0 { - offset = 0 - } else { - offset = (page * limit) - limit - } - return offset, limit, page, totalPages -} - -// An object that returns all of the adapter metadata for a given source -type AdapterMetadataProvider interface { - AllAdapterMetadata() []*AdapterMetadata -} - -// A list of adapter metadata, this is used to store all the adapter metadata -// for a given source so that it can be retrieved later for the purposes of -// generating documentation and Terraform mappings -type AdapterMetadataList struct { - // The list of adapter metadata - list []*AdapterMetadata -} - -// AllAdapterMetadata returns all the adapter metadata -func (a *AdapterMetadataList) AllAdapterMetadata() []*AdapterMetadata { - return a.list -} - -// RegisterAdapterMetadata registers a new adapter metadata with the list and -// returns a pointer to that same metadata to be used elsewhere -func (a *AdapterMetadataList) Register(metadata *AdapterMetadata) *AdapterMetadata { - if a == nil { - return metadata - } - - a.list = append(a.list, metadata) - - return metadata -} - -type RoutineRollUp struct { - ChangeId uuid.UUID - Gun string - Attr string - Value string -} - -func (rr RoutineRollUp) String() string { - val := fmt.Sprintf("%v", rr.Value) - if len(val) > 100 { - val = val[:100] - } - val = strings.ReplaceAll(val, "\n", " ") - val = strings.ReplaceAll(val, "\t", " ") - return fmt.Sprintf("change:%v\tgun:%v\tattr:%v\tval:%v", rr.ChangeId, rr.Gun, rr.Attr, val) -} - -func WalkMapToRoutineRollUp(gun string, key string, data map[string]any) []RoutineRollUp { - results := []RoutineRollUp{} - - for k, v := range data { - attr := k - if key != "" { - attr = fmt.Sprintf("%v.%v", key, k) - } - switch val := v.(type) { - case map[string]any: - results = append(results, WalkMapToRoutineRollUp(gun, attr, val)...) - default: - results = append(results, RoutineRollUp{ - Gun: gun, - Attr: attr, - Value: fmt.Sprintf("%v", val), - }) - } - } - - return results -} - -// GcpSANameFromAccountName generates a GCP service account name from the given -// Service account must be 6-30 characters long, and must comply with the -// `^[a-zA-Z][a-zA-Z\d\-]*[a-zA-Z\d]$` regex. -// -// This regex returned from an error message when trying to create a service account. -// Unfortunately, we could not find any documentation on this. -// The account name is expected to be in the format of a UUID, which is 36 characters long, -// and contains dashes. -// The service account name must be 30 characters or less, -// and must start with a letter, end with a letter or digit, and can only contain -// letters, digits, and dashes. -// So we keep the SA name simple: Start with "C-" and take the first 28 characters of the account name. -func GcpSANameFromAccountName(accountName string) string { - if accountName == "" { - return "" - } - - accountName = strings.ReplaceAll(accountName, "-", "") - - if len(accountName) >= 6 { - // Ensure the account name is at most 30 characters long - // We will prefix it with "C-" to ensure it starts with a letter - // and truncate it to 28 characters after the prefix - if len(accountName) > 28 { - accountName = accountName[:28] - } - - return "C-" + accountName - } - - return "" -} diff --git a/sdp-go/util.pb.go b/sdp-go/util.pb.go deleted file mode 100644 index 3e671d4e..00000000 --- a/sdp-go/util.pb.go +++ /dev/null @@ -1,285 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: util.proto - -package sdp - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SortOrder int32 - -const ( - SortOrder_ALPHABETICAL_ASCENDING SortOrder = 0 // A-Z - SortOrder_ALPHABETICAL_DESCENDING SortOrder = 1 // Z-A - SortOrder_DATE_ASCENDING SortOrder = 2 // Oldest first - SortOrder_DATE_DESCENDING SortOrder = 3 // Newest first -) - -// Enum value maps for SortOrder. -var ( - SortOrder_name = map[int32]string{ - 0: "ALPHABETICAL_ASCENDING", - 1: "ALPHABETICAL_DESCENDING", - 2: "DATE_ASCENDING", - 3: "DATE_DESCENDING", - } - SortOrder_value = map[string]int32{ - "ALPHABETICAL_ASCENDING": 0, - "ALPHABETICAL_DESCENDING": 1, - "DATE_ASCENDING": 2, - "DATE_DESCENDING": 3, - } -) - -func (x SortOrder) Enum() *SortOrder { - p := new(SortOrder) - *p = x - return p -} - -func (x SortOrder) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SortOrder) Descriptor() protoreflect.EnumDescriptor { - return file_util_proto_enumTypes[0].Descriptor() -} - -func (SortOrder) Type() protoreflect.EnumType { - return &file_util_proto_enumTypes[0] -} - -func (x SortOrder) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SortOrder.Descriptor instead. -func (SortOrder) EnumDescriptor() ([]byte, []int) { - return file_util_proto_rawDescGZIP(), []int{0} -} - -type PaginationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The number of items to return in a single page. The minimum is 10 and the - // maximum is 100. - PageSize int32 `protobuf:"varint,1,opt,name=pageSize,proto3" json:"pageSize,omitempty"` - // The page number to return. the first page is 1. If the page number is - // larger than the total number of pages, the last page is returned. If the - // page number is negative, the first page 1 is returned. - Page int32 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PaginationRequest) Reset() { - *x = PaginationRequest{} - mi := &file_util_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PaginationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PaginationRequest) ProtoMessage() {} - -func (x *PaginationRequest) ProtoReflect() protoreflect.Message { - mi := &file_util_proto_msgTypes[0] - 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 PaginationRequest.ProtoReflect.Descriptor instead. -func (*PaginationRequest) Descriptor() ([]byte, []int) { - return file_util_proto_rawDescGZIP(), []int{0} -} - -func (x *PaginationRequest) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *PaginationRequest) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -type PaginationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // The number of items in the current page - PageSize int32 `protobuf:"varint,1,opt,name=pageSize,proto3" json:"pageSize,omitempty"` - // The total number of items available. Expensive to calculate - // https://www.cybertec-postgresql.com/en/pagination-problem-total-result-count/ - // this is done as a separate query - TotalItems int32 `protobuf:"varint,2,opt,name=totalItems,proto3" json:"totalItems,omitempty"` - // The current page number, NB if the user provided a negative page number, - // this will be 1, if the user provided a page number larger than the total - // number of pages, this will be the last page. If there are no results at - // all, this will be 0. - Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` - // The total number of pages available. based on the totalItems and pageSize. - // If there are no results this will be zero. - TotalPages int32 `protobuf:"varint,4,opt,name=totalPages,proto3" json:"totalPages,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PaginationResponse) Reset() { - *x = PaginationResponse{} - mi := &file_util_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PaginationResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PaginationResponse) ProtoMessage() {} - -func (x *PaginationResponse) ProtoReflect() protoreflect.Message { - mi := &file_util_proto_msgTypes[1] - 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 PaginationResponse.ProtoReflect.Descriptor instead. -func (*PaginationResponse) Descriptor() ([]byte, []int) { - return file_util_proto_rawDescGZIP(), []int{1} -} - -func (x *PaginationResponse) GetPageSize() int32 { - if x != nil { - return x.PageSize - } - return 0 -} - -func (x *PaginationResponse) GetTotalItems() int32 { - if x != nil { - return x.TotalItems - } - return 0 -} - -func (x *PaginationResponse) GetPage() int32 { - if x != nil { - return x.Page - } - return 0 -} - -func (x *PaginationResponse) GetTotalPages() int32 { - if x != nil { - return x.TotalPages - } - return 0 -} - -var File_util_proto protoreflect.FileDescriptor - -const file_util_proto_rawDesc = "" + - "\n" + - "\n" + - "util.proto\"C\n" + - "\x11PaginationRequest\x12\x1a\n" + - "\bpageSize\x18\x01 \x01(\x05R\bpageSize\x12\x12\n" + - "\x04page\x18\x02 \x01(\x05R\x04page\"\x84\x01\n" + - "\x12PaginationResponse\x12\x1a\n" + - "\bpageSize\x18\x01 \x01(\x05R\bpageSize\x12\x1e\n" + - "\n" + - "totalItems\x18\x02 \x01(\x05R\n" + - "totalItems\x12\x12\n" + - "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1e\n" + - "\n" + - "totalPages\x18\x04 \x01(\x05R\n" + - "totalPages*m\n" + - "\tSortOrder\x12\x1a\n" + - "\x16ALPHABETICAL_ASCENDING\x10\x00\x12\x1b\n" + - "\x17ALPHABETICAL_DESCENDING\x10\x01\x12\x12\n" + - "\x0eDATE_ASCENDING\x10\x02\x12\x13\n" + - "\x0fDATE_DESCENDING\x10\x03B.Z,github.com/overmindtech/workspace/sdp-go;sdpb\x06proto3" - -var ( - file_util_proto_rawDescOnce sync.Once - file_util_proto_rawDescData []byte -) - -func file_util_proto_rawDescGZIP() []byte { - file_util_proto_rawDescOnce.Do(func() { - file_util_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_util_proto_rawDesc), len(file_util_proto_rawDesc))) - }) - return file_util_proto_rawDescData -} - -var file_util_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_util_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_util_proto_goTypes = []any{ - (SortOrder)(0), // 0: SortOrder - (*PaginationRequest)(nil), // 1: PaginationRequest - (*PaginationResponse)(nil), // 2: PaginationResponse -} -var file_util_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_util_proto_init() } -func file_util_proto_init() { - if File_util_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_util_proto_rawDesc), len(file_util_proto_rawDesc)), - NumEnums: 1, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_util_proto_goTypes, - DependencyIndexes: file_util_proto_depIdxs, - EnumInfos: file_util_proto_enumTypes, - MessageInfos: file_util_proto_msgTypes, - }.Build() - File_util_proto = out.File - file_util_proto_goTypes = nil - file_util_proto_depIdxs = nil -} diff --git a/sdp-go/util_test.go b/sdp-go/util_test.go deleted file mode 100644 index 40301926..00000000 --- a/sdp-go/util_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package sdp - -import ( - "fmt" - "regexp" - "testing" -) - -func TestCalculatePaginationOffsetLimit(t *testing.T) { - testCases := []struct { - page int32 - pageSize int32 - totalItems int32 - expectedOffset int32 - expectedLimit int32 - expectedPage int32 - expectedTotalPages int32 - }{ - {page: 2, pageSize: 10, totalItems: 20, expectedOffset: 10, expectedPage: 2, expectedLimit: 10, expectedTotalPages: 2}, - {page: 3, pageSize: 10, totalItems: 25, expectedOffset: 20, expectedPage: 3, expectedLimit: 10, expectedTotalPages: 3}, - {page: 0, pageSize: 5, totalItems: 15, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 2}, - {page: 5, pageSize: 7, totalItems: 23, expectedOffset: 20, expectedPage: 3, expectedLimit: 10, expectedTotalPages: 3}, - {page: 1, pageSize: 10, totalItems: 3, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 1}, - {page: -1, pageSize: 10, totalItems: 1, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 1}, - {page: 1, pageSize: 101, totalItems: 1, expectedOffset: 0, expectedPage: 1, expectedLimit: 100, expectedTotalPages: 1}, - {page: 1, pageSize: 10, totalItems: 0, expectedOffset: 0, expectedPage: 0, expectedLimit: 0, expectedTotalPages: 0}, - } - for _, tc := range testCases { - t.Run(fmt.Sprintf("page%d pagesize%d totalItems%d", tc.page, tc.pageSize, tc.totalItems), func(t *testing.T) { - req := PaginationRequest{ - Page: tc.page, - PageSize: tc.pageSize, - } - offset, limit, correctedPage, pages := CalculatePaginationOffsetLimit(&req, tc.totalItems) - if offset != tc.expectedOffset { - t.Errorf("Expected offset %d, but got %d", tc.expectedOffset, offset) - } - if correctedPage != tc.expectedPage { - t.Errorf("Expected correctedPage %d, but got %d", tc.expectedPage, correctedPage) - } - if limit != tc.expectedLimit { - t.Errorf("Expected limit %d, but got %d", tc.expectedLimit, limit) - } - if pages != tc.expectedTotalPages { - t.Errorf("Expected pages %d, but got %d", tc.expectedTotalPages, pages) - } - }) - } - - t.Run("Default values", func(t *testing.T) { - offset, limit, correctedPage, pages := CalculatePaginationOffsetLimit(nil, 100) - if offset != 0 { - t.Errorf("Expected offset 0, but got %d", offset) - } - if correctedPage != 1 { - t.Errorf("Expected correctedPage 1, but got %d", correctedPage) - } - if limit != 10 { - t.Errorf("Expected limit 10, but got %d", limit) - } - if pages != 10 { - t.Errorf("Expected pages 10, but got %d", pages) - } - }) -} - -func TestGcpSANameFromAccountName(t *testing.T) { - t.Parallel() - - tests := []struct { - accountName string - expected string - }{ - // BEWARE!! If this test needs changing, all currently existing service - // accounts in GCP will need to be updated, which sounds like an unholy - // mess. - {"test-account", "C-testaccount"}, - {"", ""}, - {"6351cbb7-cb45-481a-99cd-909d04a58512", "C-6351cbb7cb45481a99cd909d04a5"}, - {"d408ea46-f4c9-487f-9bf4-b0bcb6843815", "C-d408ea46f4c9487f9bf4b0bcb684"}, - {"63d185c7141237978cfdbaa2", "C-63d185c7141237978cfdbaa2"}, - {"b6c1119a-b80b-4a7b-b8df-acb5348525ac", "C-b6c1119ab80b4a7bb8dfacb53485"}, - } - - pattern := `^[a-zA-Z][a-zA-Z\d\-]*[a-zA-Z\d]$` - - for _, test := range tests { - t.Run(test.accountName, func(t *testing.T) { - result := GcpSANameFromAccountName(test.accountName) - if result != test.expected { - t.Errorf("expected %s, got %s", test.expected, result) - } - - if test.expected != "" { - matched, err := regexp.MatchString(pattern, result) - if err != nil { - t.Fatalf("failed to compile regex: %v", err) - } - if !matched { - t.Errorf("result %q does not match regex %q", result, pattern) - } - - if len(result) > 30 { - t.Errorf("result %q exceeds 30 characters", result) - } - - if len(result) < 6 { - t.Errorf("result %q is less than 6 characters", result) - } - } - }) - } -} diff --git a/sdp-go/validation.go b/sdp-go/validation.go deleted file mode 100644 index 99596b96..00000000 --- a/sdp-go/validation.go +++ /dev/null @@ -1,148 +0,0 @@ -package sdp - -import ( - "errors" - "fmt" -) - -// Validate Ensures that en item is valid (e.g. contains the required fields) -func (i *Item) Validate() error { - if i == nil { - return errors.New("Item is nil") - } - - if i.GetType() == "" { - return fmt.Errorf("item has empty Type: %v", i.GloballyUniqueName()) - } - - if i.GetUniqueAttribute() == "" { - return fmt.Errorf("item has empty UniqueAttribute: %v", i.GloballyUniqueName()) - } - - if i.GetAttributes() == nil { - return fmt.Errorf("item has nil Attributes: %v", i.GloballyUniqueName()) - } - - if i.GetScope() == "" { - return fmt.Errorf("item has empty Scope: %v", i.GloballyUniqueName()) - } - - if i.UniqueAttributeValue() == "" { - return fmt.Errorf("item has empty UniqueAttributeValue: %v", i.GloballyUniqueName()) - } - - return nil -} - -// Validate Ensures a reference is valid -func (r *Reference) Validate() error { - if r == nil { - return errors.New("reference is nil") - } - if r.GetType() == "" { - return fmt.Errorf("reference has empty Type: %v", r) - } - if r.GetUniqueAttributeValue() == "" { - return fmt.Errorf("reference has empty UniqueAttributeValue: %v", r) - } - if r.GetScope() == "" { - return fmt.Errorf("reference has empty Scope: %v", r) - } - - return nil -} - -// Validate Ensures an edge is valid -func (e *Edge) Validate() error { - if e == nil { - return errors.New("edge is nil") - } - - var err error - - err = e.GetFrom().Validate() - if err != nil { - return err - } - - err = e.GetTo().Validate() - - return err -} - -// Validate Ensures a Response is valid -func (r *Response) Validate() error { - if r == nil { - return errors.New("response is nil") - } - - if r.GetResponder() == "" { - return fmt.Errorf("response has empty Responder: %v", r) - } - - if len(r.GetUUID()) == 0 { - return fmt.Errorf("response has empty UUID: %v", r) - } - - return nil -} - -// Validate Ensures a QueryError is valid -func (e *QueryError) Validate() error { - if e == nil { - return errors.New("queryError is nil") - } - - if len(e.GetUUID()) == 0 { - return fmt.Errorf("queryError has empty UUID: %w", e) - } - - if e.GetErrorString() == "" { - return fmt.Errorf("queryError has empty ErrorString: %w", e) - } - - if e.GetScope() == "" { - return fmt.Errorf("queryError has empty Scope: %w", e) - } - - if e.GetSourceName() == "" { - return fmt.Errorf("queryError has empty SourceName: %w", e) - } - - if e.GetItemType() == "" { - return fmt.Errorf("queryError has empty ItemType: %w", e) - } - - if e.GetResponderName() == "" { - return fmt.Errorf("queryError has empty ResponderName: %w", e) - } - - return nil -} - -// Validate Ensures a Query is valid -func (q *Query) Validate() error { - if q == nil { - return errors.New("query is nil") - } - - if q.GetType() == "" { - return fmt.Errorf("query has empty Type: %v", q) - } - - if q.GetScope() == "" { - return fmt.Errorf("query has empty Scope: %v", q) - } - - if len(q.GetUUID()) != 16 { - return fmt.Errorf("query has invalid UUID: %v", q) - } - - if q.GetMethod() == QueryMethod_GET { - if q.GetQuery() == "" { - return fmt.Errorf("query cannot have empty Query when method is Get: %v", q) - } - } - - return nil -} diff --git a/sdp-go/validation_test.go b/sdp-go/validation_test.go deleted file mode 100644 index fba7c509..00000000 --- a/sdp-go/validation_test.go +++ /dev/null @@ -1,924 +0,0 @@ -package sdp - -import ( - "errors" - "testing" - "time" - - "buf.build/go/protovalidate" - - "github.com/google/uuid" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestValidateItem(t *testing.T) { - t.Run("item is fine", func(t *testing.T) { - err := newItem().Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("Item is nil", func(t *testing.T) { - var i *Item - err := i.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("item has empty Type", func(t *testing.T) { - i := newItem() - - i.Type = "" - - err := i.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("item has empty UniqueAttribute", func(t *testing.T) { - i := newItem() - - i.UniqueAttribute = "" - - err := i.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("item has nil Attributes", func(t *testing.T) { - i := newItem() - - i.Attributes = nil - - err := i.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("item has empty Scope", func(t *testing.T) { - i := newItem() - - i.Scope = "" - - err := i.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("item has empty UniqueAttributeValue", func(t *testing.T) { - i := newItem() - - err := i.GetAttributes().Set(i.GetUniqueAttribute(), "") - if err != nil { - t.Fatal(err) - } - - err = i.Validate() - if err == nil { - t.Error("expected error") - } - }) -} - -func TestValidateReference(t *testing.T) { - t.Run("Reference is fine", func(t *testing.T) { - r := newReference() - - err := r.Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("Reference is nil", func(t *testing.T) { - var r *Reference - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("reference has empty Type", func(t *testing.T) { - r := newReference() - - r.Type = "" - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("reference has empty UniqueAttributeValue", func(t *testing.T) { - r := newReference() - - r.UniqueAttributeValue = "" - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("reference has empty Scope", func(t *testing.T) { - r := newReference() - - r.Scope = "" - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) -} - -func TestValidateEdge(t *testing.T) { - t.Run("Edge is fine", func(t *testing.T) { - e := newEdge() - - err := e.Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("Edge has nil From", func(t *testing.T) { - e := newEdge() - - e.From = nil - - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("Edge has nil To", func(t *testing.T) { - e := newEdge() - - e.To = nil - - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("Edge has invalid From", func(t *testing.T) { - e := newEdge() - - e.From.Type = "" - - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("Edge has invalid To", func(t *testing.T) { - e := newEdge() - - e.To.Scope = "" - - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) -} - -func TestValidateResponse(t *testing.T) { - t.Run("Response is fine", func(t *testing.T) { - r := newResponse() - - err := r.Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("Response is nil", func(t *testing.T) { - var r *Response - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("Response has empty Responder", func(t *testing.T) { - r := newResponse() - r.Responder = "" - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("Response has empty UUID", func(t *testing.T) { - r := newResponse() - r.UUID = nil - - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) -} - -func TestValidateQueryError(t *testing.T) { - t.Run("QueryError is fine", func(t *testing.T) { - e := newQueryError() - - err := e.Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("QueryError is nil", func(t *testing.T) { - - }) - - t.Run("QueryError has empty UUID", func(t *testing.T) { - e := newQueryError() - e.UUID = nil - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("QueryError has empty ErrorString", func(t *testing.T) { - e := newQueryError() - e.ErrorString = "" - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("QueryError has empty Scope", func(t *testing.T) { - e := newQueryError() - e.Scope = "" - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("QueryError has empty SourceName", func(t *testing.T) { - e := newQueryError() - e.SourceName = "" - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("QueryError has empty ItemType", func(t *testing.T) { - e := newQueryError() - e.ItemType = "" - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) - - t.Run("QueryError has empty ResponderName", func(t *testing.T) { - e := newQueryError() - e.ResponderName = "" - err := e.Validate() - - if err == nil { - t.Error("expected error") - } - }) -} - -func TestValidateQuery(t *testing.T) { - t.Run("Query is fine", func(t *testing.T) { - r := newQuery() - - err := r.Validate() - - if err != nil { - t.Error(err) - } - }) - - t.Run("Query is nil", func(t *testing.T) { - - }) - - t.Run("Query has empty Type", func(t *testing.T) { - r := newQuery() - r.Type = "" - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - - }) - - t.Run("Query has empty Scope", func(t *testing.T) { - r := newQuery() - r.Scope = "" - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - - }) - - t.Run("Response has empty UUID", func(t *testing.T) { - r := newQuery() - r.UUID = nil - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - - }) - - t.Run("Query cannot have empty Query when method is Get", func(t *testing.T) { - r := newQuery() - r.Method = QueryMethod_GET - r.Query = "" - err := r.Validate() - - if err == nil { - t.Error("expected error") - } - }) - -} - -func newQuery() *Query { - u := uuid.New() - - return &Query{ - Type: "person", - Method: QueryMethod_GET, - Query: "Dylan", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 1, - }, - Scope: "global", - UUID: u[:], - Deadline: timestamppb.New(time.Now().Add(1 * time.Second)), - IgnoreCache: false, - } -} - -func newQueryError() *QueryError { - u := uuid.New() - - return &QueryError{ - UUID: u[:], - ErrorType: QueryError_OTHER, - ErrorString: "bad", - Scope: "global", - SourceName: "test-source", - ItemType: "test", - ResponderName: "test-responder", - } -} - -func newResponse() *Response { - u := uuid.New() - - ru := uuid.New() - - return &Response{ - Responder: "foo", - ResponderUUID: ru[:], - State: ResponderState_WORKING, - NextUpdateIn: durationpb.New(time.Second), - UUID: u[:], - } -} - -func newEdge() *Edge { - return &Edge{ - From: newReference(), - To: newReference(), - } -} - -func newReference() *Reference { - return &Reference{ - Type: "person", - UniqueAttributeValue: "Dylan", - Scope: "global", - } -} - -func newItem() *Item { - return &Item{ - Type: "user", - UniqueAttribute: "name", - Scope: "test", - // TODO(LIQs): delete empty data - LinkedItemQueries: []*LinkedItemQuery{}, - LinkedItems: []*LinkedItem{}, - Attributes: &ItemAttributes{ - AttrStruct: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": { - Kind: &structpb.Value_StringValue{ - StringValue: "bar", - }, - }, - }, - }, - }, - Metadata: &Metadata{ - SourceName: "users", - SourceQuery: &Query{ - Type: "user", - Method: QueryMethod_LIST, - Query: "*", - RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 12, - }, - Scope: "testScope", - }, - Timestamp: timestamppb.Now(), - SourceDuration: &durationpb.Duration{ - Seconds: 1, - Nanos: 1, - }, - SourceDurationPerItem: &durationpb.Duration{ - Seconds: 0, - Nanos: 500, - }, - }, - } -} - -func TestAdapterMetadataValidation(t *testing.T) { - t.Run("Valid Metadata", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter.test_adapter", - }, - }, - } - - err := protovalidate.Validate(md) - if err != nil { - t.Errorf("expected no errors, got %v", err) - } - }) - t.Run("Empty Terraform mappings is OK", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - } - - err := protovalidate.Validate(md) - if err != nil { - t.Errorf("expected no errors, got %v", err) - } - }) - - t.Run("Empty strings in the potential links", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{""}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter.test_adapter", - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Undefined category", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: 9999, // Undefined category - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter.test_adapter", - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Undefined Terraform query method", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: 9999, // Undefined method - TerraformQueryMap: "aws_test_adapter.test_adapter", - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Malformed Terraform query map - no dots", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter_test_adapter", // no dots! - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Malformed Terraform query map - more than 2 items", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter.test_adapter_id.something_else", // expected 2 items, got 3 - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("With Nil Terraform mapping", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - nil, - { - TerraformMethod: QueryMethod_GET, - TerraformQueryMap: "aws_test_adapter.test_adapter_id", - }, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Missing get description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Missing search description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Missing list description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Empty string in the get description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Empty string in the search description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "", - List: true, - ListDescription: "List test adapters", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) - - t.Run("Empty string in the list description", func(t *testing.T) { - md := &AdapterMetadata{ - Type: "test-adapter", - DescriptiveName: "Test Adapter", - Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - SupportedQueryMethods: &AdapterSupportedQueryMethods{ - Get: true, - GetDescription: "Get a test adapter", - Search: true, - SearchDescription: "Search test adapters", - List: true, - ListDescription: "", - }, - PotentialLinks: []string{"test-link"}, - TerraformMappings: []*TerraformMapping{ - {TerraformQueryMap: "aws_test_adapter.test_adapter"}, - }, - } - - err := protovalidate.Validate(md) - if err == nil { - t.Errorf("expected error, got nil") - } - - var validationError *protovalidate.ValidationError - if !errors.As(err, &validationError) { - t.Errorf("expected validation error, got %T: %v", err, err) - } - }) -} diff --git a/sdpcache/bolt_cache.go b/sdpcache/bolt_cache.go deleted file mode 100644 index 5bd8933c..00000000 --- a/sdpcache/bolt_cache.go +++ /dev/null @@ -1,1505 +0,0 @@ -package sdpcache - -import ( - "bytes" - "context" - "encoding/binary" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "syscall" - "time" - - "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/tracing" - log "github.com/sirupsen/logrus" - "go.etcd.io/bbolt" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/proto" -) - -// Bucket names for bbolt -var ( - itemsBucketName = []byte("items") - expiryBucketName = []byte("expiry") - metaBucketName = []byte("meta") - deletedBytesKey = []byte("deletedBytes") -) - -// DefaultCompactThreshold is the default threshold for triggering compaction (100MB) -const DefaultCompactThreshold = 100 * 1024 * 1024 - -// isDiskFullError checks if an error is due to disk being full (ENOSPC) -func isDiskFullError(err error) bool { - if err == nil { - return false - } - // Check if it wraps ENOSPC - var errno syscall.Errno - if errors.As(err, &errno) && errno == syscall.ENOSPC { - return true - } - // Check using errors.Is for wrapped errors - return errors.Is(err, syscall.ENOSPC) -} - -// encodeCachedEntry serializes a CachedEntry to bytes using protobuf -func encodeCachedEntry(e *sdp.CachedEntry) ([]byte, error) { - return proto.Marshal(e) -} - -// decodeCachedEntry deserializes bytes to a CachedEntry using protobuf -func decodeCachedEntry(data []byte) (*sdp.CachedEntry, error) { - e := &sdp.CachedEntry{} - if err := proto.Unmarshal(data, e); err != nil { - return nil, fmt.Errorf("failed to unmarshal cached entry: %w", err) - } - return e, nil -} - -// toCachedResult converts a CachedEntry to a CachedResult -func cachedEntryToCachedResult(e *sdp.CachedEntry) *CachedResult { - result := &CachedResult{ - Item: e.GetItem(), - Expiry: time.Unix(0, e.GetExpiryUnixNano()), - IndexValues: IndexValues{ - SSTHash: SSTHash(e.GetSstHash()), - UniqueAttributeValue: e.GetUniqueAttributeValue(), - Method: e.GetMethod(), - Query: e.GetQuery(), - }, - } - // Only set Error if it's actually meaningful (not nil and not zero-value) - err := e.GetError() - if err != nil && (err.GetErrorType() != 0 || err.GetErrorString() != "" || err.GetScope() != "" || err.GetSourceName() != "" || err.GetItemType() != "") { - result.Error = err - } - return result -} - -// fromCachedResult creates a CachedEntry from a CachedResult -func fromCachedResult(cr *CachedResult) (*sdp.CachedEntry, error) { - e := &sdp.CachedEntry{ - Item: cr.Item, - ExpiryUnixNano: cr.Expiry.UnixNano(), - UniqueAttributeValue: cr.IndexValues.UniqueAttributeValue, - Method: cr.IndexValues.Method, - Query: cr.IndexValues.Query, - SstHash: string(cr.IndexValues.SSTHash), - } - - if cr.Error != nil { - // Try to cast to QueryError for protobuf serialization - var qErr *sdp.QueryError - if errors.As(cr.Error, &qErr) { - e.Error = qErr - } else { - // For non-QueryError errors, wrap in a QueryError - e.Error = &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: cr.Error.Error(), - } - } - } - - return e, nil -} - -// makeEntryKey creates a key for storing an entry in the items bucket -// Format: {method}|{query}|{uniqueAttributeValue}|{globallyUniqueName} -func makeEntryKey(iv IndexValues, item *sdp.Item) []byte { - var gun string - if item != nil { - gun = item.GloballyUniqueName() - } - key := fmt.Sprintf("%d|%s|%s|%s", iv.Method, iv.Query, iv.UniqueAttributeValue, gun) - return []byte(key) -} - -// makeExpiryKey creates a key for the expiry index -// Format: {expiryNano}|{sstHash}|{entryKey} -func makeExpiryKey(expiry time.Time, sstHash SSTHash, entryKey []byte) []byte { - // Use big-endian encoding for expiry so keys sort chronologically - buf := make([]byte, 8+1+len(sstHash)+1+len(entryKey)) - expiryNano := expiry.UnixNano() - var expiryNanoUint uint64 - if expiryNano < 0 { - expiryNanoUint = 0 - } else { - expiryNanoUint = uint64(expiryNano) - } - binary.BigEndian.PutUint64(buf[0:8], expiryNanoUint) - buf[8] = '|' - copy(buf[9:], []byte(sstHash)) - buf[9+len(sstHash)] = '|' - copy(buf[10+len(sstHash):], entryKey) - return buf -} - -// parseExpiryKey extracts the expiry time, sst hash, and entry key from an expiry key -func parseExpiryKey(key []byte) (time.Time, SSTHash, []byte, error) { - if len(key) < 10 { - return time.Time{}, "", nil, errors.New("expiry key too short") - } - - expiryNanoUint := binary.BigEndian.Uint64(key[0:8]) - expiryNano := int64(expiryNanoUint) - // Check for overflow when converting uint64 to int64 - if expiryNano < 0 && expiryNanoUint > 0 { - expiryNano = 0 - } - expiry := time.Unix(0, expiryNano) - - // Find the separators - rest := key[9:] // skip the first separator - sepIdx := bytes.IndexByte(rest, '|') - if sepIdx < 0 { - return time.Time{}, "", nil, errors.New("invalid expiry key format") - } - - sstHash := SSTHash(rest[:sepIdx]) - entryKey := rest[sepIdx+1:] - - return expiry, sstHash, entryKey, nil -} - -// BoltCache implements the Cache interface using bbolt for persistent storage -type BoltCache struct { - db *bbolt.DB - path string - - // Minimum amount of time to wait between cache purges - MinWaitTime time.Duration - - // CompactThreshold is the number of deleted bytes before triggering compaction - CompactThreshold int64 - - // The timer that is used to trigger the next purge - purgeTimer *time.Timer - - // The time that the purger will run next - nextPurge time.Time - - // Ensures that purge stats like `purgeTimer` and `nextPurge` aren't being - // modified concurrently - purgeMutex sync.Mutex - - // Track deleted bytes for compaction - deletedBytes int64 - deletedMu sync.Mutex - - // Ensures that compaction operations aren't running concurrently - // Read operations use RLock, write operations and compaction use Lock - compactMutex sync.RWMutex - - // Tracks in-flight lookups to prevent duplicate work when multiple - // goroutines request the same cache key simultaneously - pending *pendingWork -} - -// assert interface -var _ Cache = (*BoltCache)(nil) - -// BoltCacheOption is a functional option for configuring BoltCache -type BoltCacheOption func(*BoltCache) - -// WithMinWaitTime sets the minimum wait time between purges -func WithMinWaitTime(d time.Duration) BoltCacheOption { - return func(c *BoltCache) { - c.MinWaitTime = d - } -} - -// WithCompactThreshold sets the threshold for triggering compaction -func WithCompactThreshold(bytes int64) BoltCacheOption { - return func(c *BoltCache) { - c.CompactThreshold = bytes - } -} - -// NewBoltCache creates a new BoltCache at the specified path. -// If a cache file already exists at the path, it will be opened and used. -// The existing file will be automatically handled by the purge process, -// which removes expired items. No explicit cleanup is needed on startup. -func NewBoltCache(path string, opts ...BoltCacheOption) (*BoltCache, error) { - // Ensure the directory exists - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, fmt.Errorf("failed to create directory: %w", err) - } - - // bbolt.Open will open an existing file if present, or create a new one - db, err := bbolt.Open(path, 0o600, &bbolt.Options{ - Timeout: 5 * time.Second, - }) - if err != nil { - return nil, fmt.Errorf("failed to open bolt database: %w", err) - } - - c := &BoltCache{ - db: db, - path: path, - CompactThreshold: DefaultCompactThreshold, - pending: newPendingWork(), - } - - for _, opt := range opts { - opt(c) - } - - // Initialize buckets - if err := c.initBuckets(); err != nil { - db.Close() - return nil, fmt.Errorf("failed to initialize buckets: %w", err) - } - - // Load deleted bytes from meta - if err := c.loadDeletedBytes(); err != nil { - db.Close() - return nil, fmt.Errorf("failed to load deleted bytes: %w", err) - } - - return c, nil -} - -// initBuckets creates the required buckets if they don't exist -func (c *BoltCache) initBuckets() error { - return c.db.Update(func(tx *bbolt.Tx) error { - if _, err := tx.CreateBucketIfNotExists(itemsBucketName); err != nil { - return fmt.Errorf("failed to create items bucket: %w", err) - } - if _, err := tx.CreateBucketIfNotExists(expiryBucketName); err != nil { - return fmt.Errorf("failed to create expiry bucket: %w", err) - } - if _, err := tx.CreateBucketIfNotExists(metaBucketName); err != nil { - return fmt.Errorf("failed to create meta bucket: %w", err) - } - return nil - }) -} - -// loadDeletedBytes loads the deleted bytes counter from the meta bucket -func (c *BoltCache) loadDeletedBytes() error { - return c.db.View(func(tx *bbolt.Tx) error { - meta := tx.Bucket(metaBucketName) - if meta == nil { - return nil - } - - data := meta.Get(deletedBytesKey) - if len(data) == 8 { - deletedBytesUint := binary.BigEndian.Uint64(data) - deletedBytes := int64(deletedBytesUint) - // Check for overflow when converting uint64 to int64 - if deletedBytes < 0 && deletedBytesUint > 0 { - deletedBytes = 0 - } - c.deletedBytes = deletedBytes - } - return nil - }) -} - -// saveDeletedBytes saves the deleted bytes counter to the meta bucket -func (c *BoltCache) saveDeletedBytes(tx *bbolt.Tx) error { - meta := tx.Bucket(metaBucketName) - if meta == nil { - return errors.New("meta bucket not found") - } - - buf := make([]byte, 8) - deletedBytes := c.deletedBytes - var deletedBytesUint uint64 - if deletedBytes < 0 { - deletedBytesUint = 0 - } else { - deletedBytesUint = uint64(deletedBytes) - } - binary.BigEndian.PutUint64(buf, deletedBytesUint) - return meta.Put(deletedBytesKey, buf) -} - -// addDeletedBytes adds to the deleted bytes counter (thread-safe) -func (c *BoltCache) addDeletedBytes(n int64) { - c.deletedMu.Lock() - c.deletedBytes += n - c.deletedMu.Unlock() -} - -// getDeletedBytes returns the current deleted bytes count (thread-safe) -func (c *BoltCache) getDeletedBytes() int64 { - c.deletedMu.Lock() - defer c.deletedMu.Unlock() - return c.deletedBytes -} - -// resetDeletedBytes resets the deleted bytes counter (thread-safe) -func (c *BoltCache) resetDeletedBytes() { - c.deletedMu.Lock() - c.deletedBytes = 0 - c.deletedMu.Unlock() -} - -// getFileSize returns the size of the BoltDB file, logging any errors -func (c *BoltCache) getFileSize() int64 { - if c == nil || c.path == "" { - return 0 - } - - stat, err := os.Stat(c.path) - if err != nil { - if os.IsNotExist(err) { - log.Warnf("BoltDB cache file does not exist: %s", c.path) - } else { - log.WithError(err).Warnf("Failed to stat BoltDB cache file: %s", c.path) - } - return 0 - } - - return stat.Size() -} - -// getDiskUsageMetrics returns disk usage metrics for the BoltDB file -func (c *BoltCache) getDiskUsageMetrics() (fileSize int64, deletedBytes int64) { - if c == nil || c.path == "" { - return 0, 0 - } - - fileSize = c.getFileSize() - deletedBytes = c.getDeletedBytes() - - return fileSize, deletedBytes -} - -// setDiskUsageAttributes sets disk usage attributes on a span -func (c *BoltCache) setDiskUsageAttributes(span trace.Span) { - if c == nil { - return - } - - fileSize, deletedBytes := c.getDiskUsageMetrics() - span.SetAttributes( - attribute.Int64("ovm.boltdb.fileSizeBytes", fileSize), - attribute.Int64("ovm.boltdb.deletedBytes", deletedBytes), - attribute.Int64("ovm.boltdb.compactThresholdBytes", c.CompactThreshold), - ) -} - -// CloseAndDestroy closes the database and deletes the cache file. -// This method makes the destructive behavior explicit. -func (c *BoltCache) CloseAndDestroy() error { - if c == nil { - return nil - } - // Acquire write lock to prevent compaction from interfering - c.compactMutex.Lock() - defer c.compactMutex.Unlock() - - // Get the file path before closing - path := c.db.Path() - - // Close the database - if err := c.db.Close(); err != nil { - return err - } - - // Delete the cache file - return os.Remove(path) -} - -// deleteCacheFile removes the cache file entirely. This is used as a last resort -// when the disk is full and cleanup doesn't help. It closes the database, -// removes the file, and resets internal state. -func (c *BoltCache) deleteCacheFile(ctx context.Context) error { - if c == nil { - return nil - } - - // Create a span for this operation - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFile", trace.WithAttributes( - attribute.String("ovm.cache.path", c.path), - )) - defer span.End() - - // Acquire write lock to prevent compaction from interfering - c.compactMutex.Lock() - defer c.compactMutex.Unlock() - - return c.deleteCacheFileLocked(ctx, span) -} - -// deleteCacheFileLocked is the internal version that assumes the caller already holds compactMutex.Lock() -func (c *BoltCache) deleteCacheFileLocked(ctx context.Context, span trace.Span) error { - // Close the database if it's open - if err := c.db.Close(); err != nil { - span.RecordError(err) - sentry.CaptureException(err) - log.WithContext(ctx).WithError(err).Error("Failed to close database during cache file deletion") - } - - // Remove the cache file - if c.path != "" { - if err := os.Remove(c.path); err != nil && !os.IsNotExist(err) { - span.RecordError(err) - sentry.CaptureException(err) - log.WithContext(ctx).WithError(err).Error("Failed to remove cache file") - return fmt.Errorf("failed to remove cache file: %w", err) - } - span.SetAttributes(attribute.Bool("ovm.cache.file_deleted", true)) - } - - // Reset internal state - c.resetDeletedBytes() - - // Reopen the database - db, err := bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to reopen database") - return fmt.Errorf("failed to reopen database: %w", err) - } - - c.db = db - - // Initialize buckets - if err := c.initBuckets(); err != nil { - _ = db.Close() - return fmt.Errorf("failed to initialize buckets after cache file deletion: %w", err) - } - - return nil -} - -// createDoneFunc returns a done function that calls pending.Complete for the given cache key. -// The done function releases resources and unblocks waiting goroutines. -// The done function is safe to call multiple times (idempotent via sync.Once). -func (c *BoltCache) createDoneFunc(ck CacheKey) func() { - if c == nil || c.pending == nil { - return noopDone - } - key := ck.String() - var once sync.Once - return func() { - once.Do(func() { - c.pending.Complete(key) - }) - } -} - -// Lookup performs a cache lookup for the given query parameters. -func (c *BoltCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.Lookup", - trace.WithAttributes( - attribute.String("ovm.cache.sourceName", srcName), - attribute.String("ovm.cache.method", method.String()), - attribute.String("ovm.cache.scope", scope), - attribute.String("ovm.cache.type", typ), - attribute.String("ovm.cache.query", query), - attribute.Bool("ovm.cache.ignoreCache", ignoreCache), - ), - ) - defer span.End() - - ck := CacheKeyFromParts(srcName, method, scope, typ, query) - - // Set disk usage metrics - c.setDiskUsageAttributes(span) - - if c == nil { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache not initialised"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "cache has not been initialised", - Scope: scope, - SourceName: srcName, - ItemType: typ, - }, noopDone - } - - if ignoreCache { - span.SetAttributes( - attribute.String("ovm.cache.result", "ignore cache"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - // Search already has RLock, so we don't need to add another one here - initialSearchStart := time.Now() - items, err := c.search(ctx, ck) - initialSearchDuration := time.Since(initialSearchStart) - span.SetAttributes( - attribute.Float64("ovm.cache.initialSearchDuration_ms", float64(initialSearchDuration.Milliseconds())), - ) - - if err != nil { - var qErr *sdp.QueryError - if errors.Is(err, ErrCacheNotFound) { - // Cache miss - check if another goroutine is already fetching this data - shouldWork, entry := c.pending.StartWork(ck.String()) - if shouldWork { - // We're the first caller, return miss so caller does the work - span.SetAttributes( - attribute.String("ovm.cache.result", "cache miss"), - attribute.Bool("ovm.cache.hit", false), - attribute.Bool("ovm.cache.workPending", false), - ) - return false, ck, nil, nil, c.createDoneFunc(ck) - } - - // Another goroutine is fetching this data, wait for it to complete - pendingWaitStart := time.Now() - ok := c.pending.Wait(ctx, entry) - pendingWaitDuration := time.Since(pendingWaitStart) - - span.SetAttributes( - attribute.Float64("ovm.cache.pendingWaitDuration_ms", float64(pendingWaitDuration.Milliseconds())), - attribute.Bool("ovm.cache.pendingWaitSuccess", ok), - ) - - if !ok { - // Context was cancelled or work was cancelled, return miss - span.SetAttributes( - attribute.String("ovm.cache.result", "pending work cancelled or timeout"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - // Work is complete, re-check the cache for results - recheckSearchStart := time.Now() - items, recheckErr := c.search(ctx, ck) - recheckSearchDuration := time.Since(recheckSearchStart) - span.SetAttributes( - attribute.Float64("ovm.cache.recheckSearchDuration_ms", float64(recheckSearchDuration.Milliseconds())), - ) - if recheckErr != nil { - if errors.Is(recheckErr, ErrCacheNotFound) { - // Cache still empty after pending work completed - // This is valid - worker may have found nothing or cancelled - span.SetAttributes( - attribute.String("ovm.cache.result", "pending work completed but cache still empty"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - var recheckQErr *sdp.QueryError - if errors.As(recheckErr, &recheckQErr) { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit from pending work: error"), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, nil, recheckQErr, noopDone - } - // Truly unexpected error - return miss - span.SetAttributes( - attribute.String("ovm.cache.result", "unexpected error on re-check"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit from pending work"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, items, nil, noopDone - } else if errors.As(err, &qErr) { - if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { - span.SetAttributes(attribute.String("ovm.cache.result", "cache hit: item not found")) - } else { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: QueryError"), - attribute.String("ovm.cache.error", err.Error()), - ) - } - - span.SetAttributes(attribute.Bool("ovm.cache.hit", true)) - return true, ck, nil, qErr, noopDone - } else { - qErr = &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - Scope: scope, - SourceName: srcName, - ItemType: typ, - } - - span.SetAttributes( - attribute.String("ovm.cache.error", err.Error()), - attribute.String("ovm.cache.result", "cache hit: unknown QueryError"), - attribute.Bool("ovm.cache.hit", true), - ) - - return true, ck, nil, qErr, noopDone - } - } - - if method == sdp.QueryMethod_GET { - if len(items) < 2 { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: 1 item"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, items, nil, noopDone - } else { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache returned >1 value, purging and continuing"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", false), - ) - // Delete already has Lock(), so we can call it directly - c.Delete(ck) - return false, ck, nil, nil, noopDone - } - } - - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: multiple items"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - - // RLock already released above - return true, ck, items, nil, noopDone -} - -// Search performs a lower-level search using a CacheKey. -// If ctx contains a span, detailed timing metrics will be added as span attributes. -func (c *BoltCache) search(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { - if c == nil { - return nil, nil - } - - // Get span from context if available - span := trace.SpanFromContext(ctx) - - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - lockAcquireStart := time.Now() - c.compactMutex.RLock() - lockAcquireDuration := time.Since(lockAcquireStart) - defer c.compactMutex.RUnlock() - - results := make([]*CachedResult, 0) - var itemsScanned int - - txStart := time.Now() - err := c.db.View(func(tx *bbolt.Tx) error { - items := tx.Bucket(itemsBucketName) - if items == nil { - return nil - } - - sstHash := ck.SST.Hash() - sstBucket := items.Bucket([]byte(sstHash)) - if sstBucket == nil { - return nil - } - - now := time.Now() - - // Scan through all entries in this SST bucket - cursor := sstBucket.Cursor() - for k, v := cursor.First(); k != nil; k, v = cursor.Next() { - itemsScanned++ - entry, err := decodeCachedEntry(v) - if err != nil { - continue // Skip corrupted entries - } - - // Check if expired - expiry := time.Unix(0, entry.GetExpiryUnixNano()) - if expiry.Before(now) { - continue - } - - // Check if matches the cache key - entryIV := IndexValues{ - SSTHash: SSTHash(entry.GetSstHash()), - UniqueAttributeValue: entry.GetUniqueAttributeValue(), - Method: entry.GetMethod(), - Query: entry.GetQuery(), - } - if !ck.Matches(entryIV) { - continue - } - - result := cachedEntryToCachedResult(entry) - results = append(results, result) - } - - return nil - }) - txDuration := time.Since(txStart) - - // Add detailed search metrics to span if available - if span.IsRecording() { - span.SetAttributes( - attribute.Int64("ovm.cache.lockAcquireDuration_ms", lockAcquireDuration.Milliseconds()), - attribute.Int64("ovm.cache.txDuration_ms", txDuration.Milliseconds()), - attribute.Int("ovm.cache.itemsScanned", itemsScanned), - attribute.Int("ovm.cache.itemsReturned", len(results)), - ) - } - - if err != nil { - return nil, fmt.Errorf("search failed: %w", err) - } - - if len(results) == 0 { - return nil, ErrCacheNotFound - } - - // Check for errors first - items := make([]*sdp.Item, 0, len(results)) - for _, res := range results { - if res.Error != nil { - return nil, res.Error - } - - if res.Item != nil { - items = append(items, res.Item) - } - } - - return items, nil -} - -// StoreItem stores an item in the cache with the specified duration. -func (c *BoltCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { - if item == nil || c == nil { - return - } - - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - c.compactMutex.RLock() - defer c.compactMutex.RUnlock() - - methodStr := "" - if ck.Method != nil { - methodStr = ck.Method.String() - } - - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.StoreItem", - trace.WithAttributes( - attribute.String("ovm.cache.method", methodStr), - attribute.String("ovm.cache.scope", ck.SST.Scope), - attribute.String("ovm.cache.type", ck.SST.Type), - attribute.String("ovm.cache.sourceName", ck.SST.SourceName), - attribute.String("ovm.cache.itemType", item.GetType()), - attribute.String("ovm.cache.itemScope", item.GetScope()), - attribute.String("ovm.cache.duration", duration.String()), - ), - ) - defer span.End() - - // Set disk usage metrics - c.setDiskUsageAttributes(span) - - // Ensure minimum duration to avoid items expiring immediately - // This handles cases where time.Until() returns 0 or negative due to timing - // Use 100ms to account for race detector overhead and slow CI environments - if duration <= 100*time.Millisecond { - duration = 100 * time.Millisecond - } - - res := CachedResult{ - Item: item, - Error: nil, - Expiry: time.Now().Add(duration), - IndexValues: IndexValues{ - UniqueAttributeValue: item.UniqueAttributeValue(), - }, - } - - if ck.Method != nil { - res.IndexValues.Method = *ck.Method - } - if ck.Query != nil { - res.IndexValues.Query = *ck.Query - } - - res.IndexValues.SSTHash = ck.SST.Hash() - - c.storeResult(ctx, res) -} - -// StoreError stores an error in the cache with the specified duration. -func (c *BoltCache) StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) { - if c == nil || err == nil { - return - } - - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - c.compactMutex.RLock() - defer c.compactMutex.RUnlock() - - methodStr := "" - if ck.Method != nil { - methodStr = ck.Method.String() - } - - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.StoreError", - trace.WithAttributes( - attribute.String("ovm.cache.method", methodStr), - attribute.String("ovm.cache.scope", ck.SST.Scope), - attribute.String("ovm.cache.type", ck.SST.Type), - attribute.String("ovm.cache.sourceName", ck.SST.SourceName), - attribute.String("ovm.cache.error", err.Error()), - attribute.String("ovm.cache.duration", duration.String()), - ), - ) - defer span.End() - - // Set disk usage metrics - c.setDiskUsageAttributes(span) - - // Ensure minimum duration to avoid items expiring immediately - // Use 100ms to account for race detector overhead and slow CI environments - if duration <= 100*time.Millisecond { - duration = 100 * time.Millisecond - } - - res := CachedResult{ - Item: nil, - Error: err, - Expiry: time.Now().Add(duration), - IndexValues: ck.ToIndexValues(), - } - - c.storeResult(ctx, res) -} - -// storeResult stores a CachedResult in the database -func (c *BoltCache) storeResult(ctx context.Context, res CachedResult) { - span := trace.SpanFromContext(ctx) - - entry, err := fromCachedResult(&res) - if err != nil { - return // Silently fail on serialization errors - } - - entryBytes, err := encodeCachedEntry(entry) - if err != nil { - return - } - - entryKey := makeEntryKey(res.IndexValues, res.Item) - expiryKey := makeExpiryKey(res.Expiry, res.IndexValues.SSTHash, entryKey) - - overwritten := false - entrySize := int64(len(entryBytes)) - - // Helper function to perform the actual database update - performUpdate := func() error { - return c.db.Update(func(tx *bbolt.Tx) error { - items := tx.Bucket(itemsBucketName) - if items == nil { - return errors.New("items bucket not found") - } - - // Get or create the SST sub-bucket - sstBucket, err := items.CreateBucketIfNotExists([]byte(res.IndexValues.SSTHash)) - if err != nil { - return fmt.Errorf("failed to create sst bucket: %w", err) - } - - // Check if we're overwriting an unexpired entry - existingData := sstBucket.Get(entryKey) - if existingData != nil { - existingEntry, err := decodeCachedEntry(existingData) - if err == nil { - existingExpiry := time.Unix(0, existingEntry.GetExpiryUnixNano()) - now := time.Now() - if existingExpiry.After(now) { - overwritten = true - timeUntilExpiry := existingExpiry.Sub(now) - - attrs := []attribute.KeyValue{ - attribute.Bool("ovm.cache.unexpired_overwrite", true), - attribute.String("ovm.cache.time_until_expiry", timeUntilExpiry.String()), - attribute.String("ovm.cache.sst_hash", string(res.IndexValues.SSTHash)), - attribute.String("ovm.cache.query_method", res.IndexValues.Method.String()), - } - - if res.Item != nil { - attrs = append(attrs, - attribute.String("ovm.cache.item_type", res.Item.GetType()), - attribute.String("ovm.cache.item_scope", res.Item.GetScope()), - ) - } - - if res.IndexValues.Query != "" { - attrs = append(attrs, attribute.String("ovm.cache.query", res.IndexValues.Query)) - } - - if res.IndexValues.UniqueAttributeValue != "" { - attrs = append(attrs, attribute.String("ovm.cache.unique_attribute", res.IndexValues.UniqueAttributeValue)) - } - - span.SetAttributes(attrs...) - - // Delete old expiry key - expiry := tx.Bucket(expiryBucketName) - if expiry != nil { - oldExpiryKey := makeExpiryKey(existingExpiry, res.IndexValues.SSTHash, entryKey) - _ = expiry.Delete(oldExpiryKey) - } - } - } - } - - // Store the entry - if err := sstBucket.Put(entryKey, entryBytes); err != nil { - return fmt.Errorf("failed to store entry: %w", err) - } - - // Store in expiry index - expiry := tx.Bucket(expiryBucketName) - if expiry == nil { - return errors.New("expiry bucket not found") - } - if err := expiry.Put(expiryKey, nil); err != nil { - return fmt.Errorf("failed to store expiry: %w", err) - } - - return nil - }) - } - - err = performUpdate() - - // Handle disk full errors - // Note: storeResult is called from StoreItem/StoreError which already holds compactMutex.RLock() - // so we use the locked versions to avoid deadlock - if err != nil && isDiskFullError(err) { - // Attempt cleanup by purging expired items - needs to happen in a - // goroutine to avoid deadlocks and get a fresh write lock - go func() { - // we need a fresh write lock to block concurrent compaction and - // deleteCacheFileLocked operations. Retrying performUpdate under - // the write lock will ensure that only one instance of this - // goroutine will actually perform the deleteCacheFileLocked. - c.compactMutex.Lock() - defer c.compactMutex.Unlock() - - ctx, purgeSpan := tracing.Tracer().Start(ctx, "BoltCache.purgeLocked") - defer purgeSpan.End() - c.purgeLocked(ctx, time.Now()) - - // Retry the write operation once - err = performUpdate() - - // If still failing with disk full, delete the cache entirely - use locked version - if err != nil && isDiskFullError(err) { - deleteCtx, deleteSpan := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFileLocked", trace.WithAttributes( - attribute.String("ovm.cache.path", c.path), - )) - defer deleteSpan.End() - _ = c.deleteCacheFileLocked(deleteCtx, deleteSpan) - // After deleting the cache, we can't store the result, so just return - return - } - }() - // now return to release the read lock and allow the goroutine above to run - return - } - - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to store result") - // Update disk usage metrics even on error - c.setDiskUsageAttributes(span) - return - } - - if !overwritten { - span.SetAttributes(attribute.Bool("ovm.cache.unexpired_overwrite", false)) - } - - // Add entry size and update disk usage metrics - span.SetAttributes( - attribute.Int64("ovm.boltdb.entrySizeBytes", entrySize), - ) - c.setDiskUsageAttributes(span) - - // Update the purge time if required - c.setNextPurgeIfEarlier(res.Expiry) -} - -// Delete removes all entries matching the given cache key. -func (c *BoltCache) Delete(ck CacheKey) { - if c == nil { - return - } - - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - c.compactMutex.RLock() - defer c.compactMutex.RUnlock() - - var totalDeleted int64 - - _ = c.db.Update(func(tx *bbolt.Tx) error { - items := tx.Bucket(itemsBucketName) - if items == nil { - return nil - } - - sstHash := ck.SST.Hash() - sstBucket := items.Bucket([]byte(sstHash)) - if sstBucket == nil { - return nil - } - - expiry := tx.Bucket(expiryBucketName) - - // Collect keys to delete - keysToDelete := make([][]byte, 0) - cursor := sstBucket.Cursor() - for k, v := cursor.First(); k != nil; k, v = cursor.Next() { - entry, err := decodeCachedEntry(v) - if err != nil { - continue - } - - entryIV := IndexValues{ - SSTHash: SSTHash(entry.GetSstHash()), - UniqueAttributeValue: entry.GetUniqueAttributeValue(), - Method: entry.GetMethod(), - Query: entry.GetQuery(), - } - if ck.Matches(entryIV) { - keysToDelete = append(keysToDelete, append([]byte(nil), k...)) - totalDeleted += int64(len(k) + len(v)) - - // Delete from expiry index - if expiry != nil { - expiryTime := time.Unix(0, entry.GetExpiryUnixNano()) - expiryKey := makeExpiryKey(expiryTime, SSTHash(entry.GetSstHash()), k) - _ = expiry.Delete(expiryKey) - } - } - } - - // Delete the entries - for _, k := range keysToDelete { - _ = sstBucket.Delete(k) - } - - return nil - }) - - if totalDeleted > 0 { - c.addDeletedBytes(totalDeleted) - } -} - -// Clear removes all entries from the cache. -func (c *BoltCache) Clear() { - if c == nil { - return - } - - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - c.compactMutex.RLock() - defer c.compactMutex.RUnlock() - - _ = c.db.Update(func(tx *bbolt.Tx) error { - // Delete and recreate buckets - _ = tx.DeleteBucket(itemsBucketName) - _ = tx.DeleteBucket(expiryBucketName) - - _, _ = tx.CreateBucketIfNotExists(itemsBucketName) - _, _ = tx.CreateBucketIfNotExists(expiryBucketName) - - // Reset deleted bytes in meta - meta := tx.Bucket(metaBucketName) - if meta != nil { - buf := make([]byte, 8) - _ = meta.Put(deletedBytesKey, buf) - } - - return nil - }) - - c.resetDeletedBytes() -} - -// Purge removes all expired items from the cache. -func (c *BoltCache) Purge(ctx context.Context, before time.Time) PurgeStats { - if c == nil { - return PurgeStats{} - } - - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.Purge", - trace.WithAttributes( - attribute.String("ovm.boltdb.purgeBefore", before.Format(time.RFC3339)), - ), - ) - defer span.End() - - stats := func() PurgeStats { - // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations - c.compactMutex.RLock() - defer c.compactMutex.RUnlock() - - return c.purgeLocked(ctx, before) - }() - - // Check if compaction is needed - deletedBytesBeforeCompact := c.getDeletedBytes() - compactionTriggered := deletedBytesBeforeCompact >= c.CompactThreshold - - if compactionTriggered { - span.SetAttributes( - attribute.Bool("ovm.boltdb.compactionTriggered", true), - attribute.Int64("ovm.boltdb.deletedBytesBeforeCompact", deletedBytesBeforeCompact), - ) - if err := c.compact(ctx); err == nil { - span.SetAttributes(attribute.Bool("ovm.boltdb.compactionSuccess", true)) - } else { - span.RecordError(err) - span.SetAttributes(attribute.Bool("ovm.boltdb.compactionSuccess", false)) - } - } else { - span.SetAttributes(attribute.Bool("ovm.boltdb.compactionTriggered", false)) - } - - return stats -} - -// purgeLocked is the internal version that assumes the caller already holds compactMutex.Lock() -// It performs the actual purging work and returns the stats, but does not handle compaction. -func (c *BoltCache) purgeLocked(ctx context.Context, before time.Time) PurgeStats { - span := trace.SpanFromContext(ctx) - - // Set initial disk usage metrics - c.setDiskUsageAttributes(span) - - start := time.Now() - var nextExpiry *time.Time - numPurged := 0 - var totalDeleted int64 - - // Collect expired entries - type expiredEntry struct { - sstHash SSTHash - entryKey []byte - size int64 - } - expired := make([]expiredEntry, 0) - - _ = c.db.View(func(tx *bbolt.Tx) error { - expiry := tx.Bucket(expiryBucketName) - if expiry == nil { - return nil - } - - items := tx.Bucket(itemsBucketName) - - cursor := expiry.Cursor() - for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { - expiryTime, sstHash, entryKey, err := parseExpiryKey(k) - if err != nil { - continue - } - - if expiryTime.Before(before) { - // Calculate size for deleted bytes tracking - var size int64 - if items != nil { - if sstBucket := items.Bucket([]byte(sstHash)); sstBucket != nil { - if v := sstBucket.Get(entryKey); v != nil { - size = int64(len(k) + len(entryKey) + len(v)) - } - } - } - expired = append(expired, expiredEntry{ - sstHash: sstHash, - entryKey: append([]byte(nil), entryKey...), - size: size, - }) - } else { - // Found first non-expired entry - nextExpiry = &expiryTime - break - } - } - - return nil - }) - - // Delete expired entries - if len(expired) > 0 { - _ = c.db.Update(func(tx *bbolt.Tx) error { - items := tx.Bucket(itemsBucketName) - expiry := tx.Bucket(expiryBucketName) - - for _, e := range expired { - // Delete from items - if items != nil { - if sstBucket := items.Bucket([]byte(e.sstHash)); sstBucket != nil { - _ = sstBucket.Delete(e.entryKey) - } - } - - // Delete from expiry index - if expiry != nil { - // We need to reconstruct the expiry key - cursor := expiry.Cursor() - for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { - _, sstHash, entryKey, err := parseExpiryKey(k) - if err != nil { - continue - } - if sstHash == e.sstHash && bytes.Equal(entryKey, e.entryKey) { - _ = expiry.Delete(k) - break - } - } - } - - totalDeleted += e.size - numPurged++ - } - - // Save deleted bytes - c.addDeletedBytes(totalDeleted) - return c.saveDeletedBytes(tx) - }) - } - - // Update final disk usage metrics - c.setDiskUsageAttributes(span) - - span.SetAttributes( - attribute.Int("ovm.boltdb.numPurged", numPurged), - attribute.Int64("ovm.boltdb.totalDeletedBytes", totalDeleted), - ) - if nextExpiry != nil { - span.SetAttributes(attribute.String("ovm.boltdb.nextExpiry", nextExpiry.Format(time.RFC3339))) - } - - return PurgeStats{ - NumPurged: numPurged, - TimeTaken: time.Since(start), - NextExpiry: nextExpiry, - } -} - -// compact performs database compaction to reclaim disk space -func (c *BoltCache) compact(ctx context.Context) error { - // Acquire global lock to prevent any concurrent bbolt operations - c.compactMutex.Lock() - defer c.compactMutex.Unlock() - - ctx, span := tracing.Tracer().Start(ctx, "BoltCache.compact") - defer span.End() - - // Set initial disk usage metrics - c.setDiskUsageAttributes(span) - - fileSizeBefore := c.getFileSize() - if fileSizeBefore > 0 { - span.SetAttributes(attribute.Int64("ovm.boltdb.fileSizeBeforeBytes", fileSizeBefore)) - } - - // Create a temporary file for the compacted database - tempPath := c.path + ".compact" - - // Helper to handle disk full errors during file operations - // Note: We already hold compactMutex.Lock(), so we use the locked versions - handleDiskFull := func(err error, operation string) error { - if isDiskFullError(err) { - // Attempt cleanup first - use locked version since we already hold the lock - c.purgeLocked(ctx, time.Now()) - // If cleanup didn't help, delete the cache - use locked version - deleteCtx, deleteSpan := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFileLocked", trace.WithAttributes( - attribute.String("ovm.cache.path", c.path), - )) - defer deleteSpan.End() - _ = c.deleteCacheFileLocked(deleteCtx, deleteSpan) - return fmt.Errorf("disk full during %s, cache deleted: %w", operation, err) - } - return err - } - - // Open the destination database - dstDB, err := bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - if err != nil { - if isDiskFullError(err) { - // Attempt cleanup first - use locked version since we already hold the lock - c.purgeLocked(ctx, time.Now()) - // Try again - dstDB, err = bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - if err != nil { - return handleDiskFull(err, "temp database creation") - } - } else { - return fmt.Errorf("failed to create temp database: %w", err) - } - } - - // Compact from source to destination - if err := bbolt.Compact(dstDB, c.db, 0); err != nil { - dstDB.Close() - os.Remove(tempPath) - if isDiskFullError(err) { - // Attempt cleanup first - use locked version since we already hold the lock - c.purgeLocked(ctx, time.Now()) - // Try compaction again - dstDB2, retryErr := bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - if retryErr != nil { - return handleDiskFull(retryErr, "temp database creation after cleanup") - } - if compactErr := bbolt.Compact(dstDB2, c.db, 0); compactErr != nil { - dstDB2.Close() - os.Remove(tempPath) - return handleDiskFull(compactErr, "compaction after cleanup") - } - // Success on retry, continue with dstDB2 - dstDB = dstDB2 - } else { - return fmt.Errorf("compaction failed: %w", err) - } - } - - // Close the destination database - if err := dstDB.Close(); err != nil { - os.Remove(tempPath) - return fmt.Errorf("failed to close temp database: %w", err) - } - - // Close the current database - if err := c.db.Close(); err != nil { - os.Remove(tempPath) - return fmt.Errorf("failed to close database: %w", err) - } - - // Replace the old file with the compacted one - if err := os.Rename(tempPath, c.path); err != nil { - // Try to reopen the original database - c.db, _ = bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - return handleDiskFull(err, "rename") - } - - // Reopen the database - db, err := bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to reopen database") - return fmt.Errorf("failed to reopen database: %w", err) - } - - c.db = db - - // Set final disk usage metrics and compaction results - fileSizeAfter := c.getFileSize() - spaceReclaimed := fileSizeBefore - fileSizeAfter - - span.SetAttributes( - attribute.Int64("ovm.boltdb.fileSizeAfterBytes", fileSizeAfter), - attribute.Int64("ovm.boltdb.spaceReclaimedBytes", spaceReclaimed), - ) - c.setDiskUsageAttributes(span) - - // update deleted bytes after compaction - c.resetDeletedBytes() - _ = c.db.Update(func(tx *bbolt.Tx) error { - return c.saveDeletedBytes(tx) - }) - - return nil -} - -// GetMinWaitTime returns the minimum time between purge operations -func (c *BoltCache) GetMinWaitTime() time.Duration { - if c == nil { - return 0 - } - - if c.MinWaitTime == 0 { - return MinWaitDefault - } - - return c.MinWaitTime -} - -// StartPurger starts a background goroutine that automatically purges expired items. -func (c *BoltCache) StartPurger(ctx context.Context) { - if c == nil { - return - } - - c.purgeMutex.Lock() - if c.purgeTimer == nil { - c.purgeTimer = time.NewTimer(0) - c.purgeMutex.Unlock() - } else { - c.purgeMutex.Unlock() - log.WithContext(ctx).Info("Purger already running") - return // the purger is already running, so we don't need to start it again - } - - go func(ctx context.Context) { - for { - select { - case <-c.purgeTimer.C: - stats := c.Purge(ctx, time.Now()) - c.setNextPurgeFromStats(stats) - case <-ctx.Done(): - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - c.purgeTimer.Stop() - c.purgeTimer = nil - return - } - } - }(ctx) -} - -// setNextPurgeFromStats sets when the next purge should run based on the stats -func (c *BoltCache) setNextPurgeFromStats(stats PurgeStats) { - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - if stats.NextExpiry == nil { - c.purgeTimer.Reset(1000 * time.Hour) - c.nextPurge = time.Now().Add(1000 * time.Hour) - } else { - if time.Until(*stats.NextExpiry) < c.GetMinWaitTime() { - c.purgeTimer.Reset(c.GetMinWaitTime()) - c.nextPurge = time.Now().Add(c.GetMinWaitTime()) - } else { - c.purgeTimer.Reset(time.Until(*stats.NextExpiry)) - c.nextPurge = *stats.NextExpiry - } - } -} - -// setNextPurgeIfEarlier sets the next purge time if the provided time is sooner -func (c *BoltCache) setNextPurgeIfEarlier(t time.Time) { - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - if t.Before(c.nextPurge) { - if c.purgeTimer == nil { - return - } - - c.purgeTimer.Stop() - c.nextPurge = t - c.purgeTimer.Reset(time.Until(t)) - } -} diff --git a/sdpcache/cache.go b/sdpcache/cache.go deleted file mode 100644 index a22aece0..00000000 --- a/sdpcache/cache.go +++ /dev/null @@ -1,1009 +0,0 @@ -package sdpcache - -import ( - "context" - "crypto/sha256" - "errors" - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/getsentry/sentry-go" - "github.com/google/btree" - "github.com/overmindtech/cli/sdp-go" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "google.golang.org/protobuf/proto" -) - -// noopDone is a reusable no-op done function returned when no cleanup is needed -var noopDone = func() {} - -type IndexValues struct { - SSTHash SSTHash - UniqueAttributeValue string - Method sdp.QueryMethod - Query string -} - -type CacheKey struct { - SST SST // *required - UniqueAttributeValue *string - Method *sdp.QueryMethod - Query *string -} - -func CacheKeyFromParts(srcName string, method sdp.QueryMethod, scope string, typ string, query string) CacheKey { - ck := CacheKey{ - SST: SST{ - SourceName: srcName, - Scope: scope, - Type: typ, - }, - } - - switch method { - case sdp.QueryMethod_GET: - // With a Get query we need just the one specific item, so also - // filter on uniqueAttributeValue - ck.UniqueAttributeValue = &query - case sdp.QueryMethod_LIST: - // In the case of a find, we just want everything that was found in - // the last find, so we only care about the method - ck.Method = &method - case sdp.QueryMethod_SEARCH: - // For a search, we only want to get from the cache items that were - // found using search, and with the exact same query - ck.Method = &method - ck.Query = &query - } - - return ck -} - -func CacheKeyFromQuery(q *sdp.Query, srcName string) CacheKey { - return CacheKeyFromParts(srcName, q.GetMethod(), q.GetScope(), q.GetType(), q.GetQuery()) -} - -func (ck CacheKey) String() string { - fields := []string{ - ("SourceName=" + ck.SST.SourceName), - ("Scope=" + ck.SST.Scope), - ("Type=" + ck.SST.Type), - } - - if ck.UniqueAttributeValue != nil { - fields = append(fields, ("UniqueAttributeValue=" + *ck.UniqueAttributeValue)) - } - - if ck.Method != nil { - fields = append(fields, ("Method=" + ck.Method.String())) - } - - if ck.Query != nil { - fields = append(fields, ("Query=" + *ck.Query)) - } - - return strings.Join(fields, ", ") -} - -// ToIndexValues Converts a cache query to a set of index values -func (ck CacheKey) ToIndexValues() IndexValues { - iv := IndexValues{ - SSTHash: ck.SST.Hash(), - } - - if ck.Method != nil { - iv.Method = *ck.Method - } - - if ck.Query != nil { - iv.Query = *ck.Query - } - - if ck.UniqueAttributeValue != nil { - iv.UniqueAttributeValue = *ck.UniqueAttributeValue - } - - return iv -} - -// Matches Returns whether or not the supplied index values match the -// CacheQuery, excluding the SST since this will have already been validated. -// Note that this only checks values that ave actually been set in the -// CacheQuery -func (ck CacheKey) Matches(i IndexValues) bool { - // Check for any mismatches on the values that are set - if ck.Method != nil { - if *ck.Method != i.Method { - return false - } - } - - if ck.Query != nil { - if *ck.Query != i.Query { - return false - } - } - - if ck.UniqueAttributeValue != nil { - if *ck.UniqueAttributeValue != i.UniqueAttributeValue { - return false - } - } - - return true -} - -var ErrCacheNotFound = errors.New("not found in cache") - -// SST A combination of SourceName, Scope and Type, all of which must be -// provided -type SST struct { - SourceName string - Scope string - Type string -} - -// Hash Creates a new SST hash from a given SST -func (s SST) Hash() SSTHash { - h := sha256.New() - h.Write([]byte(s.SourceName)) - h.Write([]byte(s.Scope)) - h.Write([]byte(s.Type)) - - sum := make([]byte, 0) - sum = h.Sum(sum) - - return SSTHash(fmt.Sprintf("%x", sum)) -} - -// CachedResult An item including cache metadata -type CachedResult struct { - // Item is the actual cached item - Item *sdp.Item - - // Error is the error that we want - Error error - - // The time at which this item expires - Expiry time.Time - - // Values that we use for calculating indexes - IndexValues IndexValues -} - -// SSTHash Represents the hash of `SourceName`, `Scope` and `Type` -type SSTHash string - -// Cache provides operations for caching SDP items and errors -type Cache interface { - // Lookup performs a cache lookup for the given query parameters. - // Returns: (cache hit, cache key, items, query error, done function) - // - // If hit=false, you MUST call the returned done function when finished, after storing all - // items/errors. The done function releases resources and unblocks waiting goroutines. - // You should defer the done function immediately after calling Lookup to ensure it's called. - // - // Example usage for cache miss: - // hit, ck, _, _, done := cache.Lookup(...) - // defer done() // MUST be called when finished - // if !hit { - // // Store all items first - // for _, item := range items { - // cache.StoreItem(ctx, item, duration, ck) - // } - // // done() is called via defer, releasing waiters - // } - // - // The done function is safe to call multiple times (idempotent). - // For cache hits or waiting goroutines, the done function is a no-op. - Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) - - // StoreItem stores an item in the cache with the specified duration. - StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) - - // StoreError stores an error in the cache with the specified duration. - StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) - - // Delete removes all entries matching the given cache key. - Delete(ck CacheKey) - - // Clear removes all entries from the cache. - Clear() - - // Purge removes all expired items from the cache. - // Returns statistics about the purge operation. - Purge(ctx context.Context, before time.Time) PurgeStats - - // GetMinWaitTime returns the minimum time between purge operations - GetMinWaitTime() time.Duration - - // StartPurger starts a background goroutine that automatically purges expired items. - // The purger will stop when the context is cancelled. - StartPurger(ctx context.Context) -} - -// NoOpCache is a cache implementation that does nothing. -// It can be used in tests or when caching is not desired, avoiding nil checks. -type NoOpCache struct{} - -// NewNoOpCache creates a new no-op cache that implements the Cache interface -// but performs no operations. Useful for testing or when caching is disabled. -func NewNoOpCache() Cache { - return &NoOpCache{} -} - -// Lookup always returns a cache miss -func (n *NoOpCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { - ck := CacheKeyFromParts(srcName, method, scope, typ, query) - return false, ck, nil, nil, noopDone -} - -// StoreItem does nothing -func (n *NoOpCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { - // No-op -} - -// StoreError does nothing -func (n *NoOpCache) StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) { - // No-op -} - -// Delete does nothing -func (n *NoOpCache) Delete(ck CacheKey) { - // No-op -} - -// Clear does nothing -func (n *NoOpCache) Clear() { - // No-op -} - -// Purge returns empty stats -func (n *NoOpCache) Purge(ctx context.Context, before time.Time) PurgeStats { - return PurgeStats{} -} - -// GetMinWaitTime returns 0 -func (n *NoOpCache) GetMinWaitTime() time.Duration { - return 0 -} - -// StartPurger does nothing -func (n *NoOpCache) StartPurger(ctx context.Context) { -} - -type MemoryCache struct { - // Minimum amount of time to wait between cache purges - MinWaitTime time.Duration - - // The timer that is used to trigger the next purge - purgeTimer *time.Timer - - // The time that the purger will run next - nextPurge time.Time - - indexes map[SSTHash]*indexSet - - // This index is used to track item expiries, since items can have different - // expiry durations we need to use a btree here rather than just appending - // to a slice or something. The purge process uses this to determine what - // needs deleting, then calls into each specific index to delete as required - expiryIndex *btree.BTreeG[*CachedResult] - - // Mutex for reading caches - indexMutex sync.RWMutex - - // Ensures that purge stats like `purgeTimer` and `nextPurge` aren't being - // modified concurrently - purgeMutex sync.Mutex - - // Tracks in-flight lookups to prevent duplicate work when multiple - // goroutines request the same cache key simultaneously - pending *pendingWork -} - -// NewMemoryCache creates a new in-memory cache implementation -func NewMemoryCache() *MemoryCache { - return &MemoryCache{ - indexes: make(map[SSTHash]*indexSet), - expiryIndex: newExpiryIndex(), - pending: newPendingWork(), - } -} - -// NewCache creates a new cache. This function returns a Cache interface. -// Currently, it returns a file-based implementation. The passed context will be -// used to start the purger. -func NewCache(ctx context.Context) Cache { - tmpFile, err := os.CreateTemp("", "sdpcache-*.db") - // close the file so bbolt can open it, but keep the file on disk. We don't - // need to check for errors since we're not using the file - _ = tmpFile.Close() - - if err != nil { - sentry.CaptureException(err) - log.WithError(err).Error("Failed to create temporary file for BoltCache, using memory cache instead") - cache := NewMemoryCache() - cache.StartPurger(ctx) - return cache - } - cache, err := NewBoltCache( - tmpFile.Name(), - WithMinWaitTime(30*time.Second), - // allocate 1GB of disk space for the cache (with 1GB additional for compaction temp file) - WithCompactThreshold(1*1024*1024*1024), - ) - if err != nil { - sentry.CaptureException(err) - log.WithError(err).Error("Failed to create BoltCache, using memory cache instead") - _ = os.Remove(tmpFile.Name()) - cache := NewMemoryCache() - cache.StartPurger(ctx) - return cache - } - cache.StartPurger(ctx) - return cache -} - -func newExpiryIndex() *btree.BTreeG[*CachedResult] { - return btree.NewG(2, func(a, b *CachedResult) bool { - return a.Expiry.Before(b.Expiry) - }) -} - -type indexSet struct { - uniqueAttributeValueIndex *btree.BTreeG[*CachedResult] - methodIndex *btree.BTreeG[*CachedResult] - queryIndex *btree.BTreeG[*CachedResult] -} - -func newIndexSet() *indexSet { - return &indexSet{ - uniqueAttributeValueIndex: btree.NewG(2, func(a, b *CachedResult) bool { - return sortString(a.IndexValues.UniqueAttributeValue, a.Item) < sortString(b.IndexValues.UniqueAttributeValue, b.Item) - }), - methodIndex: btree.NewG(2, func(a, b *CachedResult) bool { - return sortString(a.IndexValues.Method.String(), a.Item) < sortString(b.IndexValues.Method.String(), b.Item) - }), - queryIndex: btree.NewG(2, func(a, b *CachedResult) bool { - return sortString(a.IndexValues.Query, a.Item) < sortString(b.IndexValues.Query, b.Item) - }), - } -} - -// createDoneFunc returns a done function that calls pending.Complete for the given cache key. -// The done function releases resources and unblocks waiting goroutines. -// The done function is safe to call multiple times (idempotent via sync.Once). -func (c *MemoryCache) createDoneFunc(ck CacheKey) func() { - if c == nil || c.pending == nil { - return noopDone - } - key := ck.String() - var once sync.Once - return func() { - once.Do(func() { - c.pending.Complete(key) - }) - } -} - -// Lookup returns true/false whether or not the cache has a result for the given -// query. If there are results, they will be returned as slice of `sdp.Item`s or -// an `*sdp.QueryError`. -// The CacheKey is always returned, even if the lookup otherwise fails or errors -func (c *MemoryCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { - span := trace.SpanFromContext(ctx) - ck := CacheKeyFromParts(srcName, method, scope, typ, query) - - if c == nil { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache not initialised"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "cache has not been initialised", - Scope: scope, - SourceName: srcName, - ItemType: typ, - }, noopDone - } - - if ignoreCache { - span.SetAttributes( - attribute.String("ovm.cache.result", "ignore cache"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - items, err := c.search(ctx, ck) - if err != nil { - var qErr *sdp.QueryError - if errors.Is(err, ErrCacheNotFound) { - // Cache miss - check if another goroutine is already fetching this data - shouldWork, entry := c.pending.StartWork(ck.String()) - if shouldWork { - // We're the first caller, return miss so caller does the work - span.SetAttributes( - attribute.String("ovm.cache.result", "cache miss"), - attribute.Bool("ovm.cache.hit", false), - attribute.Bool("ovm.cache.workPending", false), - ) - return false, ck, nil, nil, c.createDoneFunc(ck) - } - - // Another goroutine is fetching this data, wait for it to complete - ok := c.pending.Wait(ctx, entry) - if !ok { - // Context was cancelled or work was cancelled, return miss - span.SetAttributes( - attribute.String("ovm.cache.result", "pending work cancelled or timeout"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - // Work is complete, re-check the cache for results - items, recheckErr := c.search(ctx, ck) - if recheckErr != nil { - if errors.Is(recheckErr, ErrCacheNotFound) { - // Cache still empty after pending work completed - // This is valid - worker may have found nothing or cancelled - span.SetAttributes( - attribute.String("ovm.cache.result", "pending work completed but cache still empty"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - var recheckQErr *sdp.QueryError - if errors.As(recheckErr, &recheckQErr) { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit from pending work: error"), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, nil, recheckQErr, noopDone - } - // Truly unexpected error - return miss - span.SetAttributes( - attribute.String("ovm.cache.result", "unexpected error on re-check"), - attribute.Bool("ovm.cache.hit", false), - ) - return false, ck, nil, nil, noopDone - } - - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit from pending work"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, items, nil, noopDone - } else if errors.As(err, &qErr) { - if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { - span.SetAttributes(attribute.String("ovm.cache.result", "cache hit: item not found")) - } else { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: QueryError"), - attribute.String("ovm.cache.error", err.Error()), - ) - } - - span.SetAttributes(attribute.Bool("ovm.cache.hit", true)) - return true, ck, nil, qErr, noopDone - } else { - // If it's an unknown error, convert it to SDP and skip this source - qErr = &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: err.Error(), - Scope: scope, - SourceName: srcName, - ItemType: typ, - } - - span.SetAttributes( - attribute.String("ovm.cache.error", err.Error()), - attribute.String("ovm.cache.result", "cache hit: unknown QueryError"), - attribute.Bool("ovm.cache.hit", true), - ) - - return true, ck, nil, qErr, noopDone - } - } - - if method == sdp.QueryMethod_GET { - // If the method was Get we should validate that we have - // only pulled one thing from the cache - - if len(items) < 2 { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: 1 item"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - return true, ck, items, nil, noopDone - } else { - span.SetAttributes( - attribute.String("ovm.cache.result", "cache returned >1 value, purging and continuing"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", false), - ) - c.Delete(ck) - return false, ck, nil, nil, noopDone - } - } - - span.SetAttributes( - attribute.String("ovm.cache.result", "cache hit: multiple items"), - attribute.Int("ovm.cache.numItems", len(items)), - attribute.Bool("ovm.cache.hit", true), - ) - - return true, ck, items, nil, noopDone -} - -// Search Runs a given query against the cache. If a cached error is found it -// will be returned immediately, if nothing is found a ErrCacheNotFound will -// be returned. Otherwise this will return items that match ALL of the given -// query parameters. Context is accepted for tracing but not currently used -// by MemoryCache (no I/O operations). -func (c *MemoryCache) search(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { - if c == nil { - return nil, nil - } - - items := make([]*sdp.Item, 0) - - results := c.getResults(ck) - - if len(results) == 0 { - return nil, ErrCacheNotFound - } - - now := time.Now() - - // If there is an error we want to return that, so we need to range over the - // results and separate items and errors. This is computationally less - // efficient than extracting errors inside of `getResults()` but logically - // it's a lot less complicated since `Delete()` uses the same method but - // applies different logic - for _, res := range results { - // Check if the cached result has expired - if res.Expiry.Before(now) { - // Skip expired results - continue - } - - if res.Error != nil { - return nil, res.Error - } - - // Return a copy of the item so the user can do whatever they want with - // it - itemCopy := proto.Clone(res.Item).(*sdp.Item) - - items = append(items, itemCopy) - } - - // If all results were expired, return cache not found - if len(items) == 0 { - return nil, ErrCacheNotFound - } - - return items, nil -} - -// Delete Deletes anything that matches the given cache query -func (c *MemoryCache) Delete(ck CacheKey) { - if c == nil { - return - } - - c.deleteResults(c.getResults(ck)) -} - -// getResults Searches indexes for cached results, doing no other logic. If -// nothing is found an empty slice will be returned. -func (c *MemoryCache) getResults(ck CacheKey) []*CachedResult { - c.indexMutex.RLock() - defer c.indexMutex.RUnlock() - - results := make([]*CachedResult, 0) - - // Get the relevant set of indexes based on the SST Hash - sstHash := ck.SST.Hash() - indexes, exists := c.indexes[sstHash] - pivot := CachedResult{ - IndexValues: IndexValues{ - SSTHash: sstHash, - }, - } - - if !exists { - // If we don't have a set of indexes then it definitely doesn't exist - return results - } - - // Start with the most specific index and fall back to the least specific. - // Checking all matching items and returning. These is no need to check all - // indexes since they all have the same content - if ck.UniqueAttributeValue != nil { - pivot.IndexValues.UniqueAttributeValue = *ck.UniqueAttributeValue - - indexes.uniqueAttributeValueIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { - if *ck.UniqueAttributeValue == result.IndexValues.UniqueAttributeValue { - if ck.Matches(result.IndexValues) { - results = append(results, result) - } - - // Always return true so that we continue to iterate - return true - } - - return false - }) - - return results - } - - if ck.Query != nil { - pivot.IndexValues.Query = *ck.Query - - indexes.queryIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { - if *ck.Query == result.IndexValues.Query { - if ck.Matches(result.IndexValues) { - results = append(results, result) - } - - // Always return true so that we continue to iterate - return true - } - - return false - }) - - return results - } - - if ck.Method != nil { - pivot.IndexValues.Method = *ck.Method - - indexes.methodIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { - if *ck.Method == result.IndexValues.Method { - // If the methods match, check the rest - if ck.Matches(result.IndexValues) { - results = append(results, result) - } - - // Always return true so that we continue to iterate - return true - } - - return false - }) - - return results - } - - // If nothing other than SST has been set then return everything - indexes.methodIndex.Ascend(func(result *CachedResult) bool { - results = append(results, result) - - return true - }) - - return results -} - -// StoreItem Stores an item in the cache. Note that this item must be fully -// populated (including metadata) for indexing to work correctly -func (c *MemoryCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { - if item == nil || c == nil { - return - } - - itemCopy := proto.Clone(item).(*sdp.Item) - - res := CachedResult{ - Item: itemCopy, - Error: nil, - Expiry: time.Now().Add(duration), - IndexValues: IndexValues{ - UniqueAttributeValue: itemCopy.UniqueAttributeValue(), - }, - } - - if ck.Method != nil { - res.IndexValues.Method = *ck.Method - } - if ck.Query != nil { - res.IndexValues.Query = *ck.Query - } - - res.IndexValues.SSTHash = ck.SST.Hash() - - c.storeResult(ctx, res) -} - -// StoreError Stores an error for the given duration. -func (c *MemoryCache) StoreError(ctx context.Context, err error, duration time.Duration, cacheQuery CacheKey) { - if c == nil || err == nil { - return - } - - res := CachedResult{ - Item: nil, - Error: err, - Expiry: time.Now().Add(duration), - IndexValues: cacheQuery.ToIndexValues(), - } - - c.storeResult(ctx, res) -} - -// Clear Delete all data in cache -func (c *MemoryCache) Clear() { - if c == nil { - return - } - - c.indexMutex.Lock() - defer c.indexMutex.Unlock() - - c.indexes = make(map[SSTHash]*indexSet) - c.expiryIndex = newExpiryIndex() -} - -func (c *MemoryCache) storeResult(ctx context.Context, res CachedResult) { - c.indexMutex.Lock() - defer c.indexMutex.Unlock() - - // Create the index if it doesn't exist - indexes, ok := c.indexes[res.IndexValues.SSTHash] - - if !ok { - indexes = newIndexSet() - c.indexes[res.IndexValues.SSTHash] = indexes - } - - // Add the item to the indexes and check if we're overwriting an unexpired entry - // We only need to check one index since they all reference the same CachedResult - oldResult, replaced := indexes.methodIndex.ReplaceOrInsert(&res) - indexes.queryIndex.ReplaceOrInsert(&res) - indexes.uniqueAttributeValueIndex.ReplaceOrInsert(&res) - - // Get the current span to add attributes - span := trace.SpanFromContext(ctx) - - // Check if we overwrote an entry that hasn't expired yet - // This indicates potential thundering-herd issues where multiple identical - // queries are executed concurrently instead of waiting for the first result - overwritten := false - if replaced && oldResult != nil { - now := time.Now() - if oldResult.Expiry.After(now) { - overwritten = true - timeUntilExpiry := oldResult.Expiry.Sub(now) - - // Build attributes for the overwrite event - attrs := []attribute.KeyValue{ - attribute.Bool("ovm.cache.unexpired_overwrite", true), - attribute.String("ovm.cache.time_until_expiry", timeUntilExpiry.String()), - attribute.String("ovm.cache.sst_hash", string(res.IndexValues.SSTHash)), - attribute.String("ovm.cache.query_method", res.IndexValues.Method.String()), - } - - if res.Item != nil { - attrs = append(attrs, - attribute.String("ovm.cache.item_type", res.Item.GetType()), - attribute.String("ovm.cache.item_scope", res.Item.GetScope()), - ) - } - - if res.IndexValues.Query != "" { - attrs = append(attrs, attribute.String("ovm.cache.query", res.IndexValues.Query)) - } - - if res.IndexValues.UniqueAttributeValue != "" { - attrs = append(attrs, attribute.String("ovm.cache.unique_attribute", res.IndexValues.UniqueAttributeValue)) - } - - span.SetAttributes(attrs...) - } - } - - // Always set the overwrite attribute, even if false, for consistent tracking - if !overwritten { - span.SetAttributes(attribute.Bool("ovm.cache.unexpired_overwrite", false)) - } - - // Add the item to the expiry index - c.expiryIndex.ReplaceOrInsert(&res) - - // Update the purge time if required - c.setNextPurgeIfEarlier(res.Expiry) -} - -// sortString Returns the string that the cached result should be sorted on. -// This has a prefix of the index value and suffix of the GloballyUniqueName if -// relevant -func sortString(indexValue string, item *sdp.Item) string { - if item == nil { - return indexValue - } else { - return indexValue + item.GloballyUniqueName() - } -} - -// PurgeStats Stats about the Purge -type PurgeStats struct { - // How many items were timed out of the cache - NumPurged int - // How long purging took overall - TimeTaken time.Duration - // The expiry time of the next item to expire. If there are no more items in - // the cache, this will be nil - NextExpiry *time.Time -} - -// deleteResults Deletes many cached results at once -func (c *MemoryCache) deleteResults(results []*CachedResult) { - c.indexMutex.Lock() - defer c.indexMutex.Unlock() - - for _, res := range results { - if indexSet, ok := c.indexes[res.IndexValues.SSTHash]; ok { - // For each expired item, delete it from all of the indexes that it will be in - if indexSet.methodIndex != nil { - indexSet.methodIndex.Delete(res) - } - if indexSet.queryIndex != nil { - indexSet.queryIndex.Delete(res) - } - if indexSet.uniqueAttributeValueIndex != nil { - indexSet.uniqueAttributeValueIndex.Delete(res) - } - } - - c.expiryIndex.Delete(res) - } -} - -// Purge Purges all expired items from the cache. The user must pass in the -// `before` time. All items that expired before this will be purged. Usually -// this would be just `time.Now()` however it could be overridden for testing -func (c *MemoryCache) Purge(ctx context.Context, before time.Time) PurgeStats { - if c == nil { - return PurgeStats{} - } - - // Store the current time rather than calling it a million times - start := time.Now() - - var nextExpiry *time.Time - - expired := make([]*CachedResult, 0) - - // Look through the expiry cache and work out what has expired - c.indexMutex.RLock() - c.expiryIndex.Ascend(func(res *CachedResult) bool { - if res.Expiry.Before(before) { - expired = append(expired, res) - - return true - } - - // Take note of the next expiry so we can schedule the next run - nextExpiry = &res.Expiry - - // As soon as hit this we'll stop ascending - return false - }) - c.indexMutex.RUnlock() - - c.deleteResults(expired) - - return PurgeStats{ - NumPurged: len(expired), - TimeTaken: time.Since(start), - NextExpiry: nextExpiry, - } -} - -// MinWaitDefault The default minimum wait time -const MinWaitDefault = (5 * time.Second) - -// GetMinWaitTime Returns the minimum wait time or the default if not set -func (c *MemoryCache) GetMinWaitTime() time.Duration { - if c == nil { - return 0 - } - - if c.MinWaitTime == 0 { - return MinWaitDefault - } - - return c.MinWaitTime -} - -// StartPurger Starts the purge process in the background, it will be cancelled -// when the context is cancelled. The cache will be purged initially, at which -// point the process will sleep until the next time an item expires -func (c *MemoryCache) StartPurger(ctx context.Context) { - if c == nil { - return - } - - c.purgeMutex.Lock() - if c.purgeTimer == nil { - c.purgeTimer = time.NewTimer(0) - c.purgeMutex.Unlock() - } else { - c.purgeMutex.Unlock() - log.WithContext(ctx).Info("Purger already running") - return // the purger is already running, so we don't need to start it again - } - - go func(ctx context.Context) { - for { - select { - case <-c.purgeTimer.C: - stats := c.Purge(ctx, time.Now()) - - c.setNextPurgeFromStats(stats) - case <-ctx.Done(): - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - c.purgeTimer.Stop() - c.purgeTimer = nil - return - } - } - }(ctx) -} - -// setNextPurgeFromStats Sets when the next purge should run based on the stats of the -// previous purge -func (c *MemoryCache) setNextPurgeFromStats(stats PurgeStats) { - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - if stats.NextExpiry == nil { - // If there is nothing else in the cache, wait basically - // forever - c.purgeTimer.Reset(1000 * time.Hour) - c.nextPurge = time.Now().Add(1000 * time.Hour) - } else { - if time.Until(*stats.NextExpiry) < c.GetMinWaitTime() { - c.purgeTimer.Reset(c.GetMinWaitTime()) - c.nextPurge = time.Now().Add(c.GetMinWaitTime()) - } else { - c.purgeTimer.Reset(time.Until(*stats.NextExpiry)) - c.nextPurge = *stats.NextExpiry - } - } -} - -// setNextPurgeIfEarlier Sets the next time the purger will run, if the provided -// time is sooner than the current scheduled purge time. While the purger is -// active this will be constantly updated, however if the purger is sleeping and -// new items are added this method ensures that the purger is woken up -func (c *MemoryCache) setNextPurgeIfEarlier(t time.Time) { - c.purgeMutex.Lock() - defer c.purgeMutex.Unlock() - - if t.Before(c.nextPurge) { - if c.purgeTimer == nil { - return - } - - c.purgeTimer.Stop() - c.nextPurge = t - c.purgeTimer.Reset(time.Until(t)) - } -} diff --git a/sdpcache/cache_benchmark_test.go b/sdpcache/cache_benchmark_test.go deleted file mode 100644 index e5aa2e19..00000000 --- a/sdpcache/cache_benchmark_test.go +++ /dev/null @@ -1,774 +0,0 @@ -package sdpcache - -import ( - "context" - "fmt" - "math/rand" - "runtime" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" -) - -const CacheDuration = 10 * time.Second - -// NewPopulatedCache Returns a newly populated cache and the CacheQuery that -// matches a randomly selected item in that cache -func NewPopulatedCache(ctx context.Context, numberItems int) (Cache, CacheKey) { - // Populate the cache - c := NewCache(ctx) - - var item *sdp.Item - var exampleCk CacheKey - exampleIndex := rand.Intn(numberItems) - - for i := range numberItems { - item = GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - if i == exampleIndex { - exampleCk = ck - } - - c.StoreItem(ctx, item, CacheDuration, ck) - } - - return c, exampleCk -} - -// NewPopulatedCacheWithListItems populates a cache with items that share the same -// SST (Source, Scope, Type) for LIST query benchmarking. All items will be returned -// when searching with a LIST query for the given SST. -func NewPopulatedCacheWithListItems(cache Cache, numberItems int, sst SST) CacheKey { - listMethod := sdp.QueryMethod_LIST - ck := CacheKey{SST: sst, Method: &listMethod} - - for i := range numberItems { - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - - // Ensure each item has a unique attribute value to prevent overwrites - // Format: "item-{index}" to guarantee uniqueness - uniqueValue := fmt.Sprintf("item-%d", i) - item.GetAttributes().Set("name", uniqueValue) - - cache.StoreItem(context.Background(), item, CacheDuration, ck) - } - - return ck -} - -// NewPopulatedCacheWithMultipleBuckets creates a cache with multiple SST buckets -// to enable realistic concurrent access patterns where different goroutines hit -// different buckets. -func NewPopulatedCacheWithMultipleBuckets(cache Cache, itemsPerBucket, numBuckets int) []CacheKey { - keys := make([]CacheKey, numBuckets) - listMethod := sdp.QueryMethod_LIST - - for bucketIdx := range numBuckets { - sst := SST{ - SourceName: "test-source", - Scope: fmt.Sprintf("scope-%d", bucketIdx), - Type: "test-type", - } - - keys[bucketIdx] = CacheKey{SST: sst, Method: &listMethod} - - for i := range itemsPerBucket { - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - uniqueValue := fmt.Sprintf("bucket-%d-item-%d", bucketIdx, i) - item.GetAttributes().Set("name", uniqueValue) - - cache.StoreItem(context.Background(), item, CacheDuration, keys[bucketIdx]) - } - } - - return keys -} - -func BenchmarkCache1SingleItem(b *testing.B) { - c, query := NewPopulatedCache(b.Context(), 1) - - var err error - - b.ResetTimer() - - for range b.N { - // Search for a single item - _, err = testSearch(context.Background(), c, query) - - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkCache10SingleItem(b *testing.B) { - c, query := NewPopulatedCache(b.Context(), 10) - - var err error - - b.ResetTimer() - - for range b.N { - // Search for a single item - _, err = testSearch(context.Background(), c, query) - - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkCache100SingleItem(b *testing.B) { - c, query := NewPopulatedCache(b.Context(), 100) - - var err error - - b.ResetTimer() - - for range b.N { - // Search for a single item - _, err = testSearch(context.Background(), c, query) - - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkCache1000SingleItem(b *testing.B) { - c, query := NewPopulatedCache(b.Context(), 1000) - - var err error - - b.ResetTimer() - - for range b.N { - // Search for a single item - _, err = testSearch(context.Background(), c, query) - - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkCache10_000SingleItem(b *testing.B) { - c, query := NewPopulatedCache(b.Context(), 10_000) - - var err error - - b.ResetTimer() - - for range b.N { - // Search for a single item - _, err = testSearch(context.Background(), c, query) - - if err != nil { - b.Fatal(err) - } - } -} - -// BenchmarkListQueryLookup benchmarks LIST query performance using the Lookup method, -// which includes the full production path with pending work deduplication logic. -// This provides a more realistic benchmark of end-to-end LIST query performance. -func BenchmarkListQueryLookup(b *testing.B) { - implementations := cacheImplementations(b) - cacheSizes := []int{10, 100, 1_000, 10_000} - - for _, impl := range implementations { - b.Run(impl.name, func(b *testing.B) { - for _, size := range cacheSizes { - b.Run(fmt.Sprintf("%d_items", size), func(b *testing.B) { - // Setup - cache := impl.factory() - sst := SST{ - SourceName: "test-source", - Scope: "test-scope", - Type: "test-type", - } - _ = NewPopulatedCacheWithListItems(cache, size, sst) - - b.ResetTimer() - b.ReportAllocs() - - // Benchmark - for range b.N { - hit, _, items, qErr, done := cache.Lookup( - b.Context(), - sst.SourceName, - sdp.QueryMethod_LIST, - sst.Scope, - sst.Type, - "", - false, // ignoreCache - ) - done() // Clean up immediately - if qErr != nil { - b.Fatalf("unexpected query error: %v", qErr) - } - if !hit { - b.Fatal("expected cache hit, got miss") - } - if len(items) != size { - b.Fatalf("expected %d items, got %d", size, len(items)) - } - } - }) - } - }) - } -} - -// BenchmarkListQueryConcurrent benchmarks LIST query performance under high concurrency. -// This simulates production scenarios where hundreds of goroutines hit the cache simultaneously. -func BenchmarkListQueryConcurrent(b *testing.B) { - implementations := cacheImplementations(b) - - // Test configuration similar to production - cacheSize := 5_000 // Similar to production's largest bucket - concurrencyLevels := []int{10, 50, 100, 250, 500} - - for _, impl := range implementations { - b.Run(impl.name, func(b *testing.B) { - for _, concurrency := range concurrencyLevels { - b.Run(fmt.Sprintf("%d_concurrent", concurrency), func(b *testing.B) { - // Setup: Create cache with multiple buckets for realistic access patterns - cache := impl.factory() - numBuckets := 10 // Multiple buckets to spread queries - itemsPerBucket := cacheSize / numBuckets - cacheKeys := NewPopulatedCacheWithMultipleBuckets(cache, itemsPerBucket, numBuckets) - - b.ResetTimer() - b.ReportAllocs() - b.SetParallelism(concurrency / runtime.GOMAXPROCS(0)) // Scale to desired concurrency - - // Benchmark: Each goroutine randomly queries one of the buckets - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - // Randomly select a bucket to query - bucketIdx := rand.Intn(numBuckets) - ck := cacheKeys[bucketIdx] - - // Use Lookup() to match production behavior - hit, _, items, qErr, done := cache.Lookup( - b.Context(), - ck.SST.SourceName, - sdp.QueryMethod_LIST, - ck.SST.Scope, - ck.SST.Type, - "", - false, // ignoreCache - ) - done() // Clean up immediately - if qErr != nil { - b.Errorf("unexpected query error: %v", qErr) - return - } - if !hit { - b.Error("expected cache hit, got miss") - return - } - if len(items) != itemsPerBucket { - b.Errorf("expected %d items, got %d", itemsPerBucket, len(items)) - return - } - } - }) - }) - } - }) - } -} - -// BenchmarkListQueryConcurrentSameKey benchmarks worst-case contention where all -// goroutines query the same cache key simultaneously. This tests pending work -// deduplication and maximum lock contention. -func BenchmarkListQueryConcurrentSameKey(b *testing.B) { - implementations := cacheImplementations(b) - - cacheSize := 5_000 - concurrencyLevels := []int{10, 50, 100, 250, 500} - - for _, impl := range implementations { - b.Run(impl.name, func(b *testing.B) { - for _, concurrency := range concurrencyLevels { - b.Run(fmt.Sprintf("%d_concurrent", concurrency), func(b *testing.B) { - // Setup: Single SST bucket that all goroutines will hit - cache := impl.factory() - sst := SST{ - SourceName: "test-source", - Scope: "test-scope", - Type: "test-type", - } - _ = NewPopulatedCacheWithListItems(cache, cacheSize, sst) - - b.ResetTimer() - b.ReportAllocs() - b.SetParallelism(concurrency / runtime.GOMAXPROCS(0)) - - // Benchmark: All goroutines hit the same key - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - // Use Lookup() to match production behavior - hit, _, items, qErr, done := cache.Lookup( - b.Context(), - sst.SourceName, - sdp.QueryMethod_LIST, - sst.Scope, - sst.Type, - "", - false, // ignoreCache - ) - done() // Clean up immediately - if qErr != nil { - b.Errorf("unexpected query error: %v", qErr) - return - } - if !hit { - b.Error("expected cache hit, got miss") - return - } - if len(items) != cacheSize { - b.Errorf("expected %d items, got %d", cacheSize, len(items)) - return - } - } - }) - }) - } - }) - } -} - -// BenchmarkPendingWorkContention tests cache behavior when many concurrent goroutines -// all call Lookup() for the same cache key simultaneously. This simulates the production -// scenario where hundreds of goroutines wait in pending.Wait() for a single slow -// aggregatedList operation to complete. -func BenchmarkPendingWorkContention(b *testing.B) { - // Test parameters matching production scenarios - concurrencyLevels := []int{100, 200, 400, 500} - fetchDurations := []time.Duration{1 * time.Second, 5 * time.Second, 10 * time.Second} - resultSizes := []int{100, 1000, 5000} - - for _, impl := range cacheImplementations(b) { - b.Run(impl.name, func(b *testing.B) { - for _, concurrency := range concurrencyLevels { - b.Run(fmt.Sprintf("concurrency=%d", concurrency), func(b *testing.B) { - for _, fetchDuration := range fetchDurations { - b.Run(fmt.Sprintf("fetchDuration=%s", fetchDuration), func(b *testing.B) { - for _, resultSize := range resultSizes { - b.Run(fmt.Sprintf("resultSize=%d", resultSize), func(b *testing.B) { - // Run the actual benchmark - benchmarkPendingWorkContentionScenario( - b, - impl.factory, - concurrency, - fetchDuration, - resultSize, - ) - }) - } - }) - } - }) - } - }) - } -} - -// benchmarkPendingWorkContentionScenario runs a single pending work contention scenario -func benchmarkPendingWorkContentionScenario( - b *testing.B, - cacheFactory func() Cache, - concurrency int, - fetchDuration time.Duration, - resultSize int, -) { - b.ReportAllocs() - - // Create a fresh cache for this test - cache := cacheFactory() - defer func() { - if closer, ok := cache.(interface{ Close() error }); ok { - closer.Close() - } - }() - - // Define the shared cache key that all goroutines will use - sst := SST{ - SourceName: "test-source", - Scope: "test-scope-*", - Type: "test-type", - } - listMethod := sdp.QueryMethod_LIST - sharedCacheKey := CacheKey{SST: sst, Method: &listMethod} - - // Track timing metrics across all goroutines - var ( - firstStartTime time.Time - firstCompleteTime time.Time - lastCompleteTime time.Time - timingMutex sync.Mutex - ) - - // Atomic flag to detect the first goroutine (the one that does the work) - var firstGoroutine atomic.Bool - - // Use a start barrier to ensure all goroutines begin simultaneously - startBarrier := make(chan struct{}) - - b.ResetTimer() - - for range b.N { - // Clear cache between iterations - cache.Clear() - - // Reset state - firstGoroutine.Store(false) - firstStartTime = time.Time{} - firstCompleteTime = time.Time{} - lastCompleteTime = time.Time{} - - var wg sync.WaitGroup - wg.Add(concurrency) - - // Spawn all goroutines - for range concurrency { - go func() { - defer wg.Done() - - // Wait for start signal to ensure simultaneous execution - <-startBarrier - - startTime := time.Now() - - // Call Lookup - this is where the contention happens - hit, _, items, qErr, done := cache.Lookup( - b.Context(), - sst.SourceName, - sdp.QueryMethod_LIST, - sst.Scope, - sst.Type, - "", - false, // ignoreCache - ) - - endTime := time.Now() - - // Check if this goroutine was the first one (the worker) - isFirst := firstGoroutine.CompareAndSwap(false, true) - - if isFirst { - // This goroutine got the cache miss and needs to do the work - if hit { - b.Errorf("First goroutine should get cache miss, got hit") - done() - return - } - - // Record when work started - timingMutex.Lock() - firstStartTime = startTime - timingMutex.Unlock() - - // Simulate slow fetch operation (like aggregatedList) - time.Sleep(fetchDuration) - - // Store items in cache (simulating results from aggregatedList) - for itemIdx := range resultSize { - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - item.GetAttributes().Set("name", fmt.Sprintf("item-%d", itemIdx)) - - cache.StoreItem(b.Context(), item, CacheDuration, sharedCacheKey) - } - - // Record when work completed - timingMutex.Lock() - firstCompleteTime = time.Now() - timingMutex.Unlock() - - // Call done() to complete pending work and release waiting goroutines - done() - } else { - // This goroutine should have waited in pending.Wait() and then got a cache hit - // Note: It might get partial results if it wakes up while the first goroutine - // is still storing items (released when the first goroutine calls done()) - done() // No-op for waiters, but good practice - if !hit { - b.Errorf("Waiting goroutine should get cache hit after pending work completes, got miss") - return - } - if qErr != nil { - b.Errorf("Waiting goroutine got error: %v", qErr) - return - } - if len(items) == 0 { - b.Errorf("Waiting goroutine got cache hit but no items") - return - } - // Don't check exact count - waiters may get partial results - } - - // Track when each goroutine completes - timingMutex.Lock() - if lastCompleteTime.IsZero() || endTime.After(lastCompleteTime) { - lastCompleteTime = endTime - } - timingMutex.Unlock() - }() - } - - // Release all goroutines simultaneously - close(startBarrier) - - // Wait for all goroutines to complete - wg.Wait() - - // Calculate and report metrics for this iteration - if !firstStartTime.IsZero() && !firstCompleteTime.IsZero() && !lastCompleteTime.IsZero() { - workDuration := firstCompleteTime.Sub(firstStartTime) - totalDuration := lastCompleteTime.Sub(firstStartTime) - maxWaitTime := lastCompleteTime.Sub(firstCompleteTime) - - // Report metrics - b.ReportMetric(workDuration.Seconds(), "work_duration_sec") - b.ReportMetric(totalDuration.Seconds(), "total_duration_sec") - b.ReportMetric(maxWaitTime.Seconds(), "max_wait_sec") - b.ReportMetric(float64(concurrency-1), "waiting_goroutines") - - // Calculate efficiency: ideally, waiters should return immediately after work completes - // A ratio close to 1.0 means waiters waited approximately the work duration - waitToWorkRatio := totalDuration.Seconds() / workDuration.Seconds() - b.ReportMetric(waitToWorkRatio, "wait_to_work_ratio") - } - - // Recreate start barrier for next iteration - startBarrier = make(chan struct{}) - } - - b.StopTimer() -} - -// BenchmarkConcurrentMultiKeyWrites tests cache behavior when many concurrent goroutines -// call Lookup() with DIFFERENT cache keys, all get cache misses, and all write results -// concurrently to the same BoltDB file. This simulates the production scenario where -// a wildcard query is expanded into 620+ separate queries with different scopes. -func BenchmarkConcurrentMultiKeyWrites(b *testing.B) { - // Test parameters matching production scenarios - concurrencyLevels := []int{100, 200, 400, 600} - itemsPerGoroutine := []int{10, 100, 500} - fetchDurations := []time.Duration{100 * time.Millisecond, 1 * time.Second, 5 * time.Second} - - for _, impl := range cacheImplementations(b) { - b.Run(impl.name, func(b *testing.B) { - for _, concurrency := range concurrencyLevels { - b.Run(fmt.Sprintf("concurrency=%d", concurrency), func(b *testing.B) { - for _, itemsPerGoroutine := range itemsPerGoroutine { - b.Run(fmt.Sprintf("itemsPerGoroutine=%d", itemsPerGoroutine), func(b *testing.B) { - for _, fetchDuration := range fetchDurations { - b.Run(fmt.Sprintf("fetchDuration=%s", fetchDuration), func(b *testing.B) { - // Run the actual benchmark - benchmarkConcurrentMultiKeyWritesScenario( - b, - impl.factory, - concurrency, - itemsPerGoroutine, - fetchDuration, - ) - }) - } - }) - } - }) - } - }) - } -} - -// benchmarkConcurrentMultiKeyWritesScenario runs a single concurrent multi-key write scenario -func benchmarkConcurrentMultiKeyWritesScenario( - b *testing.B, - cacheFactory func() Cache, - concurrency int, - itemsPerGoroutine int, - fetchDuration time.Duration, -) { - b.ReportAllocs() - - // Create a fresh cache for this test - cache := cacheFactory() - defer func() { - if closer, ok := cache.(interface{ Close() error }); ok { - closer.Close() - } - }() - - // Generate unique cache keys for each goroutine (different scopes) - cacheKeys := make([]CacheKey, concurrency) - listMethod := sdp.QueryMethod_LIST - for i := range concurrency { - cacheKeys[i] = CacheKey{ - SST: SST{ - SourceName: "test-source", - Scope: fmt.Sprintf("scope-%d", i), // Different scope = different cache key - Type: "test-type", - }, - Method: &listMethod, - } - } - - // Track timing metrics - var ( - goroutineStartTimes []time.Time - goroutineEndTimes []time.Time - timesMutex sync.Mutex - totalStoreItemCalls atomic.Int64 - ) - - // Use a start barrier to ensure all goroutines begin simultaneously - startBarrier := make(chan struct{}) - - b.ResetTimer() - - for range b.N { - // Clear cache between iterations - cache.Clear() - - // Reset metrics - goroutineStartTimes = make([]time.Time, 0, concurrency) - goroutineEndTimes = make([]time.Time, 0, concurrency) - totalStoreItemCalls.Store(0) - - var wg sync.WaitGroup - wg.Add(concurrency) - - // Spawn all goroutines - for g := range concurrency { - goroutineIdx := g - go func() { - defer wg.Done() - - // Wait for start signal to ensure simultaneous execution - <-startBarrier - - startTime := time.Now() - - // Track start time - timesMutex.Lock() - goroutineStartTimes = append(goroutineStartTimes, startTime) - timesMutex.Unlock() - - // Call Lookup with unique cache key - should be a cache miss - myCacheKey := cacheKeys[goroutineIdx] - hit, _, _, qErr, done := cache.Lookup( - b.Context(), - myCacheKey.SST.SourceName, - sdp.QueryMethod_LIST, - myCacheKey.SST.Scope, - myCacheKey.SST.Type, - "", - false, // ignoreCache - ) - - if hit { - b.Errorf("Expected cache miss for goroutine %d, got hit", goroutineIdx) - done() - return - } - if qErr != nil { - b.Errorf("Unexpected error for goroutine %d: %v", goroutineIdx, qErr) - done() - return - } - - // Simulate slow fetch operation (like aggregatedList API call) - time.Sleep(fetchDuration) - - // Store multiple items (simulating API results) - for itemIdx := range itemsPerGoroutine { - item := GenerateRandomItem() - item.Scope = myCacheKey.SST.Scope - item.Type = myCacheKey.SST.Type - item.Metadata.SourceName = myCacheKey.SST.SourceName - item.GetAttributes().Set("name", fmt.Sprintf("goroutine-%d-item-%d", goroutineIdx, itemIdx)) - - cache.StoreItem(b.Context(), item, CacheDuration, myCacheKey) - totalStoreItemCalls.Add(1) - } - - // Call done() to complete pending work - done() - - endTime := time.Now() - - // Track end time - timesMutex.Lock() - goroutineEndTimes = append(goroutineEndTimes, endTime) - timesMutex.Unlock() - }() - } - - // Release all goroutines simultaneously - close(startBarrier) - - // Wait for all goroutines to complete - wg.Wait() - - // Calculate and report metrics for this iteration - if len(goroutineStartTimes) > 0 && len(goroutineEndTimes) > 0 { - // Find earliest start and latest end - earliestStart := goroutineStartTimes[0] - latestEnd := goroutineEndTimes[0] - - for _, t := range goroutineStartTimes { - if t.Before(earliestStart) { - earliestStart = t - } - } - for _, t := range goroutineEndTimes { - if t.After(latestEnd) { - latestEnd = t - } - } - - totalDuration := latestEnd.Sub(earliestStart) - totalWrites := totalStoreItemCalls.Load() - writeThroughput := float64(totalWrites) / totalDuration.Seconds() - - // Calculate average goroutine duration - var totalGoroutineDuration time.Duration - for idx := range goroutineStartTimes { - if idx < len(goroutineEndTimes) { - totalGoroutineDuration += goroutineEndTimes[idx].Sub(goroutineStartTimes[idx]) - } - } - avgGoroutineDuration := totalGoroutineDuration / time.Duration(len(goroutineStartTimes)) - - // Report metrics - b.ReportMetric(totalDuration.Seconds(), "total_duration_sec") - b.ReportMetric(avgGoroutineDuration.Seconds(), "avg_goroutine_sec") - b.ReportMetric(float64(concurrency), "concurrent_writers") - b.ReportMetric(float64(totalWrites), "total_store_calls") - b.ReportMetric(writeThroughput, "writes_per_sec") - } - - // Recreate start barrier for next iteration - startBarrier = make(chan struct{}) - } - - b.StopTimer() -} diff --git a/sdpcache/cache_stuck_test.go b/sdpcache/cache_stuck_test.go deleted file mode 100644 index c9c9713b..00000000 --- a/sdpcache/cache_stuck_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package sdpcache - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" -) - -// TestListErrorWithProperCleanup tests the correct behavior where: -// 1. A LIST operation is performed and gets a cache miss -// 2. The caller starts the work -// 3. The query encounters an error -// 4. The caller properly calls StoreError to cache the error -// 5. Subsequent requests get the cached error immediately (don't block) -// -// This test documents the fix for the cache timeout bug. -func TestListErrorWithProperCleanup(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // Track timing - var secondCallDuration time.Duration - - // First goroutine: Gets cache miss, simulates work that errors, - // and properly calls StoreError to cache the error - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that takes time and then errors - time.Sleep(50 * time.Millisecond) - - // CORRECT BEHAVIOR: Worker encounters an error and properly caches it - err := &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: "simulated list error", - } - cache.StoreError(ctx, err, 1*time.Hour, ck) - t.Log("First goroutine: properly called StoreError") - }() - - // Second goroutine: Should get cached error immediately - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - // Use a short timeout to detect blocking - timeoutCtx, done := context.WithTimeout(ctx, 500*time.Millisecond) - defer done() - - start := time.Now() - hit, _, _, qErr, done := cache.Lookup(timeoutCtx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - secondCallDuration = time.Since(start) - - if !hit { - t.Error("second goroutine: expected cache hit (cached error)") - } - if qErr == nil { - t.Error("second goroutine: expected cached error") - } - t.Logf("Second goroutine: got cached error after %v", secondCallDuration) - }() - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // Verify the second call got the result quickly (didn't block) - if secondCallDuration > 200*time.Millisecond { - t.Fatalf("Second call took too long (%v), possibly blocked waiting for pending work", secondCallDuration) - } - - t.Logf("✓ Second call returned quickly (%v) with cached error - proper cleanup is working", secondCallDuration) - }) - } -} - -// TestListErrorWithProperCancellation tests the CORRECT behavior where: -// 1. A LIST operation is performed and gets a cache miss -// 2. The query encounters an error -// 3. The caller properly calls the done function -// 4. Subsequent requests should get a cache miss immediately (not block) -func TestListErrorWithProperDone(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // Track timing - var secondCallDuration time.Duration - - // First goroutine: Gets cache miss, simulates work that errors, - // and PROPERLY calls the done function - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - - if hit { - t.Error("first goroutine: expected cache miss") - done() // Clean up even on error - return - } - - // Simulate work that takes time and then errors - time.Sleep(100 * time.Millisecond) - - // CORRECT BEHAVIOR: Call done to release resources - done() - t.Log("First goroutine: properly called done()") - }() - - // Second goroutine: Should receive cache miss quickly (not block) - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - start := time.Now() - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - secondCallDuration = time.Since(start) - - if hit { - t.Error("second goroutine: expected cache miss") - } - - t.Logf("Second goroutine: got cache miss after %v", secondCallDuration) - }() - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // The second call should NOT block for long - // It should get a cache miss shortly after the first call done() (~100ms) - if secondCallDuration > 300*time.Millisecond { - t.Errorf("Expected second call to return quickly after cancellation, but it took %v", secondCallDuration) - } - - t.Logf("Test demonstrates correct behavior: second call returned in %v", secondCallDuration) - }) - } -} - -// TestListErrorWithStoreError tests the CORRECT behavior where: -// 1. A LIST operation is performed and gets a cache miss -// 2. The query encounters an error -// 3. The caller properly calls StoreError -// 4. Subsequent requests should get the cached error immediately -func TestListErrorWithStoreError(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - expectedError := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "list returned error", - Scope: sst.Scope, - SourceName: sst.SourceName, - ItemType: sst.Type, - } - - // Track results - var secondCallHit bool - var secondCallError *sdp.QueryError - var secondCallDuration time.Duration - - // First goroutine: Gets cache miss, simulates work that errors, - // and PROPERLY calls StoreError - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that takes time and then errors - time.Sleep(100 * time.Millisecond) - - // CORRECT BEHAVIOR: Store the error so other callers can get it - cache.StoreError(ctx, expectedError, 10*time.Second, ck) - t.Log("First goroutine: properly called StoreError") - }() - - // Second goroutine: Should receive the cached error - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - start := time.Now() - var items []*sdp.Item - var done func() - secondCallHit, _, items, secondCallError, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - secondCallDuration = time.Since(start) - - if items != nil { - t.Error("second goroutine: expected nil items with error") - } - - t.Logf("Second goroutine: got result after %v", secondCallDuration) - }() - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // The second call should get the cached error - if !secondCallHit { - t.Error("Expected cache hit with error") - } - - if secondCallError == nil { - t.Error("Expected error to be returned") - } - - if secondCallError != nil && secondCallError.GetErrorType() != expectedError.GetErrorType() { - t.Errorf("Expected error type %v, got %v", expectedError.GetErrorType(), secondCallError.GetErrorType()) - } - - // Should return relatively quickly (~100ms for first goroutine work) - if secondCallDuration > 300*time.Millisecond { - t.Errorf("Expected second call to return quickly with cached error, but it took %v", secondCallDuration) - } - - t.Logf("Test demonstrates correct behavior: second call got cached error in %v", secondCallDuration) - }) - } -} - -// TestListReturnsEmptyButNoStore tests the scenario where: -// 1. A LIST operation completes successfully but finds no items -// 2. The caller calls Complete() but doesn't store anything -// 3. Subsequent requests should get cache miss (not error) -func TestListReturnsEmptyButNoStore(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - var secondCallHit bool - var secondCallDuration time.Duration - - // First goroutine: LIST returns 0 items, completes without storing - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that completes and finds no items - time.Sleep(100 * time.Millisecond) - - // Complete without storing anything (LIST found 0 items) - // This is handled by the underlying pending work mechanism - switch c := cache.(type) { - case *MemoryCache: - c.pending.Complete(ck.String()) - case *BoltCache: - c.pending.Complete(ck.String()) - } - - t.Log("First goroutine: completed work but stored nothing") - }() - - // Second goroutine: Should get cache miss - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - start := time.Now() - secondCallHit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - secondCallDuration = time.Since(start) - - t.Logf("Second goroutine: hit=%v, duration=%v", secondCallHit, secondCallDuration) - }() - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // Second call should get cache miss (not error) - if secondCallHit { - t.Error("Expected cache miss when first caller completed without storing") - } - - // Should return relatively quickly (~100ms for first goroutine work) - if secondCallDuration > 300*time.Millisecond { - t.Errorf("Expected second call to return quickly, but it took %v", secondCallDuration) - } - }) - } -} diff --git a/sdpcache/cache_test.go b/sdpcache/cache_test.go deleted file mode 100644 index d4e9c088..00000000 --- a/sdpcache/cache_test.go +++ /dev/null @@ -1,2156 +0,0 @@ -package sdpcache - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/overmindtech/cli/sdp-go" -) - -// testSearch is a helper function that calls the internal search method -// on either MemoryCache or BoltCache implementations for testing purposes -func testSearch(ctx context.Context, cache Cache, ck CacheKey) ([]*sdp.Item, error) { - switch c := cache.(type) { - case *MemoryCache: - return c.search(ctx, ck) - case *BoltCache: - return c.search(ctx, ck) - default: - return nil, fmt.Errorf("unsupported cache type for search: %T", cache) - } -} - -// cacheImplementations returns the list of cache implementations to test -// Accepts testing.TB so it can be used by both tests and benchmarks -func cacheImplementations(tb testing.TB) []struct { - name string - factory func() Cache -} { - return []struct { - name string - factory func() Cache - }{ - {"MemoryCache", func() Cache { return NewMemoryCache() }}, - {"BoltCache", func() Cache { - c, err := NewBoltCache(filepath.Join(tb.TempDir(), "cache.db")) - if err != nil { - tb.Fatalf("failed to create BoltCache: %v", err) - } - tb.Cleanup(func() { - _ = c.CloseAndDestroy() - }) - return c - }}, - } -} - -func TestStoreItem(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(t.Context(), item, 10*time.Second, ck) - - results, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - - if len(results) != 1 { - t.Errorf("expected 1 result, got %v", len(results)) - } - - // Test another match - item = GenerateRandomItem() - ck = CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(t.Context(), item, 10*time.Second, ck) - - results, err = testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - - if len(results) != 1 { - t.Errorf("expected 1 result, got %v", len(results)) - } - - // Test different scope - item = GenerateRandomItem() - ck = CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(t.Context(), item, 10*time.Second, ck) - - ck.SST.Scope = fmt.Sprintf("new scope %v", ck.SST.Scope) - - results, err = testSearch(t.Context(), cache, ck) - if err != nil { - if !errors.Is(err, ErrCacheNotFound) { - t.Error(err) - } else { - t.Log("expected cache miss") - } - } - - if len(results) != 0 { - t.Errorf("expected 0 result, got %v", results) - } - }) - } -} - -func TestStoreError(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - // Test with just an error - sst := SST{ - SourceName: "foo", - Scope: "foo", - Type: "foo", - } - - uav := "foo" - - cache.StoreError(t.Context(), errors.New("arse"), 10*time.Second, CacheKey{ - SST: sst, - Method: sdp.QueryMethod_GET.Enum(), - Query: &uav, - }) - - items, err := testSearch(t.Context(), cache, CacheKey{ - SST: sst, - Method: sdp.QueryMethod_GET.Enum(), - Query: &uav, - }) - - if len(items) > 0 { - t.Errorf("expected 0 items, got %v", len(items)) - } - - if err == nil { - t.Error("expected error, got nil") - } - - // Test with items and an error for the same query - // Add an item with the same details as above - item := GenerateRandomItem() - item.Metadata.SourceQuery.Method = sdp.QueryMethod_GET - item.Metadata.SourceQuery.Query = "foo" - item.Metadata.SourceName = "foo" - item.Scope = "foo" - item.Type = "foo" - - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - items, err = testSearch(t.Context(), cache, ck) - - if len(items) > 0 { - t.Errorf("expected 0 items, got %v", len(items)) - } - - if err == nil { - t.Error("expected error, got nil") - } - - // Test with multiple errors - cache.StoreError(t.Context(), errors.New("nope"), 10*time.Second, CacheKey{ - SST: sst, - Method: sdp.QueryMethod_GET.Enum(), - Query: &uav, - }) - - items, err = testSearch(t.Context(), cache, CacheKey{ - SST: sst, - Method: sdp.QueryMethod_GET.Enum(), - Query: &uav, - }) - - if len(items) > 0 { - t.Errorf("expected 0 items, got %v", len(items)) - } - - if err == nil { - t.Error("expected error, got nil") - } - }) - } -} - -func TestPurge(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - cachedItems := []struct { - Item *sdp.Item - Expiry time.Time - }{ - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(50 * time.Millisecond), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(1 * time.Second), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(2 * time.Second), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(3 * time.Second), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(4 * time.Second), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(5 * time.Second), - }, - } - - for _, i := range cachedItems { - ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) - cache.StoreItem(t.Context(), i.Item, time.Until(i.Expiry), ck) - } - - // Make sure all the items are in the cache - for _, i := range cachedItems { - ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) - items, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - - if len(items) != 1 { - t.Errorf("expected 1 item, got %v", len(items)) - } - } - - // Purge just the first one - stats := cache.Purge(t.Context(), cachedItems[0].Expiry.Add(500*time.Millisecond)) - - if stats.NumPurged != 1 { - t.Errorf("expected 1 item purged, got %v", stats.NumPurged) - } - - // The times won't be exactly equal because we're checking it against - // time.Now more than once. So I need to check that they are *almost* the - // same, but not exactly - nextExpiryString := stats.NextExpiry.Format(time.RFC3339) - expectedNextExpiryString := cachedItems[1].Expiry.Format(time.RFC3339) - - if nextExpiryString != expectedNextExpiryString { - t.Errorf("expected next expiry to be %v, got %v", expectedNextExpiryString, nextExpiryString) - } - - // Purge all but the last one - stats = cache.Purge(t.Context(), cachedItems[4].Expiry.Add(500*time.Millisecond)) - - if stats.NumPurged != 4 { - t.Errorf("expected 4 item purged, got %v", stats.NumPurged) - } - - // Purge the last one - stats = cache.Purge(t.Context(), cachedItems[5].Expiry.Add(500*time.Millisecond)) - - if stats.NumPurged != 1 { - t.Errorf("expected 1 item purged, got %v", stats.NumPurged) - } - - if stats.NextExpiry != nil { - t.Errorf("expected expiry to be nil, got %v", stats.NextExpiry) - } - }) - } -} - -func TestDelete(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - // Insert an item - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(t.Context(), item, time.Millisecond, ck) - sst := SST{ - SourceName: item.GetMetadata().GetSourceName(), - Scope: item.GetScope(), - Type: item.GetType(), - } - - // It should be there - items, err := testSearch(t.Context(), cache, CacheKey{ - SST: sst, - }) - if err != nil { - t.Error(err) - } - - if len(items) != 1 { - t.Errorf("expected 1 item, got %v", len(items)) - } - - // Delete it - cache.Delete(CacheKey{ - SST: sst, - }) - - // It should be gone - items, err = testSearch(t.Context(), cache, CacheKey{ - SST: sst, - }) - - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("expected ErrCacheNotFound, got %v", err) - } - - if len(items) != 0 { - t.Errorf("expected 0 item, got %v", len(items)) - } - }) - } -} - -func TestPointers(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(t.Context(), item, time.Minute, ck) - - item.Type = "bad" - - items, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - - if len(items) != 1 { - t.Errorf("expected 1 item, got %v", len(items)) - } - - if items[0].GetType() == "bad" { - t.Error("item was changed in cache") - } - }) - } -} - -func TestCacheClear(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - cache.Clear() - - // Populate the cache - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(ctx, item, 500*time.Millisecond, ck) - - // Start purging just to make sure it doesn't break - ctx, done := context.WithCancel(ctx) - defer done() - cache.StartPurger(ctx) - - // Make sure the cache is populated - _, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - - // Clear the cache - cache.Clear() - - // Make sure the cache is empty - _, err = testSearch(t.Context(), cache, ck) - - if err == nil { - t.Error("expected error, cache not cleared") - } - - // Make sure we can populate it again - cache.StoreItem(ctx, item, 500*time.Millisecond, ck) - _, err = testSearch(t.Context(), cache, ck) - if err != nil { - t.Error(err) - } - }) - } -} - -func TestLookup(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // ignore the cache - cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), true) - defer done() - if err != nil { - t.Fatal(err) - } - if cacheHit { - t.Error("expected cache miss, got hit") - } - if cachedItems != nil { - t.Errorf("expected nil items, got %v", cachedItems) - } - - // Lookup the item - cacheHit, _, cachedItems, err, done = cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) - defer done() - - if err != nil { - t.Fatal(err) - } - if !cacheHit { - t.Fatal("expected cache hit, got miss") - } - if len(cachedItems) != 1 { - t.Fatalf("expected 1 item, got %v", len(cachedItems)) - } - - if cachedItems[0].GetType() != item.GetType() { - t.Errorf("expected type %v, got %v", item.GetType(), cachedItems[0].GetType()) - } - - if cachedItems[0].Health == nil { - t.Error("expected health to be set") - } - - if len(cachedItems[0].GetTags()) != len(item.GetTags()) { - t.Error("expected tags to be set") - } - - stats := cache.Purge(ctx, time.Now().Add(1*time.Hour)) - if stats.NumPurged != 1 { - t.Errorf("expected 1 item purged, got %v", stats.NumPurged) - } - - // Lookup the item - cacheHit, _, cachedItems, err, done = cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) - defer done() - - if err != nil { - t.Fatal(err) - } - if cacheHit { - t.Fatal("expected cache miss, got hit") - } - if len(cachedItems) != 0 { - t.Fatalf("expected 0 item, got %v", len(cachedItems)) - } - }) - } -} - -func TestStoreSearch(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - item := GenerateRandomItem() - item.Metadata.SourceQuery.Method = sdp.QueryMethod_SEARCH - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Lookup the item as GET request - cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) - defer done() - if err != nil { - t.Fatal(err) - } - - if !cacheHit { - t.Fatal("expected cache hit, got miss") - } - - if len(cachedItems) != 1 { - t.Fatalf("expected 1 item, got %v", len(cachedItems)) - } - - if cachedItems[0].GetType() != item.GetType() { - t.Errorf("expected type %v, got %v", item.GetType(), cachedItems[0].GetType()) - } - }) - } -} - -func TestLookupWithListMethod(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - // Store multiple items with same SST - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - listMethod := sdp.QueryMethod_LIST - - item1 := GenerateRandomItem() - item1.Scope = sst.Scope - item1.Type = sst.Type - item1.Metadata.SourceName = sst.SourceName - ck1 := CacheKey{SST: sst, Method: &listMethod} - cache.StoreItem(ctx, item1, 10*time.Second, ck1) - - item2 := GenerateRandomItem() - item2.Scope = sst.Scope - item2.Type = sst.Type - item2.Metadata.SourceName = sst.SourceName - ck2 := CacheKey{SST: sst, Method: &listMethod} - cache.StoreItem(ctx, item2, 10*time.Second, ck2) - - // Lookup with LIST should return both items - cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, sst.SourceName, sdp.QueryMethod_LIST, sst.Scope, sst.Type, "", false) - defer done() - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !cacheHit { - t.Fatal("expected cache hit, got miss") - } - if len(cachedItems) != 2 { - t.Errorf("expected 2 items, got %v", len(cachedItems)) - } - }) - } -} - -func TestSearchWithListMethod(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - // Store items with LIST method - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - listMethod := sdp.QueryMethod_LIST - ck := CacheKey{SST: sst, Method: &listMethod} - - item1 := GenerateRandomItem() - item1.Scope = sst.Scope - item1.Type = sst.Type - cache.StoreItem(t.Context(), item1, 10*time.Second, ck) - - item2 := GenerateRandomItem() - item2.Scope = sst.Scope - item2.Type = sst.Type - cache.StoreItem(t.Context(), item2, 10*time.Second, ck) - - // Search should return both items - items, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(items) != 2 { - t.Errorf("expected 2 items, got %v", len(items)) - } - }) - } -} - -func TestSearchMethodWithDifferentQueries(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - searchMethod := sdp.QueryMethod_SEARCH - - // Store items with different search queries - query1 := "query1" - ck1 := CacheKey{SST: sst, Method: &searchMethod, Query: &query1} - item1 := GenerateRandomItem() - item1.Scope = sst.Scope - item1.Type = sst.Type - cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) - - query2 := "query2" - ck2 := CacheKey{SST: sst, Method: &searchMethod, Query: &query2} - item2 := GenerateRandomItem() - item2.Scope = sst.Scope - item2.Type = sst.Type - cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) - - // Search with query1 should only return item1 - items, err := testSearch(t.Context(), cache, ck1) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item for query1, got %v", len(items)) - } - - // Search with query2 should only return item2 - items, err = testSearch(t.Context(), cache, ck2) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item for query2, got %v", len(items)) - } - }) - } -} - -func TestSearchWithPartialCacheKey(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - - // Store items with different methods - getMethod := sdp.QueryMethod_GET - listMethod := sdp.QueryMethod_LIST - - item1 := GenerateRandomItem() - item1.Scope = sst.Scope - item1.Type = sst.Type - uav1 := "item1" - ck1 := CacheKey{SST: sst, Method: &getMethod, UniqueAttributeValue: &uav1} - cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) - - item2 := GenerateRandomItem() - item2.Scope = sst.Scope - item2.Type = sst.Type - ck2 := CacheKey{SST: sst, Method: &listMethod} - cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) - - // Search with SST only should return both items - ckPartial := CacheKey{SST: sst} - items, err := testSearch(t.Context(), cache, ckPartial) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(items) != 2 { - t.Errorf("expected 2 items with SST-only search, got %v", len(items)) - } - }) - } -} - -func TestDeleteWithPartialCacheKey(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - - // Store multiple items with same SST - item1 := GenerateRandomItem() - item1.Scope = sst.Scope - item1.Type = sst.Type - ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), sst.SourceName) - cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) - - item2 := GenerateRandomItem() - item2.Scope = sst.Scope - item2.Type = sst.Type - ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), sst.SourceName) - cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) - - // Delete with SST only should remove all items - cache.Delete(CacheKey{SST: sst}) - - // Verify all items are gone - items, err := testSearch(t.Context(), cache, CacheKey{SST: sst}) - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("expected ErrCacheNotFound after delete, got: %v", err) - } - if len(items) != 0 { - t.Errorf("expected 0 items after delete, got %v", len(items)) - } - }) - } -} - -func TestLookupWithCachedError(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - // Test different error types - errorTypes := []struct { - name string - errorType sdp.QueryError_ErrorType - }{ - {"NOTFOUND", sdp.QueryError_NOTFOUND}, - {"NOSCOPE", sdp.QueryError_NOSCOPE}, - {"TIMEOUT", sdp.QueryError_TIMEOUT}, - {"OTHER", sdp.QueryError_OTHER}, - } - - for i, et := range errorTypes { - t.Run(et.name, func(t *testing.T) { - sst := SST{ - SourceName: fmt.Sprintf("test%d", i), - Scope: "scope", - Type: "type", - } - method := sdp.QueryMethod_GET - query := "test" - ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &query} - - // Store error - qErr := &sdp.QueryError{ - ErrorType: et.errorType, - ErrorString: fmt.Sprintf("test error %s", et.name), - Scope: sst.Scope, - SourceName: sst.SourceName, - ItemType: sst.Type, - } - cache.StoreError(ctx, qErr, 10*time.Second, ck) - - // Lookup should return cached error - cacheHit, _, items, returnedErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - if !cacheHit { - t.Error("expected cache hit for cached error") - } - if items != nil { - t.Errorf("expected nil items, got %v", items) - } - if returnedErr == nil { - t.Fatal("expected error to be returned") - } - if returnedErr.GetErrorType() != et.errorType { - t.Errorf("expected error type %v, got %v", et.errorType, returnedErr.GetErrorType()) - } - }) - } - }) - } -} - -func TestGetMinWaitTime(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - minWaitTime := cache.GetMinWaitTime() - - // Should return a positive duration - if minWaitTime <= 0 { - t.Errorf("expected positive duration, got %v", minWaitTime) - } - - // Default should be reasonable (e.g., 5 seconds) - if minWaitTime > time.Minute { - t.Errorf("expected reasonable default (< 1 minute), got %v", minWaitTime) - } - }) - } -} - -func TestEmptyCacheOperations(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - cache := impl.factory() - - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - ck := CacheKey{SST: sst} - - // Search on empty cache - items, err := testSearch(t.Context(), cache, ck) - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("expected ErrCacheNotFound on empty cache, got: %v", err) - } - if len(items) != 0 { - t.Errorf("expected 0 items on empty cache, got %v", len(items)) - } - - // Delete on empty cache (should be idempotent) - cache.Delete(ck) - - // Purge on empty cache - stats := cache.Purge(t.Context(), time.Now()) - if stats.NumPurged != 0 { - t.Errorf("expected 0 items purged on empty cache, got %v", stats.NumPurged) - } - if stats.NextExpiry != nil { - t.Errorf("expected nil NextExpiry on empty cache, got %v", stats.NextExpiry) - } - - // Clear on empty cache (should not error) - cache.Clear() - }) - } -} - -func TestMultipleItemsSameSST(t *testing.T) { - implementations := cacheImplementations(t) - - for _, impl := range implementations { - t.Run(impl.name, func(t *testing.T) { - ctx := t.Context() - cache := impl.factory() - - sst := SST{SourceName: "test", Scope: "scope", Type: "type"} - method := sdp.QueryMethod_GET - - // Store multiple items with same SST but different unique attributes - items := make([]*sdp.Item, 3) - for i := range 3 { - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - uav := fmt.Sprintf("item%d", i) - - // Set the item's unique attribute value to match the CacheKey - attrs := make(map[string]interface{}) - if item.GetAttributes() != nil && item.GetAttributes().GetAttrStruct() != nil { - for k, v := range item.GetAttributes().GetAttrStruct().GetFields() { - attrs[k] = v - } - } - attrs["name"] = uav - attributes, _ := sdp.ToAttributes(attrs) - item.Attributes = attributes - - ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} - cache.StoreItem(ctx, item, 10*time.Second, ck) - items[i] = item - } - - // Search with SST only should return all 3 items - allItems, err := testSearch(t.Context(), cache, CacheKey{SST: sst}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(allItems) != 3 { - t.Errorf("expected 3 items, got %v", len(allItems)) - } - - // Search with specific unique attribute should return only that item - for i := range 3 { - uav := fmt.Sprintf("item%d", i) - ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} - foundItems, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Errorf("unexpected error for item%d: %v", i, err) - } - if len(foundItems) != 1 { - t.Errorf("expected 1 item for item%d, got %v", i, len(foundItems)) - } - } - }) - } -} - -// Implementation-specific tests for MemoryCache - -// TestMemoryCacheStartPurge tests the memory cache implementation's purger -func TestMemoryCacheStartPurge(t *testing.T) { - ctx := t.Context() - cache := NewMemoryCache() - cache.MinWaitTime = 100 * time.Millisecond - - cachedItems := []struct { - Item *sdp.Item - Expiry time.Time - }{ - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(0), - }, - { - Item: GenerateRandomItem(), - Expiry: time.Now().Add(100 * time.Millisecond), - }, - } - - for _, i := range cachedItems { - ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, i.Item, time.Until(i.Expiry), ck) - } - - ctx, done := context.WithCancel(ctx) - defer done() - - cache.StartPurger(ctx) - - // Wait for everything to be purged - time.Sleep(200 * time.Millisecond) - - // At this point everything should be been cleaned, and the purger should be - // sleeping forever - items, err := testSearch(t.Context(), cache, CacheKeyFromQuery( - cachedItems[1].Item.GetMetadata().GetSourceQuery(), - cachedItems[1].Item.GetMetadata().GetSourceName(), - )) - - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("unexpected error: %v", err) - t.Errorf("unexpected items: %v", len(items)) - } - - cache.purgeMutex.Lock() - if cache.nextPurge.Before(time.Now().Add(time.Hour)) { - // If the next purge is within the next hour that's an error, it should - // be really, really for in the future - t.Errorf("Expected next purge to be in 1000 years, got %v", cache.nextPurge.String()) - } - cache.purgeMutex.Unlock() - - // Adding a new item should kick off the purging again - for _, i := range cachedItems { - ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, i.Item, 100*time.Millisecond, ck) - } - - time.Sleep(200 * time.Millisecond) - - // It should be empty again - items, err = testSearch(t.Context(), cache, CacheKeyFromQuery( - cachedItems[1].Item.GetMetadata().GetSourceQuery(), - cachedItems[1].Item.GetMetadata().GetSourceName(), - )) - - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("unexpected error: %v", err) - t.Errorf("unexpected items: %v: %v", len(items), items) - } -} - -// TestMemoryCacheStopPurge tests the memory cache implementation's purger stop functionality -func TestMemoryCacheStopPurge(t *testing.T) { - cache := NewMemoryCache() - cache.MinWaitTime = 1 * time.Millisecond - - ctx, done := context.WithCancel(t.Context()) - - cache.StartPurger(ctx) - - // Stop the purger - done() - - // Insert an item - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(ctx, item, 1*time.Second, ck) - sst := SST{ - SourceName: item.GetMetadata().GetSourceName(), - Scope: item.GetScope(), - Type: item.GetType(), - } - - // Make sure it's not purged - time.Sleep(100 * time.Millisecond) - items, err := testSearch(t.Context(), cache, CacheKey{ - SST: sst, - }) - if err != nil { - t.Error(err) - } - - if len(items) != 1 { - t.Errorf("Expected 1 item, got %v", len(items)) - } -} - -// TestMemoryCacheConcurrent tests the memory cache implementation for data races. -// This test is designed to be run with -race to ensure that there aren't any -// data races -func TestMemoryCacheConcurrent(t *testing.T) { - cache := NewMemoryCache() - // Run the purger super fast to generate a worst-case scenario - cache.MinWaitTime = 1 * time.Millisecond - - ctx, done := context.WithCancel(t.Context()) - defer done() - cache.StartPurger(ctx) - var wg sync.WaitGroup - - numParallel := 1_000 - - for range numParallel { - wg.Add(1) - go func() { - defer wg.Done() - // Store the item - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - cache.StoreItem(ctx, item, 100*time.Millisecond, ck) - - wg.Add(1) - // Create a goroutine to also delete in parallel - go func() { - defer wg.Done() - cache.Delete(ck) - }() - }() - } - - wg.Wait() -} - -// TestMemoryCacheLookupDeduplication tests that multiple concurrent Lookup calls -// for the same cache key in MemoryCache result in only one caller doing work. -func TestMemoryCacheLookupDeduplication(t *testing.T) { - cache := NewMemoryCache() - ctx := t.Context() - - // Create a cache key for the test - use LIST method to avoid UniqueAttributeValue matching issues - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - - // Track how many goroutines actually do work - var workCount int32 - var mu sync.Mutex - var wg sync.WaitGroup - - numGoroutines := 10 - results := make([]struct { - hit bool - items []*sdp.Item - }, numGoroutines) - - startBarrier := make(chan struct{}) - - for i := range numGoroutines { - wg.Add(1) - go func(idx int) { - defer wg.Done() - <-startBarrier - - hit, ck, items, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) - defer done() - - if !hit { - mu.Lock() - workCount++ - mu.Unlock() - - time.Sleep(50 * time.Millisecond) - - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - - cache.StoreItem(ctx, item, 10*time.Second, ck) - hit, _, items, _, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) - defer done() - } - - results[idx] = struct { - hit bool - items []*sdp.Item - }{hit, items} - }(i) - } - - close(startBarrier) - wg.Wait() - - if workCount != 1 { - t.Errorf("expected exactly 1 goroutine to do work, got %d", workCount) - } - - for i, r := range results { - if !r.hit { - t.Errorf("goroutine %d: expected cache hit after dedup, got miss", i) - } - if len(r.items) != 1 { - t.Errorf("goroutine %d: expected 1 item, got %d", i, len(r.items)) - } - } -} - -// TestMemoryCacheLookupDeduplicationCompleteWithoutStore tests the scenario where -// Complete is called but nothing was stored in the cache. This tests the explicit -// ErrCacheNotFound check in the re-check logic. -func TestMemoryCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { - cache := NewMemoryCache() - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "complete-without-store-test" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // Track results - var waiterHits []bool - var waiterMu sync.Mutex - - numWaiters := 3 - - // First goroutine: starts work and completes without storing anything - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that completes successfully but returns nothing - time.Sleep(50 * time.Millisecond) - - // Complete without storing anything - triggers ErrCacheNotFound on re-check - cache.pending.Complete(ck.String()) - }() - - // Waiter goroutines - for range numWaiters { - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - time.Sleep(10 * time.Millisecond) - - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - waiterMu.Lock() - waiterHits = append(waiterHits, hit) - waiterMu.Unlock() - }() - } - - close(startBarrier) - wg.Wait() - - if len(waiterHits) != numWaiters { - t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) - } - - // All waiters should get a cache miss since nothing was stored - for i, hit := range waiterHits { - if hit { - t.Errorf("waiter %d: expected cache miss (hit=false), got hit=true", i) - } - } -} - -func TestToIndexValues(t *testing.T) { - ck := CacheKey{ - SST: SST{ - SourceName: "foo", - Scope: "foo", - Type: "foo", - }, - } - - t.Run("with just SST", func(t *testing.T) { - iv := ck.ToIndexValues() - - if iv.SSTHash != ck.SST.Hash() { - t.Error("hash mismatch") - } - }) - - t.Run("with SST & Method", func(t *testing.T) { - ck.Method = sdp.QueryMethod_GET.Enum() - iv := ck.ToIndexValues() - - if iv.Method != sdp.QueryMethod_GET { - t.Errorf("expected %v, got %v", sdp.QueryMethod_GET, iv.Method) - } - }) - - t.Run("with SST & Query", func(t *testing.T) { - q := "query" - ck.Query = &q - iv := ck.ToIndexValues() - - if iv.Query != "query" { - t.Errorf("expected %v, got %v", "query", iv.Query) - } - }) - - t.Run("with SST & UniqueAttributeValue", func(t *testing.T) { - q := "foo" - ck.UniqueAttributeValue = &q - iv := ck.ToIndexValues() - - if iv.UniqueAttributeValue != "foo" { - t.Errorf("expected %v, got %v", "foo", iv.UniqueAttributeValue) - } - }) -} - -func TestUnexpiredOverwriteLogging(t *testing.T) { - cache := NewCache(t.Context()) - - t.Run("overwriting unexpired entry increments counter", func(t *testing.T) { - ctx := t.Context() - // Create an item and cache key - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - // Store the item with a long TTL (10 seconds) - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Store the same item again before it expires (overwrite will be tracked via span attributes) - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Store it again - cache.StoreItem(ctx, item, 10*time.Second, ck) - }) - - t.Run("overwriting expired entry does not increment counter", func(t *testing.T) { - ctx := t.Context() - // Create a new cache for this test - cache := NewCache(ctx) - - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - - // Store the item with a very short TTL - cache.StoreItem(ctx, item, 1*time.Millisecond, ck) - - // Wait for it to expire - time.Sleep(10 * time.Millisecond) - - // Store the same item again after it expired (overwrite tracking via span attributes) - cache.StoreItem(ctx, item, 10*time.Second, ck) - }) - - t.Run("overwriting different items does not increment counter", func(t *testing.T) { - ctx := t.Context() - // Create a new cache for this test - cache := NewCache(ctx) - - item1 := GenerateRandomItem() - item2 := GenerateRandomItem() - - ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), item1.GetMetadata().GetSourceName()) - ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), item2.GetMetadata().GetSourceName()) - - // Store two different items (no overwrites, just new items) - cache.StoreItem(ctx, item1, 10*time.Second, ck1) - cache.StoreItem(ctx, item2, 10*time.Second, ck2) - }) - - t.Run("overwriting error entries increments counter", func(t *testing.T) { - ctx := t.Context() - // Create a new cache for this test - cache := NewCache(ctx) - - sst := SST{ - SourceName: "test-source", - Scope: "test-scope", - Type: "test-type", - } - - method := sdp.QueryMethod_LIST - query := "test-query" - - ck := CacheKey{ - SST: sst, - Method: &method, - Query: &query, - } - - // Store an error - cache.StoreError(ctx, errors.New("test error"), 10*time.Second, ck) - - // Store the same error again before it expires (overwrite will be tracked via span attributes) - cache.StoreError(ctx, errors.New("another error"), 10*time.Second, ck) - }) -} - -// TestBoltCacheCloseAndDestroy verifies that CloseAndDestroy() correctly -// closes the database and deletes the cache file. -func TestBoltCacheCloseAndDestroy(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - // Create a cache and store some data - ctx := t.Context() - cache1, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - - // Store an item - item1 := GenerateRandomItem() - ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), item1.GetMetadata().GetSourceName()) - cache1.StoreItem(ctx, item1, 10*time.Second, ck1) - - // Store another item with a short TTL (will expire) - item2 := GenerateRandomItem() - ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), item2.GetMetadata().GetSourceName()) - cache1.StoreItem(ctx, item2, 100*time.Millisecond, ck2) - - // Verify both items are in the cache - items, err := testSearch(t.Context(), cache1, ck1) - if err != nil { - t.Errorf("failed to search for item1: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item for ck1, got %d", len(items)) - } - - // Verify the cache file exists - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - t.Fatal("cache file should exist before CloseAndDestroy") - } - - // Close and destroy the cache - if err := cache1.CloseAndDestroy(); err != nil { - t.Fatalf("failed to close and destroy cache1: %v", err) - } - - // Verify the cache file is deleted - if _, err := os.Stat(cachePath); !os.IsNotExist(err) { - t.Error("cache file should be deleted after CloseAndDestroy") - } - - // Create a new cache at the same path - should create a fresh, empty cache - cache2, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create new BoltCache: %v", err) - } - defer func() { - _ = cache2.CloseAndDestroy() - }() - - // Verify the old item is NOT accessible (cache was destroyed) - items, err = testSearch(ctx, cache2, ck1) - if !errors.Is(err, ErrCacheNotFound) { - t.Errorf("expected cache miss for item1 in new cache, got: err=%v, items=%d", err, len(items)) - } - - // Verify we can store new items in the fresh cache - item3 := GenerateRandomItem() - ck3 := CacheKeyFromQuery(item3.GetMetadata().GetSourceQuery(), item3.GetMetadata().GetSourceName()) - cache2.StoreItem(ctx, item3, 10*time.Second, ck3) - - items, err = testSearch(ctx, cache2, ck3) - if err != nil { - t.Errorf("failed to search for newly stored item3: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item for ck3, got %d", len(items)) - } -} - -// TestBoltCacheOperationsAfterCloseAndDestroy verifies that operations after -// CloseAndDestroy() return proper errors instead of panicking. -func TestBoltCacheOperationsAfterCloseAndDestroy(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - ctx := t.Context() - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - - // Store an item before closing - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Close and destroy the cache - if err := cache.CloseAndDestroy(); err != nil { - t.Fatalf("failed to close and destroy cache: %v", err) - } - - // Now try various operations after the cache is closed and destroyed - // These should return errors, not panic - - t.Run("Search after CloseAndDestroy", func(t *testing.T) { - // This should error because the database is closed - _, err := testSearch(ctx, cache, ck) - if err == nil { - t.Error("expected error when searching after CloseAndDestroy, got nil") - } - t.Logf("Search returned expected error: %v", err) - }) - - t.Run("StoreItem after CloseAndDestroy", func(t *testing.T) { - // This should not panic - it might silently fail or error - // The key is that it doesn't panic - newItem := GenerateRandomItem() - newCk := CacheKeyFromQuery(newItem.GetMetadata().GetSourceQuery(), newItem.GetMetadata().GetSourceName()) - - // This should either complete without panic or handle the closed DB gracefully - cache.StoreItem(ctx, newItem, 10*time.Second, newCk) - t.Log("StoreItem completed without panic (may have failed internally)") - }) - - t.Run("Delete after CloseAndDestroy", func(t *testing.T) { - // This should not panic - cache.Delete(ck) - t.Log("Delete completed without panic (may have failed internally)") - }) - - t.Run("Purge after CloseAndDestroy", func(t *testing.T) { - // This should not panic - stats := cache.Purge(ctx, time.Now()) - t.Logf("Purge completed without panic, purged %d items", stats.NumPurged) - }) -} - -// TestBoltCacheConcurrentCloseAndDestroy verifies that CloseAndDestroy() -// properly synchronizes with concurrent operations using the compaction lock. -func TestBoltCacheConcurrentCloseAndDestroy(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - ctx := t.Context() - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - - // Store some items - for range 10 { - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - } - - // Start some concurrent operations - var wg sync.WaitGroup - numOperations := 50 - - // Launch concurrent read/write operations - for range numOperations { - wg.Add(1) - go func() { - defer wg.Done() - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - }() - } - - // Wait a bit to let operations start - time.Sleep(10 * time.Millisecond) - - // Close and destroy while operations are in flight - // The compaction lock should serialize this properly - wg.Add(1) - go func() { - defer wg.Done() - err := cache.CloseAndDestroy() - if err != nil { - t.Logf("CloseAndDestroy returned error: %v", err) - } - }() - - // Wait for all operations to complete - wg.Wait() - - // Verify the file is deleted - if _, err := os.Stat(cachePath); !os.IsNotExist(err) { - t.Error("cache file should be deleted after CloseAndDestroy") - } - - t.Log("Concurrent operations with CloseAndDestroy completed without data races") -} - -// TestBoltCacheDiskFullErrorDetection tests the isDiskFullError helper function -func TestBoltCacheDiskFullErrorDetection(t *testing.T) { - // This test verifies that isDiskFullError correctly identifies disk full errors - // We can't easily simulate actual disk full in tests, but we can test the detection logic - - // Note: We can't directly test syscall.ENOSPC without actually filling the disk, - // but we can verify the function exists and works with the error types it's designed for. - // In a real scenario, BoltDB would return syscall.ENOSPC when the disk is full. - - // Test that non-disk-full errors are not detected - regularErr := errors.New("some other error") - if isDiskFullError(regularErr) { - t.Error("isDiskFullError should return false for regular errors") - } - - // Test nil error - if isDiskFullError(nil) { - t.Error("isDiskFullError should return false for nil") - } -} - -// TestBoltCacheDeleteOnDiskFull tests that the cache is deleted when disk is full -// and cleanup doesn't help. Since we can't easily simulate disk full in unit tests, -// this test verifies the deleteCacheFile method works correctly. -func TestBoltCacheDeleteOnDiskFull(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - // Create a cache and store some data - ctx := t.Context() - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - - // Store an item - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Verify the cache file exists - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - t.Fatal("cache file should exist") - } - - // Verify item is in cache - items, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Errorf("failed to search: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item, got %d", len(items)) - } - - // Delete the cache file (cache is already *BoltCache) - if err := cache.deleteCacheFile(ctx); err != nil { - t.Fatalf("failed to delete cache file: %v", err) - } - - // Verify the cache file is gone - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - t.Error("cache file should be recreated") - } - - // Verify the database is closed (can't search anymore) - _, _ = testSearch(t.Context(), cache, ck) - // The search might fail or return empty, but the important thing is the file is gone - // and we can't use the cache anymore -} - -// TestBoltCacheDiskFullDuringCompact tests error handling during compaction. -// Since we can't easily simulate disk full, this test verifies the compaction -// process works normally and that the error handling paths exist. -func TestBoltCacheDiskFullDuringCompact(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath, WithCompactThreshold(1024)) // Small threshold to trigger compaction - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { - _ = cache.CloseAndDestroy() - }() - - ctx := t.Context() - - // Store enough items to trigger compaction - // We'll store items and then delete them to accumulate deleted bytes - for range 10 { - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - } - - // Manually set deleted bytes to trigger compaction - cache.addDeletedBytes(cache.CompactThreshold) - - // Trigger purge which should trigger compaction - stats := cache.Purge(ctx, time.Now().Add(-1*time.Hour)) // Purge items from an hour ago (none should exist) - _ = stats // Use stats to avoid unused variable - - // Verify cache still works after compaction attempt - item := GenerateRandomItem() - ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) - cache.StoreItem(ctx, item, 10*time.Second, ck) - - items, err := testSearch(t.Context(), cache, ck) - if err != nil { - t.Errorf("failed to search after compaction: %v", err) - } - if len(items) != 1 { - t.Errorf("expected 1 item after compaction, got %d", len(items)) - } -} - -// TestBoltCacheLookupDeduplication tests that multiple concurrent Lookup calls -// for the same cache key result in only one caller doing the actual work. -func TestBoltCacheLookupDeduplication(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { _ = cache.CloseAndDestroy() }() - - ctx := t.Context() - - // Create a cache key for the test - use LIST method to avoid UniqueAttributeValue matching issues - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - - // Track how many goroutines actually do work (get cache miss as first caller) - var workCount int32 - var mu sync.Mutex - var wg sync.WaitGroup - - numGoroutines := 10 - results := make([]struct { - hit bool - items []*sdp.Item - err *sdp.QueryError - }, numGoroutines) - - // Start barrier to ensure all goroutines start at roughly the same time - startBarrier := make(chan struct{}) - - for i := range numGoroutines { - wg.Add(1) - go func(idx int) { - defer wg.Done() - - // Wait for the start signal - <-startBarrier - - // Lookup the cache - all should get miss initially - hit, ck, items, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) - defer done() - - if !hit { - // This goroutine is doing the work - mu.Lock() - workCount++ - mu.Unlock() - - // Simulate some work - time.Sleep(50 * time.Millisecond) - - // Create and store the item - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - item.Metadata.SourceName = sst.SourceName - - cache.StoreItem(ctx, item, 10*time.Second, ck) - - // Re-lookup to get the stored item for our result - hit, _, items, qErr, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) - defer done() - } - - results[idx] = struct { - hit bool - items []*sdp.Item - err *sdp.QueryError - }{hit, items, qErr} - }(i) - } - - // Release all goroutines at once - close(startBarrier) - - // Wait for all goroutines to complete - wg.Wait() - - // Verify that only one goroutine did the work - if workCount != 1 { - t.Errorf("expected exactly 1 goroutine to do work, got %d", workCount) - } - - // Verify all goroutines got results - for i, r := range results { - if !r.hit { - t.Errorf("goroutine %d: expected cache hit after dedup, got miss", i) - } - if len(r.items) != 1 { - t.Errorf("goroutine %d: expected 1 item, got %d", i, len(r.items)) - } - } -} - -// TestBoltCacheLookupDeduplicationTimeout tests that waiters properly timeout -// when the context is cancelled. -func TestBoltCacheLookupDeduplicationTimeout(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { _ = cache.CloseAndDestroy() }() - - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_GET - query := "timeout-test" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // First goroutine: does the work but takes a long time - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate slow work - time.Sleep(500 * time.Millisecond) - - // Store the item - item := GenerateRandomItem() - item.Scope = sst.Scope - item.Type = sst.Type - cache.StoreItem(ctx, item, 10*time.Second, ck) - }() - - // Second goroutine: should timeout waiting - var secondHit bool - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - // Use a short timeout context - shortCtx, done := context.WithTimeout(ctx, 50*time.Millisecond) - defer done() - - hit, _, _, _, done := cache.Lookup(shortCtx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - secondHit = hit - }() - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // Second goroutine should have timed out and returned miss - if secondHit { - t.Error("second goroutine should have timed out and returned miss") - } -} - -// TestBoltCacheLookupDeduplicationError tests that waiters receive the error -// when the first caller stores an error. -func TestBoltCacheLookupDeduplicationError(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { _ = cache.CloseAndDestroy() }() - - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_GET - query := "error-test" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - expectedError := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "item not found", - Scope: sst.Scope, - SourceName: sst.SourceName, - ItemType: sst.Type, - } - - // Track results from waiters - var waiterErrors []*sdp.QueryError - var waiterMu sync.Mutex - - numWaiters := 5 - - // First goroutine: does the work and stores an error - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that results in an error - time.Sleep(50 * time.Millisecond) - - // Store the error - cache.StoreError(ctx, expectedError, 10*time.Second, ck) - }() - - // Waiter goroutines: should receive the error - for range numWaiters { - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - hit, _, _, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - waiterMu.Lock() - if hit && qErr != nil { - waiterErrors = append(waiterErrors, qErr) - } - waiterMu.Unlock() - }() - } - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // All waiters should have received the error - if len(waiterErrors) != numWaiters { - t.Errorf("expected %d waiters to receive error, got %d", numWaiters, len(waiterErrors)) - } - - // Verify the error content - for i, qErr := range waiterErrors { - if qErr.GetErrorType() != expectedError.GetErrorType() { - t.Errorf("waiter %d: expected error type %v, got %v", i, expectedError.GetErrorType(), qErr.GetErrorType()) - } - } -} - -// TestBoltCacheLookupDeduplicationCancel tests the Cancel() path for error recovery. -func TestBoltCacheLookupDeduplicationCancel(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { _ = cache.CloseAndDestroy() }() - - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_GET - query := "done-test" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // Track results - var waiterHits []bool - var waiterMu sync.Mutex - - numWaiters := 3 - - // First goroutine: starts work but then calls done() without storing anything - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - if hit { - t.Error("first goroutine: expected cache miss") - done() - return - } - - // Simulate work that fails - done the pending work - time.Sleep(50 * time.Millisecond) - done() - }() - - // Waiter goroutines - for range numWaiters { - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - waiterMu.Lock() - waiterHits = append(waiterHits, hit) - waiterMu.Unlock() - }() - } - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // When work is cancelled, waiters receive ok=false from Wait - // (because entry.cancelled is true) and return a cache miss without re-checking. - // This is the correct behavior - waiters don't hang forever and can retry. - if len(waiterHits) != numWaiters { - t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) - } -} - -// TestBoltCacheLookupDeduplicationCompleteWithoutStore tests the scenario where -// Complete is called but nothing was stored in the cache. This tests the explicit -// ErrCacheNotFound check in the re-check logic. -func TestBoltCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { - tempDir := t.TempDir() - cachePath := filepath.Join(tempDir, "cache.db") - - cache, err := NewBoltCache(cachePath) - if err != nil { - t.Fatalf("failed to create BoltCache: %v", err) - } - defer func() { _ = cache.CloseAndDestroy() }() - - ctx := t.Context() - - sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} - method := sdp.QueryMethod_LIST - query := "complete-without-store-test" - - var wg sync.WaitGroup - startBarrier := make(chan struct{}) - - // Track results - var waiterHits []bool - var waiterMu sync.Mutex - - numWaiters := 3 - - // First goroutine: starts work and completes without storing anything - // This simulates a LIST query that returns 0 items - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - if hit { - t.Error("first goroutine: expected cache miss") - return - } - - // Simulate work that completes successfully but returns nothing - time.Sleep(50 * time.Millisecond) - - // Complete without storing anything - no items, no error - // This triggers the ErrCacheNotFound path in waiters' re-check - cache.pending.Complete(ck.String()) - }() - - // Waiter goroutines - for range numWaiters { - wg.Add(1) - go func() { - defer wg.Done() - <-startBarrier - - // Small delay to ensure first goroutine starts first - time.Sleep(10 * time.Millisecond) - - hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) - defer done() - - waiterMu.Lock() - waiterHits = append(waiterHits, hit) - waiterMu.Unlock() - }() - } - - // Release all goroutines - close(startBarrier) - wg.Wait() - - // When Complete is called without storing anything: - // 1. Waiters' Wait returns ok=true (not cancelled) - // 2. Waiters re-check the cache and get ErrCacheNotFound - // 3. Waiters return hit=false (cache miss) - if len(waiterHits) != numWaiters { - t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) - } - - // All waiters should get a cache miss since nothing was stored - for i, hit := range waiterHits { - if hit { - t.Errorf("waiter %d: expected cache miss (hit=false), got hit=true", i) - } - } -} - -// TestPendingWorkUnit tests the pendingWork component in isolation. -func TestPendingWorkUnit(t *testing.T) { - t.Run("StartWork first caller", func(t *testing.T) { - pw := newPendingWork() - shouldWork, entry := pw.StartWork("key1") - - if !shouldWork { - t.Error("first caller should do work") - } - if entry == nil { - t.Error("entry should not be nil") - } - }) - - t.Run("StartWork second caller", func(t *testing.T) { - pw := newPendingWork() - - // First caller - shouldWork1, entry1 := pw.StartWork("key1") - if !shouldWork1 { - t.Error("first caller should do work") - } - - // Second caller for same key - shouldWork2, entry2 := pw.StartWork("key1") - if shouldWork2 { - t.Error("second caller should not do work") - } - if entry2 != entry1 { - t.Error("second caller should get same entry") - } - }) - - t.Run("Complete wakes waiters", func(t *testing.T) { - pw := newPendingWork() - ctx := context.Background() - - // First caller - _, entry := pw.StartWork("key1") - - // Second caller waits - var wg sync.WaitGroup - var waitOk bool - - wg.Add(1) - go func() { - defer wg.Done() - waitOk = pw.Wait(ctx, entry) - }() - - // Give waiter time to start waiting - time.Sleep(10 * time.Millisecond) - - // Complete the work - pw.Complete("key1") - - wg.Wait() - - if !waitOk { - t.Error("wait should succeed") - } - }) - - t.Run("Wait respects context donelation", func(t *testing.T) { - pw := newPendingWork() - ctx, done := context.WithCancel(context.Background()) - - // First caller - _, entry := pw.StartWork("key1") - - // Second caller waits with donelable context - var wg sync.WaitGroup - var waitOk bool - - wg.Add(1) - go func() { - defer wg.Done() - waitOk = pw.Wait(ctx, entry) - }() - - // Give waiter time to start waiting - time.Sleep(10 * time.Millisecond) - - // Cancel the context - done() - - wg.Wait() - - if waitOk { - t.Error("wait should fail due to context donelation") - } - }) -} diff --git a/sdpcache/item_generator_test.go b/sdpcache/item_generator_test.go deleted file mode 100644 index ea21ff0d..00000000 --- a/sdpcache/item_generator_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package sdpcache - -import ( - "math/rand" - "time" - - "github.com/google/uuid" - "github.com/overmindtech/cli/sdp-go" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var Types = []string{ - "person", - "dog", - "kite", - "flag", - "cat", - "leopard", - "fish", - "bird", - "kangaroo", - "ostrich", - "emu", - "hawk", - "mole", - "badger", - "lemur", -} - -const MaxAttributes = 30 -const MaxTags = 10 -const MaxTagKeyLength = 10 -const MaxTagValueLength = 10 -const MaxAttributeKeyLength = 20 -const MaxAttributeValueLength = 50 - -// TODO(LIQs): rewrite this to `MaxEdges` -const MaxLinkedItems = 10 - -// TODO(LIQs): delete -const MaxLinkedItemQueries = 10 - -// GenerateRandomItem Generates a random item and the tags for this item. The -// tags include the name, type and a tag called "all" with a value of "all" -func GenerateRandomItem() *sdp.Item { - attrs := make(map[string]interface{}) - - name := randSeq(rand.Intn(MaxAttributeValueLength)) - typ := Types[rand.Intn(len(Types))] - scope := randSeq(rand.Intn(MaxAttributeKeyLength)) - attrs["name"] = name - - for range rand.Intn(MaxAttributes) { - attrs[randSeq(rand.Intn(MaxAttributeKeyLength))] = randSeq(rand.Intn(MaxAttributeValueLength)) - } - - attributes, _ := sdp.ToAttributes(attrs) - - tags := make(map[string]string) - - for range rand.Intn(MaxTags) { - tags[randSeq(rand.Intn(MaxTagKeyLength))] = randSeq(rand.Intn(MaxTagValueLength)) - } - - // TODO(LIQs): rewrite this to `MaxEdges` and return and additional []*sdp.Edge - linkedItems := make([]*sdp.LinkedItem, rand.Intn(MaxLinkedItems)) - - for i := range linkedItems { - linkedItems[i] = &sdp.LinkedItem{Item: &sdp.Reference{ - Type: randSeq(rand.Intn(MaxAttributeKeyLength)), - UniqueAttributeValue: randSeq(rand.Intn(MaxAttributeValueLength)), - Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), - }} - } - - linkedItemQueries := make([]*sdp.LinkedItemQuery, rand.Intn(MaxLinkedItemQueries)) - - for i := range linkedItemQueries { - linkedItemQueries[i] = &sdp.LinkedItemQuery{Query: &sdp.Query{ - Type: randSeq(rand.Intn(MaxAttributeKeyLength)), - Method: sdp.QueryMethod(rand.Intn(3)), - Query: randSeq(rand.Intn(MaxAttributeValueLength)), - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: rand.Uint32(), - FollowOnlyBlastPropagation: rand.Intn(2) == 0, - }, - Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), - }} - } - - // Generate health (which is an int32 between 0 and 4) - health := sdp.Health(rand.Intn(int(sdp.Health_HEALTH_PENDING) + 1)) - - queryUuid := uuid.New() - - item := sdp.Item{ - Type: typ, - UniqueAttribute: "name", - Attributes: attributes, - Scope: scope, - LinkedItemQueries: linkedItemQueries, - LinkedItems: linkedItems, - Metadata: &sdp.Metadata{ - SourceName: randSeq(rand.Intn(MaxAttributeKeyLength)), - SourceQuery: &sdp.Query{ - Type: typ, - Method: sdp.QueryMethod_GET, - Query: name, - RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: 1, - }, - Scope: scope, - UUID: queryUuid[:], - }, - Timestamp: timestamppb.New(time.Now()), - SourceDuration: durationpb.New(time.Millisecond * time.Duration(rand.Int63())), - SourceDurationPerItem: durationpb.New(time.Millisecond * time.Duration(rand.Int63())), - }, - Tags: tags, - Health: &health, - } - - return &item -} - -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randSeq(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} diff --git a/sdpcache/pending.go b/sdpcache/pending.go deleted file mode 100644 index dc72864b..00000000 --- a/sdpcache/pending.go +++ /dev/null @@ -1,114 +0,0 @@ -package sdpcache - -import ( - "context" - "sync" - "time" -) - -// pendingWork tracks in-flight cache lookups to prevent duplicate work. -// When multiple goroutines request the same cache key simultaneously, -// only the first one does the actual work while others wait for the result. -type pendingWork struct { - mu sync.Mutex - pending map[string]*workEntry -} - -// maxPendingWorkAge is a safety timeout for pending work -const maxPendingWorkAge = 5 * time.Minute - -// workEntry represents a pending piece of work that one or more goroutines -// are waiting on. -type workEntry struct { - done chan struct{} - cancelled bool - startTime time.Time -} - -// newPendingWork creates a new pendingWork tracker. -func newPendingWork() *pendingWork { - return &pendingWork{ - pending: make(map[string]*workEntry), - } -} - -// StartWork checks if work is already pending for the given key. -// If no work is pending, it creates a new entry and returns (true, entry) - -// the caller should do the work and call Complete when done. -// If work is already pending, it returns (false, entry) - the caller should -// call Wait on the entry to get the result. -func (p *pendingWork) StartWork(key string) (shouldWork bool, entry *workEntry) { - p.mu.Lock() - defer p.mu.Unlock() - - if existing, ok := p.pending[key]; ok { - return false, existing - } - - entry = &workEntry{ - done: make(chan struct{}), - startTime: time.Now(), - } - p.pending[key] = entry - return true, entry -} - -// Wait blocks until the work entry is ready or the context is cancelled. -// Returns ok=true if the work completed successfully (caller should re-check cache). -// Returns ok=false if the context was cancelled or work was cancelled. -func (p *pendingWork) Wait(ctx context.Context, entry *workEntry) (ok bool) { - // Calculate safety timeout based on when work started - deadline := entry.startTime.Add(maxPendingWorkAge) - timeUntilDeadline := time.Until(deadline) - - // If we're already past the deadline, return immediately - if timeUntilDeadline <= 0 { - return false - } - - // Create a timer for the safety timeout - timer := time.NewTimer(timeUntilDeadline) - defer timer.Stop() - - select { - case <-entry.done: - // Work completed normally - return !entry.cancelled - case <-ctx.Done(): - // Context was cancelled by caller - return false - case <-timer.C: - // SAFETY: Work has been pending too long (worker likely forgot to call Complete/Cancel) - // Return false so caller gets a cache miss and can retry - return false - } -} - -// Complete marks the work as done and wakes all waiters. -// Waiters will receive ok=true and should re-lookup the cache. -func (p *pendingWork) Complete(key string) { - p.mu.Lock() - defer p.mu.Unlock() - - entry, ok := p.pending[key] - if !ok { - return - } - delete(p.pending, key) - close(entry.done) -} - -// Cancel removes a pending work entry without storing a result. -// Waiters will receive ok=false and should retry or return error. -func (p *pendingWork) Cancel(key string) { - p.mu.Lock() - defer p.mu.Unlock() - - entry, ok := p.pending[key] - if !ok { - return - } - delete(p.pending, key) - entry.cancelled = true - close(entry.done) -} diff --git a/sources/aws/apigateway-api-key.go b/sources/aws/apigateway-api-key.go index 6cd112d4..cf3e5a78 100644 --- a/sources/aws/apigateway-api-key.go +++ b/sources/aws/apigateway-api-key.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/aws/apigateway-stage.go b/sources/aws/apigateway-stage.go index 196c841d..8d9fbb32 100644 --- a/sources/aws/apigateway-stage.go +++ b/sources/aws/apigateway-stage.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/aws/base.go b/sources/aws/base.go index 00ce1909..1c369e72 100644 --- a/sources/aws/base.go +++ b/sources/aws/base.go @@ -3,7 +3,7 @@ package aws import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/aws/errors.go b/sources/aws/errors.go index 2b028d50..7c283261 100644 --- a/sources/aws/errors.go +++ b/sources/aws/errors.go @@ -6,7 +6,7 @@ import ( awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // queryError takes an error and returns a sdp.QueryError. diff --git a/sources/aws/validation_test.go b/sources/aws/validation_test.go index 84c69e96..c5a40f11 100644 --- a/sources/aws/validation_test.go +++ b/sources/aws/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/azure/build/package/Dockerfile b/sources/azure/build/package/Dockerfile index 8108bed6..d53e7e4a 100644 --- a/sources/azure/build/package/Dockerfile +++ b/sources/azure/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/azure/clients/disk-accesses-client.go b/sources/azure/clients/disk-accesses-client.go new file mode 100644 index 00000000..5f7a1b20 --- /dev/null +++ b/sources/azure/clients/disk-accesses-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_disk_accesses_client.go -package=mocks -source=disk-accesses-client.go + +// DiskAccessesPager is a type alias for the generic Pager interface with disk access response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type DiskAccessesPager = Pager[armcompute.DiskAccessesClientListByResourceGroupResponse] + +// DiskAccessesClient is an interface for interacting with Azure disk access +type DiskAccessesClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) DiskAccessesPager + Get(ctx context.Context, resourceGroupName string, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) +} + +type diskAccessesClient struct { + client *armcompute.DiskAccessesClient +} + +func (a *diskAccessesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) DiskAccessesPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *diskAccessesClient) Get(ctx context.Context, resourceGroupName string, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, diskAccessName, options) +} + +// NewDiskAccessesClient creates a new DiskAccessesClient from the Azure SDK client +func NewDiskAccessesClient(client *armcompute.DiskAccessesClient) DiskAccessesClient { + return &diskAccessesClient{client: client} +} \ No newline at end of file diff --git a/sources/azure/cmd/root.go b/sources/azure/cmd/root.go index 2800afe0..df4324cf 100644 --- a/sources/azure/cmd/root.go +++ b/sources/azure/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/workspace/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/discovery" "github.com/overmindtech/cli/sources/azure/proc" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/tracing" ) var cfgFile string diff --git a/sources/azure/integration-tests/authorization-role-assignment_test.go b/sources/azure/integration-tests/authorization-role-assignment_test.go index 8a15c3e8..5982988f 100644 --- a/sources/azure/integration-tests/authorization-role-assignment_test.go +++ b/sources/azure/integration-tests/authorization-role-assignment_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index 6ef8d0aa..c267b75f 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -18,9 +18,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index 874d0f6f..3e7cb904 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-disk-access_test.go b/sources/azure/integration-tests/compute-disk-access_test.go new file mode 100644 index 00000000..5c14aecc --- /dev/null +++ b/sources/azure/integration-tests/compute-disk-access_test.go @@ -0,0 +1,367 @@ +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/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestDiskAccessName = "ovm-integ-test-disk-access" +) + +func TestComputeDiskAccessIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + diskAccessClient, err := armcompute.NewDiskAccessesClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Disk Accesses client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createDiskAccess(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create disk access: %v", err) + } + + err = waitForDiskAccessAvailable(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName) + if err != nil { + t.Fatalf("Failed waiting for disk access to be available: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetDiskAccess", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving disk access %s in subscription %s, resource group %s", + integrationTestDiskAccessName, subscriptionID, integrationTestResourceGroup) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestDiskAccessName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestDiskAccessName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved disk access %s", integrationTestDiskAccessName) + }) + + t.Run("ListDiskAccesses", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing disk accesses in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + + listable, ok := diskAccessAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list disk accesses: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one disk access, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestDiskAccessName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find disk access %s in the list of disk accesses", integrationTestDiskAccessName) + } + + log.Printf("Found %d disk accesses in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for disk access %s", integrationTestDiskAccessName) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDiskAccess.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeDiskAccess.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for disk access %s", integrationTestDiskAccessName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for disk access %s", integrationTestDiskAccessName) + + diskAccessWrapper := manual.NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := diskAccessWrapper.Scopes()[0] + + diskAccessAdapter := sources.WrapperToAdapter(diskAccessWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := diskAccessAdapter.Get(ctx, scope, integrationTestDiskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for disk access %s", len(linkedQueries), integrationTestDiskAccessName) + + // Disk access always has at least one linked query: ComputeDiskAccessPrivateEndpointConnection (SEARCH) + if len(linkedQueries) < 1 { + t.Errorf("Expected at least one linked item query (private endpoint connection), got %d", len(linkedQueries)) + } + + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + bp := liq.GetBlastPropagation() + if bp == nil { + t.Error("Linked item query has nil BlastPropagation") + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) + } else { + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), + bp.GetIn(), bp.GetOut()) + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteDiskAccess(ctx, diskAccessClient, integrationTestResourceGroup, integrationTestDiskAccessName) + if err != nil { + t.Fatalf("Failed to delete disk access: %v", err) + } + }) +} + +// createDiskAccess creates an Azure disk access resource (idempotent). +func createDiskAccess(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName, location string) error { + existing, err := client.Get(ctx, resourceGroupName, diskAccessName, nil) + if err == nil { + if existing.Properties != nil && existing.Properties.ProvisioningState != nil { + state := *existing.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Disk access %s already exists with state %s, skipping creation", diskAccessName, state) + return nil + } + log.Printf("Disk access %s exists but in state %s, will wait for it", diskAccessName, state) + } else { + log.Printf("Disk access %s already exists, skipping creation", diskAccessName) + return nil + } + } + + poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, diskAccessName, armcompute.DiskAccess{ + Location: ptr.To(location), + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-disk-access"), + }, + }, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Disk access %s already exists (conflict), skipping creation", diskAccessName) + return nil + } + return fmt.Errorf("failed to begin creating disk access: %w", err) + } + + resp, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to create disk access: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state != "Succeeded" { + return fmt.Errorf("disk access provisioning state is %s, expected Succeeded", state) + } + log.Printf("Disk access %s created successfully with provisioning state: %s", diskAccessName, state) + } else { + log.Printf("Disk access %s created successfully", diskAccessName) + } + + return nil +} + +// waitForDiskAccessAvailable polls until the disk access is available via the Get API. +func waitForDiskAccessAvailable(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName string) error { + maxAttempts := 20 + pollInterval := 5 * time.Second + + log.Printf("Waiting for disk access %s to be available via API...", diskAccessName) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + resp, err := client.Get(ctx, resourceGroupName, diskAccessName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Disk access %s not yet available (attempt %d/%d), waiting %v...", diskAccessName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + return fmt.Errorf("error checking disk access availability: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Disk access %s is available with provisioning state: %s", diskAccessName, state) + return nil + } + if state == "Failed" { + return fmt.Errorf("disk access provisioning failed with state: %s", state) + } + log.Printf("Disk access %s provisioning state: %s (attempt %d/%d), waiting...", diskAccessName, state, attempt, maxAttempts) + time.Sleep(pollInterval) + continue + } + + log.Printf("Disk access %s is available", diskAccessName) + return nil + } + + return fmt.Errorf("timeout waiting for disk access %s to be available after %d attempts", diskAccessName, maxAttempts) +} + +// deleteDiskAccess deletes an Azure disk access resource. +func deleteDiskAccess(ctx context.Context, client *armcompute.DiskAccessesClient, resourceGroupName, diskAccessName string) error { + poller, err := client.BeginDelete(ctx, resourceGroupName, diskAccessName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Disk access %s not found, skipping deletion", diskAccessName) + return nil + } + return fmt.Errorf("failed to begin deleting disk access: %w", err) + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to delete disk access: %w", err) + } + + log.Printf("Disk access %s deleted successfully", diskAccessName) + return nil +} diff --git a/sources/azure/integration-tests/compute-disk-encryption-set_test.go b/sources/azure/integration-tests/compute-disk-encryption-set_test.go index a800dea6..ddb247fc 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-disk_test.go b/sources/azure/integration-tests/compute-disk_test.go index 2932bbce..c068672d 100644 --- a/sources/azure/integration-tests/compute-disk_test.go +++ b/sources/azure/integration-tests/compute-disk_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-image_test.go b/sources/azure/integration-tests/compute-image_test.go index a9c57f99..01de67f3 100644 --- a/sources/azure/integration-tests/compute-image_test.go +++ b/sources/azure/integration-tests/compute-image_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-proximity-placement-group_test.go b/sources/azure/integration-tests/compute-proximity-placement-group_test.go index c2aae14a..5c12a005 100644 --- a/sources/azure/integration-tests/compute-proximity-placement-group_test.go +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go index 8da2091a..9c4b3a5f 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go index 67482ac1..1d4267df 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go index 0abf3148..6f111bef 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index 5db54d03..b0525875 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/dbforpostgresql-database_test.go b/sources/azure/integration-tests/dbforpostgresql-database_test.go index a5704f46..85e65454 100644 --- a/sources/azure/integration-tests/dbforpostgresql-database_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go index b42ef5e5..b6bc370a 100644 --- a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/documentdb-database-accounts_test.go b/sources/azure/integration-tests/documentdb-database-accounts_test.go index 88d581de..5f9ef70b 100644 --- a/sources/azure/integration-tests/documentdb-database-accounts_test.go +++ b/sources/azure/integration-tests/documentdb-database-accounts_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-managed-hsm_test.go b/sources/azure/integration-tests/keyvault-managed-hsm_test.go index 200e4b5d..d3a9c7b1 100644 --- a/sources/azure/integration-tests/keyvault-managed-hsm_test.go +++ b/sources/azure/integration-tests/keyvault-managed-hsm_test.go @@ -14,9 +14,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-secret_test.go b/sources/azure/integration-tests/keyvault-secret_test.go index 4f4939e9..67433469 100644 --- a/sources/azure/integration-tests/keyvault-secret_test.go +++ b/sources/azure/integration-tests/keyvault-secret_test.go @@ -14,8 +14,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-vault_test.go b/sources/azure/integration-tests/keyvault-vault_test.go index 29d9d390..17d866f1 100644 --- a/sources/azure/integration-tests/keyvault-vault_test.go +++ b/sources/azure/integration-tests/keyvault-vault_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go index 3d1e32e2..2c5f556a 100644 --- a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index a06655fa..08e3ab92 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index eb3aae5e..fb80fb14 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index 828b0049..517dcaee 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-network-security-group_test.go b/sources/azure/integration-tests/network-network-security-group_test.go index a38c54bf..0954a53d 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-public-ip-address_test.go b/sources/azure/integration-tests/network-public-ip-address_test.go index e68169eb..8b343f4d 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index c2d2a8b9..9e0dae8f 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index 2c0e0e43..b08e1a70 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-zone_test.go b/sources/azure/integration-tests/network-zone_test.go index ec54ad17..525774d7 100644 --- a/sources/azure/integration-tests/network-zone_test.go +++ b/sources/azure/integration-tests/network-zone_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index 2646d385..7b46799a 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/sql-server_test.go b/sources/azure/integration-tests/sql-server_test.go index 76a625cb..e7fc3c70 100644 --- a/sources/azure/integration-tests/sql-server_test.go +++ b/sources/azure/integration-tests/sql-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-account_test.go b/sources/azure/integration-tests/storage-account_test.go index eb04c4cb..37641751 100644 --- a/sources/azure/integration-tests/storage-account_test.go +++ b/sources/azure/integration-tests/storage-account_test.go @@ -8,9 +8,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-blob-container_test.go b/sources/azure/integration-tests/storage-blob-container_test.go index 8558917b..755c4f69 100644 --- a/sources/azure/integration-tests/storage-blob-container_test.go +++ b/sources/azure/integration-tests/storage-blob-container_test.go @@ -17,8 +17,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-fileshare_test.go b/sources/azure/integration-tests/storage-fileshare_test.go index 0d2ccd6a..9b503958 100644 --- a/sources/azure/integration-tests/storage-fileshare_test.go +++ b/sources/azure/integration-tests/storage-fileshare_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-queues_test.go b/sources/azure/integration-tests/storage-queues_test.go index 0275ae33..ee1d4697 100644 --- a/sources/azure/integration-tests/storage-queues_test.go +++ b/sources/azure/integration-tests/storage-queues_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-table_test.go b/sources/azure/integration-tests/storage-table_test.go index 51e7954b..292be150 100644 --- a/sources/azure/integration-tests/storage-table_test.go +++ b/sources/azure/integration-tests/storage-table_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc b/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc index 6cb89c24..fb831091 100644 --- a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc +++ b/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc @@ -41,9 +41,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute" // Use specific Azure SDK imports - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 1b1a0697..a8a3f0aa 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -19,8 +19,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -238,6 +238,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred if err != nil { return nil, fmt.Errorf("failed to create zones client: %w", err) } + diskAccessesClient, err := armcompute.NewDiskAccessesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create disk accesses client: %w", err) + } // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { @@ -374,6 +378,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewProximityPlacementGroupsClient(proximityPlacementGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeDiskAccess( + clients.NewDiskAccessesClient(diskAccessesClient), + resourceGroupScopes, + ), cache), ) } @@ -422,6 +430,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeVirtualMachineRunCommand(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeVirtualMachineExtension(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeProximityPlacementGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeDiskAccess(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index 66f6f6eb..14cb7f8f 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -6,13 +6,13 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var AuthorizationRoleAssignmentLookupByName = shared.NewItemTypeLookup("name", azureshared.AuthorizationRoleAssignment) diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index 1635cdbe..7071e122 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index cb78de1b..27af9a19 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var BatchAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchAccount) diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index bd9d37a8..adb32891 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-availability-set.go b/sources/azure/manual/compute-availability-set.go index 8fed33fb..6c0dca2c 100644 --- a/sources/azure/manual/compute-availability-set.go +++ b/sources/azure/manual/compute-availability-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-availability-set_test.go b/sources/azure/manual/compute-availability-set_test.go index ad5cd317..b773940f 100644 --- a/sources/azure/manual/compute-availability-set_test.go +++ b/sources/azure/manual/compute-availability-set_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-disk-access.go b/sources/azure/manual/compute-disk-access.go new file mode 100644 index 00000000..4e7ba8dc --- /dev/null +++ b/sources/azure/manual/compute-disk-access.go @@ -0,0 +1,193 @@ +package manual + +import ( + "context" + "errors" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + discovery "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + sdpcache "github.com/overmindtech/workspace/sdpcache" + sources "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeDiskAccessLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeDiskAccess) + +type computeDiskAccessWrapper struct { + client clients.DiskAccessesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeDiskAccess(client clients.DiskAccessesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeDiskAccessWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.ComputeDiskAccess, + ), + } +} + +func (c *computeDiskAccessWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the disk access name"), scope, c.Type()) + } + diskAccessName := queryParts[0] + if diskAccessName == "" { + return nil, azureshared.QueryError(errors.New("disk access name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + diskAccess, err := c.client.Get(ctx, rgScope.ResourceGroup, diskAccessName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureDiskAccessToSDPItem(&diskAccess.DiskAccess, scope) +} + +func (c *computeDiskAccessWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, diskAccess := range page.Value { + if diskAccess.Name == nil { + continue + } + item, sdpErr := c.azureDiskAccessToSDPItem(diskAccess, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeDiskAccessWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, diskAccess := range page.Value { + if diskAccess.Name == nil { + continue + } + item, sdpErr := c.azureDiskAccessToSDPItem(diskAccess, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} +func (c *computeDiskAccessWrapper) azureDiskAccessToSDPItem(diskAccess *armcompute.DiskAccess, scope string) (*sdp.Item, *sdp.QueryError) { + if diskAccess.Name == nil { + return nil, azureshared.QueryError(errors.New("name is nil"), scope, c.Type()) + } + attributes, err := shared.ToAttributesWithExclude(diskAccess, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + sdpItem := &sdp.Item{ + Type: azureshared.ComputeDiskAccess.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(diskAccess.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Link to Private Endpoint Connections (child resource) + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-accesses/list-private-endpoint-connections + // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/diskAccesses/{diskAccessName}/privateEndpointConnections + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *diskAccess.Name, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // Disk access changes affect private endpoint connections + Out: true, // Private endpoint connection state affects disk access connectivity + }, + }) + + // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections + // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/private-endpoints/get + if diskAccess.Properties != nil && diskAccess.Properties.PrivateEndpointConnections != nil { + for _, peConnection := range diskAccess.Properties.PrivateEndpointConnections { + if peConnection.Properties != nil && peConnection.Properties.PrivateEndpoint != nil && peConnection.Properties.PrivateEndpoint.ID != nil { + privateEndpointID := *peConnection.Properties.PrivateEndpoint.ID + privateEndpointName := azureshared.ExtractResourceName(privateEndpointID) + if privateEndpointName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(privateEndpointID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: privateEndpointName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // Private endpoint changes affect disk access private connectivity + Out: true, // Disk access deletion or config changes may affect the private endpoint connection state + }, + }) + } + } + } + } + + return sdpItem, nil +} + +func (c *computeDiskAccessWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeDiskAccessLookupByName, + } +} + +func (c *computeDiskAccessWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeDiskAccessPrivateEndpointConnection: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (c *computeDiskAccessWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_disk_access.name", + }, + } +} diff --git a/sources/azure/manual/compute-disk-access_test.go b/sources/azure/manual/compute-disk-access_test.go new file mode 100644 index 00000000..bdec9e20 --- /dev/null +++ b/sources/azure/manual/compute-disk-access_test.go @@ -0,0 +1,441 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeDiskAccess(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + diskAccessName := "test-disk-access" + diskAccess := createAzureDiskAccess(diskAccessName) + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, diskAccessName, nil).Return( + armcompute.DiskAccessesClientGetResponse{ + DiskAccess: *diskAccess, + }, nil) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, diskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDiskAccess.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeDiskAccess.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != diskAccessName { + t.Errorf("Expected unique attribute value %s, got %s", diskAccessName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Child resource: Private Endpoint Connections + ExpectedType: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: diskAccessName, + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithPrivateEndpointConnections", func(t *testing.T) { + diskAccessName := "test-disk-access-with-pe" + diskAccess := createAzureDiskAccessWithPrivateEndpointConnections(diskAccessName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, diskAccessName, nil).Return( + armcompute.DiskAccessesClientGetResponse{ + DiskAccess: *diskAccess, + }, nil) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, diskAccessName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeDiskAccessPrivateEndpointConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: diskAccessName, + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + // Network Private Endpoint (same resource group) + ExpectedType: azureshared.NetworkPrivateEndpoint.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-private-endpoint", + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + // Network Private Endpoint (different resource group) + ExpectedType: azureshared.NetworkPrivateEndpoint.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-private-endpoint-other-rg", + ExpectedScope: subscriptionID + ".other-rg", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("disk access not found") + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + armcompute.DiskAccessesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccess2 := createAzureDiskAccess("test-disk-access-2") + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccess2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccess2 := createAzureDiskAccess("test-disk-access-2") + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccess2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + diskAccess1 := createAzureDiskAccess("test-disk-access-1") + diskAccessNilName := &armcompute.DiskAccess{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + mockPager := newMockDiskAccessesPager(ctrl, []*armcompute.DiskAccess{diskAccess1, diskAccessNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + errorPager := newErrorDiskAccessesPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDiskAccessesClient(ctrl) + errorPager := newErrorDiskAccessesPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDiskAccess(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +// createAzureDiskAccess creates a mock Azure Disk Access for testing. +func createAzureDiskAccess(diskAccessName string) *armcompute.DiskAccess { + return &armcompute.DiskAccess{ + Name: to.Ptr(diskAccessName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.DiskAccessProperties{ + ProvisioningState: to.Ptr("Succeeded"), + }, + } +} + +// createAzureDiskAccessWithPrivateEndpointConnections creates a mock Azure Disk Access with private endpoint connections. +func createAzureDiskAccessWithPrivateEndpointConnections(diskAccessName, subscriptionID, resourceGroup string) *armcompute.DiskAccess { + return &armcompute.DiskAccess{ + Name: to.Ptr(diskAccessName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DiskAccessProperties{ + ProvisioningState: to.Ptr("Succeeded"), + PrivateEndpointConnections: []*armcompute.PrivateEndpointConnection{ + { + Name: to.Ptr("pe-connection-1"), + Properties: &armcompute.PrivateEndpointConnectionProperties{ + PrivateEndpoint: &armcompute.PrivateEndpoint{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + }, + }, + }, + { + Name: to.Ptr("pe-connection-2"), + Properties: &armcompute.PrivateEndpointConnectionProperties{ + PrivateEndpoint: &armcompute.PrivateEndpoint{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-other-rg"), + }, + }, + }, + }, + }, + } +} + +// mockDiskAccessesPager is a mock pager for DiskAccessesClientListByResourceGroupResponse. +type mockDiskAccessesPager struct { + ctrl *gomock.Controller + items []*armcompute.DiskAccess + index int + more bool +} + +func newMockDiskAccessesPager(ctrl *gomock.Controller, items []*armcompute.DiskAccess) clients.DiskAccessesPager { + return &mockDiskAccessesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockDiskAccessesPager) More() bool { + return m.more +} + +func (m *mockDiskAccessesPager) NextPage(ctx context.Context) (armcompute.DiskAccessesClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.DiskAccessesClientListByResourceGroupResponse{ + DiskAccessList: armcompute.DiskAccessList{ + Value: []*armcompute.DiskAccess{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.DiskAccessesClientListByResourceGroupResponse{ + DiskAccessList: armcompute.DiskAccessList{ + Value: []*armcompute.DiskAccess{item}, + }, + }, nil +} + +// errorDiskAccessesPager is a mock pager that always returns an error. +type errorDiskAccessesPager struct { + ctrl *gomock.Controller +} + +func newErrorDiskAccessesPager(ctrl *gomock.Controller) clients.DiskAccessesPager { + return &errorDiskAccessesPager{ctrl: ctrl} +} + +func (e *errorDiskAccessesPager) More() bool { + return true +} + +func (e *errorDiskAccessesPager) NextPage(ctx context.Context) (armcompute.DiskAccessesClientListByResourceGroupResponse, error) { + return armcompute.DiskAccessesClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/compute-disk-encryption-set.go b/sources/azure/manual/compute-disk-encryption-set.go index c1d19b8b..8c5d91df 100644 --- a/sources/azure/manual/compute-disk-encryption-set.go +++ b/sources/azure/manual/compute-disk-encryption-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-disk-encryption-set_test.go b/sources/azure/manual/compute-disk-encryption-set_test.go index f243fbf1..e5f75451 100644 --- a/sources/azure/manual/compute-disk-encryption-set_test.go +++ b/sources/azure/manual/compute-disk-encryption-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-disk.go b/sources/azure/manual/compute-disk.go index 79ba209b..d6fd975e 100644 --- a/sources/azure/manual/compute-disk.go +++ b/sources/azure/manual/compute-disk.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index e610f146..0e064480 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-image.go b/sources/azure/manual/compute-image.go index 86bdf6c2..8181f8af 100644 --- a/sources/azure/manual/compute-image.go +++ b/sources/azure/manual/compute-image.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-image_test.go b/sources/azure/manual/compute-image_test.go index 3ee1cfd2..efa8d2d9 100644 --- a/sources/azure/manual/compute-image_test.go +++ b/sources/azure/manual/compute-image_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go index 42029628..a26589da 100644 --- a/sources/azure/manual/compute-proximity-placement-group.go +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -5,13 +5,13 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var ComputeProximityPlacementGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeProximityPlacementGroup) diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go index 23921bae..b3f14b63 100644 --- a/sources/azure/manual/compute-proximity-placement-group_test.go +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-virtual-machine-extension.go b/sources/azure/manual/compute-virtual-machine-extension.go index 5017576e..5856cc69 100644 --- a/sources/azure/manual/compute-virtual-machine-extension.go +++ b/sources/azure/manual/compute-virtual-machine-extension.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index c2cd9a87..9a374bf7 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index 62bede15..2197a1a0 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-run-command_test.go b/sources/azure/manual/compute-virtual-machine-run-command_test.go index e33ef6ac..dcd5929c 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-virtual-machine-scale-set.go b/sources/azure/manual/compute-virtual-machine-scale-set.go index 44b2354e..601dca50 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-scale-set_test.go b/sources/azure/manual/compute-virtual-machine-scale-set_test.go index fbdc03e9..d2b663a9 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine.go b/sources/azure/manual/compute-virtual-machine.go index 5e5f78e9..710577b0 100644 --- a/sources/azure/manual/compute-virtual-machine.go +++ b/sources/azure/manual/compute-virtual-machine.go @@ -6,9 +6,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine_test.go b/sources/azure/manual/compute-virtual-machine_test.go index 857b9b64..c5dcd69c 100644 --- a/sources/azure/manual/compute-virtual-machine_test.go +++ b/sources/azure/manual/compute-virtual-machine_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-database.go b/sources/azure/manual/dbforpostgresql-database.go index 0d6f3a8a..a263362c 100644 --- a/sources/azure/manual/dbforpostgresql-database.go +++ b/sources/azure/manual/dbforpostgresql-database.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-database_test.go b/sources/azure/manual/dbforpostgresql-database_test.go index 43d71ff5..fcd04b67 100644 --- a/sources/azure/manual/dbforpostgresql-database_test.go +++ b/sources/azure/manual/dbforpostgresql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/dbforpostgresql-flexible-server.go b/sources/azure/manual/dbforpostgresql-flexible-server.go index c77f080b..c116dc18 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index 600ac3b6..4090c383 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/dns_links.go b/sources/azure/manual/dns_links.go index 8f9f96da..c03a1867 100644 --- a/sources/azure/manual/dns_links.go +++ b/sources/azure/manual/dns_links.go @@ -3,7 +3,7 @@ package manual import ( "net" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 51afb676..5757c854 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index e6905f10..228a25a8 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-managed-hsm.go b/sources/azure/manual/keyvault-managed-hsm.go index 12be8064..daeb232e 100644 --- a/sources/azure/manual/keyvault-managed-hsm.go +++ b/sources/azure/manual/keyvault-managed-hsm.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index 056613cc..03eeb7e0 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index b0aba42c..88e758da 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index 65a69ae4..b31cf531 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index 5116777c..d51fabfe 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index ce440e9c..c0cca2d0 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index 40b84657..b892839d 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -1,7 +1,7 @@ package manual import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/azure/manual/managedidentity-user-assigned-identity.go b/sources/azure/manual/managedidentity-user-assigned-identity.go index c8e3258f..66f85f67 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/managedidentity-user-assigned-identity_test.go b/sources/azure/manual/managedidentity-user-assigned-identity_test.go index 67716545..dd7534e4 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index 76288cd9..49a2e28c 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index 53afb962..1840ec20 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index 187b50fe..0c681b7d 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -7,14 +7,14 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var NetworkLoadBalancerLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkLoadBalancer) diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index 92d6b5cc..70d16a3d 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 1e8fe0f9..5811ab1c 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var NetworkNetworkInterfaceLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkInterface) diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index d491ac3e..c0229cac 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index 30e19904..f61dda88 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index 559c196a..c7a2d0dc 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index f06ed5ff..7775222f 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index 6da3eeff..3adf2b2a 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 0194ef5a..10c569a6 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var NetworkRouteTableLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkRouteTable) diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 3dd23909..2d57c078 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index 4cc91fac..63c2c026 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/sdpcache" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/workspace/discovery" ) var NetworkVirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index e230a831..a7641002 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-zone.go b/sources/azure/manual/network-zone.go index aaec32ff..fe28f5e2 100644 --- a/sources/azure/manual/network-zone.go +++ b/sources/azure/manual/network-zone.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-zone_test.go b/sources/azure/manual/network-zone_test.go index e2c919de..946db118 100644 --- a/sources/azure/manual/network-zone_test.go +++ b/sources/azure/manual/network-zone_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index 34a1f32c..8a28e0e1 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index 4e08fba1..1f9da171 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/sql-server.go b/sources/azure/manual/sql-server.go index c6f7b3f0..8d378720 100644 --- a/sources/azure/manual/sql-server.go +++ b/sources/azure/manual/sql-server.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index 255126f6..7457b34a 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index b6465eb6..3737227a 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -5,14 +5,14 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" ) var StorageAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.StorageAccount) diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 4c595eed..87935781 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index 7c424c87..c20f97f3 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index 9c83b4d0..10492bd7 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index ed3a3366..59937452 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index ab0f5c91..2247cd2b 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index f3121421..406ca758 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index 2e003bcc..920a966a 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index f8db9364..0fef764d 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index 0ecc47cd..b172f224 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/proc/proc.go b/sources/azure/proc/proc.go index b0c78d2e..d4f0936a 100644 --- a/sources/azure/proc/proc.go +++ b/sources/azure/proc/proc.go @@ -10,9 +10,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" // TODO: Uncomment when Azure dynamic adapters are implemented // "github.com/overmindtech/cli/sources/azure/dynamic" diff --git a/sources/azure/proc/proc_test.go b/sources/azure/proc/proc_test.go index 2f5c1533..afb278f5 100644 --- a/sources/azure/proc/proc_test.go +++ b/sources/azure/proc/proc_test.go @@ -6,7 +6,7 @@ import ( // TODO: Uncomment when Azure dynamic adapters are implemented // _ "github.com/overmindtech/cli/sources/azure/dynamic" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" ) diff --git a/sources/azure/shared/adapter-meta.go b/sources/azure/shared/adapter-meta.go index cddb8bac..75b0c606 100644 --- a/sources/azure/shared/adapter-meta.go +++ b/sources/azure/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/base.go b/sources/azure/shared/base.go index e71d1554..20253ba0 100644 --- a/sources/azure/shared/base.go +++ b/sources/azure/shared/base.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/errors.go b/sources/azure/shared/errors.go index 3366db93..8019b62a 100644 --- a/sources/azure/shared/errors.go +++ b/sources/azure/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 1cdd82a3..f27e2f86 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -7,22 +7,23 @@ import "github.com/overmindtech/cli/sources/shared" // to create unique item type identifiers following the pattern: azure-{api}-{resource} var ( // Compute item types - ComputeVirtualMachine = shared.NewItemType(Azure, Compute, VirtualMachine) - ComputeDisk = shared.NewItemType(Azure, Compute, Disk) - ComputeAvailabilitySet = shared.NewItemType(Azure, Compute, AvailabilitySet) - ComputeVirtualMachineExtension = shared.NewItemType(Azure, Compute, VirtualMachineExtension) - ComputeVirtualMachineRunCommand = shared.NewItemType(Azure, Compute, VirtualMachineRunCommand) - ComputeVirtualMachineScaleSet = shared.NewItemType(Azure, Compute, VirtualMachineScaleSet) - ComputeDiskEncryptionSet = shared.NewItemType(Azure, Compute, DiskEncryptionSet) - ComputeProximityPlacementGroup = shared.NewItemType(Azure, Compute, ProximityPlacementGroup) - ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) - ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) - ComputeImage = shared.NewItemType(Azure, Compute, Image) - ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) - ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) - ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) - ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) - ComputeSharedGalleryApplicationVersion = shared.NewItemType(Azure, Compute, SharedGalleryApplicationVersion) + ComputeVirtualMachine = shared.NewItemType(Azure, Compute, VirtualMachine) + ComputeDisk = shared.NewItemType(Azure, Compute, Disk) + ComputeAvailabilitySet = shared.NewItemType(Azure, Compute, AvailabilitySet) + ComputeVirtualMachineExtension = shared.NewItemType(Azure, Compute, VirtualMachineExtension) + ComputeVirtualMachineRunCommand = shared.NewItemType(Azure, Compute, VirtualMachineRunCommand) + ComputeVirtualMachineScaleSet = shared.NewItemType(Azure, Compute, VirtualMachineScaleSet) + ComputeDiskEncryptionSet = shared.NewItemType(Azure, Compute, DiskEncryptionSet) + ComputeProximityPlacementGroup = shared.NewItemType(Azure, Compute, ProximityPlacementGroup) + ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) + ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) + ComputeImage = shared.NewItemType(Azure, Compute, Image) + ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) + ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) + ComputeDiskAccessPrivateEndpointConnection = shared.NewItemType(Azure, Compute, DiskAccessPrivateEndpointConnection) + ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) + ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) + ComputeSharedGalleryApplicationVersion = shared.NewItemType(Azure, Compute, SharedGalleryApplicationVersion) // Network item types NetworkVirtualNetwork = shared.NewItemType(Azure, Network, VirtualNetwork) diff --git a/sources/azure/shared/mocks/mock_disk_accesses_client.go b/sources/azure/shared/mocks/mock_disk_accesses_client.go new file mode 100644 index 00000000..f8a14448 --- /dev/null +++ b/sources/azure/shared/mocks/mock_disk_accesses_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: disk-accesses-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_disk_accesses_client.go -package=mocks -source=disk-accesses-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDiskAccessesClient is a mock of DiskAccessesClient interface. +type MockDiskAccessesClient struct { + ctrl *gomock.Controller + recorder *MockDiskAccessesClientMockRecorder + isgomock struct{} +} + +// MockDiskAccessesClientMockRecorder is the mock recorder for MockDiskAccessesClient. +type MockDiskAccessesClientMockRecorder struct { + mock *MockDiskAccessesClient +} + +// NewMockDiskAccessesClient creates a new mock instance. +func NewMockDiskAccessesClient(ctrl *gomock.Controller) *MockDiskAccessesClient { + mock := &MockDiskAccessesClient{ctrl: ctrl} + mock.recorder = &MockDiskAccessesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDiskAccessesClient) EXPECT() *MockDiskAccessesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDiskAccessesClient) Get(ctx context.Context, resourceGroupName, diskAccessName string, options *armcompute.DiskAccessesClientGetOptions) (armcompute.DiskAccessesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, diskAccessName, options) + ret0, _ := ret[0].(armcompute.DiskAccessesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDiskAccessesClientMockRecorder) Get(ctx, resourceGroupName, diskAccessName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDiskAccessesClient)(nil).Get), ctx, resourceGroupName, diskAccessName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockDiskAccessesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DiskAccessesClientListByResourceGroupOptions) clients.DiskAccessesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.DiskAccessesPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockDiskAccessesClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockDiskAccessesClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 572659ac..744c215d 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -52,22 +52,23 @@ const ( // These represent the actual resource types within each Azure resource provider const ( // Compute resources - VirtualMachine shared.Resource = "virtual-machine" - Disk shared.Resource = "disk" - AvailabilitySet shared.Resource = "availability-set" - VirtualMachineExtension shared.Resource = "virtual-machine-extension" - VirtualMachineRunCommand shared.Resource = "virtual-machine-run-command" - VirtualMachineScaleSet shared.Resource = "virtual-machine-scale-set" - DiskEncryptionSet shared.Resource = "disk-encryption-set" - ProximityPlacementGroup shared.Resource = "proximity-placement-group" - DedicatedHostGroup shared.Resource = "dedicated-host-group" - CapacityReservationGroup shared.Resource = "capacity-reservation-group" - Image shared.Resource = "image" - Snapshot shared.Resource = "snapshot" - DiskAccess shared.Resource = "disk-access" - SharedGalleryImage shared.Resource = "shared-gallery-image" - CommunityGalleryImage shared.Resource = "community-gallery-image" - SharedGalleryApplicationVersion shared.Resource = "shared-gallery-application-version" + VirtualMachine shared.Resource = "virtual-machine" + Disk shared.Resource = "disk" + AvailabilitySet shared.Resource = "availability-set" + VirtualMachineExtension shared.Resource = "virtual-machine-extension" + VirtualMachineRunCommand shared.Resource = "virtual-machine-run-command" + VirtualMachineScaleSet shared.Resource = "virtual-machine-scale-set" + DiskEncryptionSet shared.Resource = "disk-encryption-set" + ProximityPlacementGroup shared.Resource = "proximity-placement-group" + DedicatedHostGroup shared.Resource = "dedicated-host-group" + CapacityReservationGroup shared.Resource = "capacity-reservation-group" + Image shared.Resource = "image" + Snapshot shared.Resource = "snapshot" + DiskAccess shared.Resource = "disk-access" + DiskAccessPrivateEndpointConnection shared.Resource = "disk-access-private-endpoint-connection" + SharedGalleryImage shared.Resource = "shared-gallery-image" + CommunityGalleryImage shared.Resource = "community-gallery-image" + SharedGalleryApplicationVersion shared.Resource = "shared-gallery-application-version" // Network resources VirtualNetwork shared.Resource = "virtual-network" diff --git a/sources/azure/shared/scope.go b/sources/azure/shared/scope.go index 3c0a9281..4adf5e0d 100644 --- a/sources/azure/shared/scope.go +++ b/sources/azure/shared/scope.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/base.go b/sources/example/base.go index 528c9702..80fb6af1 100644 --- a/sources/example/base.go +++ b/sources/example/base.go @@ -3,7 +3,7 @@ package example import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/custom_searchable_listable.go b/sources/example/custom_searchable_listable.go index d7efcd54..0e9bbc32 100644 --- a/sources/example/custom_searchable_listable.go +++ b/sources/example/custom_searchable_listable.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/errors.go b/sources/example/errors.go index 84cd4d8d..b5756565 100644 --- a/sources/example/errors.go +++ b/sources/example/errors.go @@ -3,7 +3,7 @@ package example import ( "errors" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // queryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/example/metadata_test.go b/sources/example/metadata_test.go index a4ecaec8..7f3a5cdc 100644 --- a/sources/example/metadata_test.go +++ b/sources/example/metadata_test.go @@ -8,8 +8,8 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/example/shared" ) diff --git a/sources/example/standard_searchable_listable.go b/sources/example/standard_searchable_listable.go index 755515c0..57a13033 100644 --- a/sources/example/standard_searchable_listable.go +++ b/sources/example/standard_searchable_listable.go @@ -3,7 +3,7 @@ package example import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" exampleshared "github.com/overmindtech/cli/sources/example/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/example/standard_searchable_listable_test.go b/sources/example/standard_searchable_listable_test.go index d20fc984..655f5e77 100644 --- a/sources/example/standard_searchable_listable_test.go +++ b/sources/example/standard_searchable_listable_test.go @@ -6,7 +6,7 @@ import ( "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/example" "github.com/overmindtech/cli/sources/example/mocks" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/example/validation_test.go b/sources/example/validation_test.go index 28dc926e..fa8d6ed3 100644 --- a/sources/example/validation_test.go +++ b/sources/example/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/gcp/build/package/Dockerfile b/sources/gcp/build/package/Dockerfile index 538d6b40..a933bfeb 100644 --- a/sources/gcp/build/package/Dockerfile +++ b/sources/gcp/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/gcp/cmd/root.go b/sources/gcp/cmd/root.go index 30cb4aae..0c6e7640 100644 --- a/sources/gcp/cmd/root.go +++ b/sources/gcp/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/workspace/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/cli/discovery" + "github.com/overmindtech/workspace/discovery" "github.com/overmindtech/cli/sources/gcp/proc" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/tracing" ) var cfgFile string diff --git a/sources/gcp/dynamic/adapter-listable.go b/sources/gcp/dynamic/adapter-listable.go index 5a4d9fda..93f10767 100644 --- a/sources/gcp/dynamic/adapter-listable.go +++ b/sources/gcp/dynamic/adapter-listable.go @@ -6,9 +6,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index 34d39f1f..5ddcd11e 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -7,9 +7,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapter-searchable.go b/sources/gcp/dynamic/adapter-searchable.go index b3f009eb..5ffff54b 100644 --- a/sources/gcp/dynamic/adapter-searchable.go +++ b/sources/gcp/dynamic/adapter-searchable.go @@ -7,9 +7,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index f04c9674..b43063bd 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -8,9 +8,9 @@ import ( "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapters.go b/sources/gcp/dynamic/adapters.go index 82835a22..5e19af55 100644 --- a/sources/gcp/dynamic/adapters.go +++ b/sources/gcp/dynamic/adapters.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc index 02b324a3..12118ff2 100644 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc +++ b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc @@ -29,8 +29,8 @@ When writing unit tests for GCP dynamic adapters in the Overmind codebase, follo // "cloud.google.com/go/bigtable/admin/apiv2/adminpb" // BigTable Admin v2 "cloud.google.com/go/yourservice/apiv1/yourservicepb" // Replace with actual service - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go index a851d1a4..9be4fdcf 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go index bc70f1f0..ff522bd7 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go @@ -22,9 +22,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" "google.golang.org/genproto/googleapis/rpc/status" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go index 8e6b2dd8..02e24ab9 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go index 17858ae4..b77ec13d 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go index 52eb5458..87f3a4e4 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index 40c080a9..24797117 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go index a8e62200..ca74dee1 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go index b676b735..efe28db3 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-model.go b/sources/gcp/dynamic/adapters/ai-platform-model.go index 08750104..29f48853 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index 34e67548..838b4f03 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go index 27c53225..9709ffb8 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go index 24e7b7d0..ff5037d3 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go index 80db140a..695dcdb7 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go index e78e098c..0ed4ba85 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/artifactregistry/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/artifact-registry-repository.go b/sources/gcp/dynamic/adapters/artifact-registry-repository.go index f40e8f28..c57545d6 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-repository.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go index b855610a..f3e90cad 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go index f3d3e4ef..d4909abf 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/bigquery/datatransfer/apiv1/datatransferpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go index d70cd425..ee413fd5 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go index 97576094..abdd53a5 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup.go b/sources/gcp/dynamic/adapters/big-table-admin-backup.go index 7dc0c7e8..0386beca 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go index 1499c31b..a3a30bdb 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go index 58cbc8c4..478f9f0e 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go index 118abc28..2de47266 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index b2c9e399..47bc68c7 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go index 374c819a..936adbbc 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 373c63d7..3a2b8930 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go index d469bbda..363a8197 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go index f565c697..43939e5d 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go index c753b4a9..7b325452 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudbilling/v1" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-build-build.go b/sources/gcp/dynamic/adapters/cloud-build-build.go index 4ed11931..7d3c5a0c 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-build-build_test.go b/sources/gcp/dynamic/adapters/cloud-build-build_test.go index a7bf65a3..dcea1448 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build_test.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/cloudbuild/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go index ff35a22e..51a51ded 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go index 202013e2..cc4f6e2f 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudresourcemanager/v3" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go index d4310b82..42a91d61 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go index 5556f40a..dfd90e90 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go @@ -9,8 +9,8 @@ import ( resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go index 6d9e0b80..d8179495 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go index 86f4cd9f..afa06d04 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function.go b/sources/gcp/dynamic/adapters/cloudfunctions-function.go index 56dd6032..93d1da6c 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go index d3427ba2..7e0cbe47 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/functions/apiv2/functionspb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-accelerator-type.go b/sources/gcp/dynamic/adapters/compute-accelerator-type.go index 4bf173d9..9ea4dca6 100644 --- a/sources/gcp/dynamic/adapters/compute-accelerator-type.go +++ b/sources/gcp/dynamic/adapters/compute-accelerator-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-disk-type.go b/sources/gcp/dynamic/adapters/compute-disk-type.go index 6e305f36..69307f4a 100644 --- a/sources/gcp/dynamic/adapters/compute-disk-type.go +++ b/sources/gcp/dynamic/adapters/compute-disk-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go index ef744fb0..8bbd27d3 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go index cb20f99c..c7463a21 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index db9ff41f..ab10b9bb 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-firewall_test.go b/sources/gcp/dynamic/adapters/compute-firewall_test.go index a6045c50..a5125654 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall_test.go +++ b/sources/gcp/dynamic/adapters/compute-firewall_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-global-address.go b/sources/gcp/dynamic/adapters/compute-global-address.go index 25d8623a..093b9129 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address.go +++ b/sources/gcp/dynamic/adapters/compute-global-address.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-global-address_test.go b/sources/gcp/dynamic/adapters/compute-global-address_test.go index fec76123..e66c34f7 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-address_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go index 0b51b1e8..663ceafe 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go index 5aae12f6..ed26f145 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check.go b/sources/gcp/dynamic/adapters/compute-http-health-check.go index f95aa589..6bfe96c3 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go index 008edb94..2c514227 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -6,9 +6,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index 72dd3c68..d982abbf 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-instance-template_test.go b/sources/gcp/dynamic/adapters/compute-instance-template_test.go index da3482e1..5494bc3b 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template_test.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-license.go b/sources/gcp/dynamic/adapters/compute-license.go index 04dbab29..b4d56b85 100644 --- a/sources/gcp/dynamic/adapters/compute-license.go +++ b/sources/gcp/dynamic/adapters/compute-license.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go index af496af4..29c86303 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go index 9b4f5db7..fb4fdd3c 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-network.go b/sources/gcp/dynamic/adapters/compute-network.go index 6d23683b..5a328373 100644 --- a/sources/gcp/dynamic/adapters/compute-network.go +++ b/sources/gcp/dynamic/adapters/compute-network.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network_test.go b/sources/gcp/dynamic/adapters/compute-network_test.go index cfd6ca31..896c93fe 100644 --- a/sources/gcp/dynamic/adapters/compute-network_test.go +++ b/sources/gcp/dynamic/adapters/compute-network_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index 7bb60655..862c9064 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-project_test.go b/sources/gcp/dynamic/adapters/compute-project_test.go index cb677599..aecf096e 100644 --- a/sources/gcp/dynamic/adapters/compute-project_test.go +++ b/sources/gcp/dynamic/adapters/compute-project_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go index 4feb6f76..f371cb95 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go index de358aae..c4458ffc 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment.go b/sources/gcp/dynamic/adapters/compute-region-commitment.go index 9ab40008..12ddbec4 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go index f095c743..851ad18d 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-resource-policy.go b/sources/gcp/dynamic/adapters/compute-resource-policy.go index 77311d46..f22a4659 100644 --- a/sources/gcp/dynamic/adapters/compute-resource-policy.go +++ b/sources/gcp/dynamic/adapters/compute-resource-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index aa7babeb..799573a0 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-route_test.go b/sources/gcp/dynamic/adapters/compute-route_test.go index 07707fa6..63d859d3 100644 --- a/sources/gcp/dynamic/adapters/compute-route_test.go +++ b/sources/gcp/dynamic/adapters/compute-route_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-router.go b/sources/gcp/dynamic/adapters/compute-router.go index 5093e034..050fee7f 100644 --- a/sources/gcp/dynamic/adapters/compute-router.go +++ b/sources/gcp/dynamic/adapters/compute-router.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index e402422c..27777a40 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go index 95fea2a8..adf5b755 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go index 70d81ca1..217940bc 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy.go b/sources/gcp/dynamic/adapters/compute-ssl-policy.go index 3626809a..9bb9cf32 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go index 712d8bb8..48e4c892 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-storage-pool.go b/sources/gcp/dynamic/adapters/compute-storage-pool.go index e3af0df7..27b681b4 100644 --- a/sources/gcp/dynamic/adapters/compute-storage-pool.go +++ b/sources/gcp/dynamic/adapters/compute-storage-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork.go b/sources/gcp/dynamic/adapters/compute-subnetwork.go index 411e4229..ac4ebec2 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go index b1292a17..c93ba5c3 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go index 07a414c5..51311677 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go index 62c520c4..9fe965e3 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go index 27d1d16f..aec3f12d 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go index c8ac996a..7e38e34e 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-pool.go b/sources/gcp/dynamic/adapters/compute-target-pool.go index 00765b77..73c0c581 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-pool_test.go b/sources/gcp/dynamic/adapters/compute-target-pool_test.go index fd487c1c..49b7c8c1 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-url-map.go b/sources/gcp/dynamic/adapters/compute-url-map.go index aa2acba9..38215b50 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map.go +++ b/sources/gcp/dynamic/adapters/compute-url-map.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-url-map_test.go b/sources/gcp/dynamic/adapters/compute-url-map_test.go index 3849605b..8c61752f 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map_test.go +++ b/sources/gcp/dynamic/adapters/compute-url-map_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go index 3a13fd37..50d5f83f 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go index 5fddfc4a..505bc6e9 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go index 25754813..4a5777ff 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go index 8a13c352..1c8ea8c3 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/container-cluster.go b/sources/gcp/dynamic/adapters/container-cluster.go index 9141bde3..6a773438 100644 --- a/sources/gcp/dynamic/adapters/container-cluster.go +++ b/sources/gcp/dynamic/adapters/container-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 0ac983eb..9bd38706 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/container-node-pool.go b/sources/gcp/dynamic/adapters/container-node-pool.go index b90a78de..92588928 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool.go +++ b/sources/gcp/dynamic/adapters/container-node-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/container-node-pool_test.go b/sources/gcp/dynamic/adapters/container-node-pool_test.go index c636ddf7..d7cb3770 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool_test.go +++ b/sources/gcp/dynamic/adapters/container-node-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataform-repository.go b/sources/gcp/dynamic/adapters/dataform-repository.go index 9a7fd74d..3a67b2b7 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository.go +++ b/sources/gcp/dynamic/adapters/dataform-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/dataform-repository_test.go b/sources/gcp/dynamic/adapters/dataform-repository_test.go index 49debb92..1529449b 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository_test.go +++ b/sources/gcp/dynamic/adapters/dataform-repository_test.go @@ -8,9 +8,9 @@ import ( dataform "google.golang.org/api/dataform/v1beta1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go index 9985cc69..22272151 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go index c1691571..13d1d34d 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go @@ -10,8 +10,8 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan.go b/sources/gcp/dynamic/adapters/dataplex-data-scan.go index 21b9a0d8..83013316 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go index 59ce6d78..693ee367 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group.go b/sources/gcp/dynamic/adapters/dataplex-entry-group.go index 6c530eb3..d5c087ad 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go index 07c3a801..bab58b4c 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/dataplex/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go index 7531bf7e..ee7233cb 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go index 8ecea33b..4b566767 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go @@ -9,8 +9,8 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" "google.golang.org/protobuf/types/known/durationpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster.go b/sources/gcp/dynamic/adapters/dataproc-cluster.go index 3b24daa8..25ec4843 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go index 207c41d0..9d008e9a 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone.go b/sources/gcp/dynamic/adapters/dns-managed-zone.go index 4cf1fd83..470ae6c2 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go index 61b791a8..1000e5a4 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/dns/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact.go b/sources/gcp/dynamic/adapters/essential-contacts-contact.go index febac2e8..cbaaea43 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go index 614c2426..716b8ab1 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/essentialcontacts/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/eventarc-trigger.go b/sources/gcp/dynamic/adapters/eventarc-trigger.go index 35411727..93844c6e 100644 --- a/sources/gcp/dynamic/adapters/eventarc-trigger.go +++ b/sources/gcp/dynamic/adapters/eventarc-trigger.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/file-instance.go b/sources/gcp/dynamic/adapters/file-instance.go index 79448222..ba02b1cd 100644 --- a/sources/gcp/dynamic/adapters/file-instance.go +++ b/sources/gcp/dynamic/adapters/file-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/file-instance_test.go b/sources/gcp/dynamic/adapters/file-instance_test.go index 3d5cd341..1c51a916 100644 --- a/sources/gcp/dynamic/adapters/file-instance_test.go +++ b/sources/gcp/dynamic/adapters/file-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/filestore/apiv1/filestorepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/iam-role.go b/sources/gcp/dynamic/adapters/iam-role.go index 8603f99d..98175155 100644 --- a/sources/gcp/dynamic/adapters/iam-role.go +++ b/sources/gcp/dynamic/adapters/iam-role.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/iam-role_test.go b/sources/gcp/dynamic/adapters/iam-role_test.go index b08e6d48..71e0db5b 100644 --- a/sources/gcp/dynamic/adapters/iam-role_test.go +++ b/sources/gcp/dynamic/adapters/iam-role_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/iam/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-bucket.go b/sources/gcp/dynamic/adapters/logging-bucket.go index 3423d51a..e5d34072 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket.go +++ b/sources/gcp/dynamic/adapters/logging-bucket.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-bucket_test.go b/sources/gcp/dynamic/adapters/logging-bucket_test.go index 1918a014..d8abb7c5 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket_test.go +++ b/sources/gcp/dynamic/adapters/logging-bucket_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-link.go b/sources/gcp/dynamic/adapters/logging-link.go index ffc93799..3395de37 100644 --- a/sources/gcp/dynamic/adapters/logging-link.go +++ b/sources/gcp/dynamic/adapters/logging-link.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-link_test.go b/sources/gcp/dynamic/adapters/logging-link_test.go index 0795164d..34f5bab7 100644 --- a/sources/gcp/dynamic/adapters/logging-link_test.go +++ b/sources/gcp/dynamic/adapters/logging-link_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-saved-query.go b/sources/gcp/dynamic/adapters/logging-saved-query.go index e93ca2ca..120f74c8 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-saved-query_test.go b/sources/gcp/dynamic/adapters/logging-saved-query_test.go index 14f86ed9..a60082f2 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query_test.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/logging/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go index ad72573b..ba26ed13 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index e1ef84a7..7519ca32 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go index 3c00d4ab..034e317f 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go index 1fa3b6e3..0b5e954b 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/monitoring/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go index e69f1623..db62c788 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go index 84c59ad9..f56347bc 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy.go b/sources/gcp/dynamic/adapters/orgpolicy-policy.go index 8985dc6d..aca9b5a8 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go index 4b5b1a4d..9569e6ef 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/orgpolicy/apiv2/orgpolicypb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index 7b5624fe..3e277006 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index 5c381584..61690e67 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index 5af7d57a..e611bed4 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" diff --git a/sources/gcp/dynamic/adapters/pubsub-topic_test.go b/sources/gcp/dynamic/adapters/pubsub-topic_test.go index ee391d40..d966d592 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/redis-instance.go b/sources/gcp/dynamic/adapters/redis-instance.go index 47172a70..b30d6f1b 100644 --- a/sources/gcp/dynamic/adapters/redis-instance.go +++ b/sources/gcp/dynamic/adapters/redis-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index be69192e..2eec3db8 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/redis/apiv1/redispb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-revision.go b/sources/gcp/dynamic/adapters/run-revision.go index 8ecd8526..6e56d253 100644 --- a/sources/gcp/dynamic/adapters/run-revision.go +++ b/sources/gcp/dynamic/adapters/run-revision.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/run-revision_test.go b/sources/gcp/dynamic/adapters/run-revision_test.go index d0624fb4..8848555f 100644 --- a/sources/gcp/dynamic/adapters/run-revision_test.go +++ b/sources/gcp/dynamic/adapters/run-revision_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/run/v2" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-service.go b/sources/gcp/dynamic/adapters/run-service.go index 10b8cd12..0cfa1cba 100644 --- a/sources/gcp/dynamic/adapters/run-service.go +++ b/sources/gcp/dynamic/adapters/run-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/run-service_test.go b/sources/gcp/dynamic/adapters/run-service_test.go index 0407ec00..05e42b12 100644 --- a/sources/gcp/dynamic/adapters/run-service_test.go +++ b/sources/gcp/dynamic/adapters/run-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/run/apiv2/runpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-worker-pool.go b/sources/gcp/dynamic/adapters/run-worker-pool.go index d06d1f5c..412cd99f 100644 --- a/sources/gcp/dynamic/adapters/run-worker-pool.go +++ b/sources/gcp/dynamic/adapters/run-worker-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret.go b/sources/gcp/dynamic/adapters/secret-manager-secret.go index 2edcbe49..43389084 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go index ad0720c9..a7e360f0 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go index 948636e5..ac2ee399 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go index 5a144d85..e5446b55 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/securitycentermanagement/apiv1/securitycentermanagementpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint.go b/sources/gcp/dynamic/adapters/service-directory-endpoint.go index d5daee47..754acc9a 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go index 0152f300..71e078e8 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/servicedirectory/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/service-directory-service.go b/sources/gcp/dynamic/adapters/service-directory-service.go index 2fd12cac..dea25378 100644 --- a/sources/gcp/dynamic/adapters/service-directory-service.go +++ b/sources/gcp/dynamic/adapters/service-directory-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/service-usage-service.go b/sources/gcp/dynamic/adapters/service-usage-service.go index b07eefb6..26e5141c 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service.go +++ b/sources/gcp/dynamic/adapters/service-usage-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/service-usage-service_test.go b/sources/gcp/dynamic/adapters/service-usage-service_test.go index d9526938..39c9029e 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service_test.go +++ b/sources/gcp/dynamic/adapters/service-usage-service_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/serviceusage/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/spanner-backup.go b/sources/gcp/dynamic/adapters/spanner-backup.go index f42309f2..970df1f6 100644 --- a/sources/gcp/dynamic/adapters/spanner-backup.go +++ b/sources/gcp/dynamic/adapters/spanner-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-database.go b/sources/gcp/dynamic/adapters/spanner-database.go index d2347a30..fab2e1ab 100644 --- a/sources/gcp/dynamic/adapters/spanner-database.go +++ b/sources/gcp/dynamic/adapters/spanner-database.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-database_test.go b/sources/gcp/dynamic/adapters/spanner-database_test.go index ef14bb83..a36db73b 100644 --- a/sources/gcp/dynamic/adapters/spanner-database_test.go +++ b/sources/gcp/dynamic/adapters/spanner-database_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/spanner/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/spanner-instance-config.go b/sources/gcp/dynamic/adapters/spanner-instance-config.go index 8583b0f4..2b6cf1d7 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance-config.go +++ b/sources/gcp/dynamic/adapters/spanner-instance-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-instance.go b/sources/gcp/dynamic/adapters/spanner-instance.go index f15c2de9..87aea189 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance.go +++ b/sources/gcp/dynamic/adapters/spanner-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-instance_test.go b/sources/gcp/dynamic/adapters/spanner-instance_test.go index 8fe9fdf5..7990fbf1 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance_test.go +++ b/sources/gcp/dynamic/adapters/spanner-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go index 345cb4ec..6f6a90c2 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup.go b/sources/gcp/dynamic/adapters/sql-admin-backup.go index aac90d48..1edda8ad 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index df1bdb2f..05db9a4d 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance.go b/sources/gcp/dynamic/adapters/sql-admin-instance.go index 7c716d61..c0294b6c 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index c959170b..aef3f3eb 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index 74c86772..eb0c4176 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index 272ceece..30d9ac1a 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/storage/v1" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index e0ecb8e9..db91071e 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go index b8784fa6..8459a0fc 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/storagetransfer/apiv1/storagetransferpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters_test.go b/sources/gcp/dynamic/adapters_test.go index a09cedeb..06e6f25e 100644 --- a/sources/gcp/dynamic/adapters_test.go +++ b/sources/gcp/dynamic/adapters_test.go @@ -4,9 +4,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index ca7a0271..1ec5859d 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -13,9 +13,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/shared_test.go b/sources/gcp/dynamic/shared_test.go index 57f65625..c6ef236d 100644 --- a/sources/gcp/dynamic/shared_test.go +++ b/sources/gcp/dynamic/shared_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/integration-tests/big-query-model_test.go b/sources/gcp/integration-tests/big-query-model_test.go index 7a942d1c..37688097 100644 --- a/sources/gcp/integration-tests/big-query-model_test.go +++ b/sources/gcp/integration-tests/big-query-model_test.go @@ -9,7 +9,7 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-address_test.go b/sources/gcp/integration-tests/compute-address_test.go index f46215d2..c3691def 100644 --- a/sources/gcp/integration-tests/compute-address_test.go +++ b/sources/gcp/integration-tests/compute-address_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-autoscaler_test.go b/sources/gcp/integration-tests/compute-autoscaler_test.go index a9b23d49..9b2454b5 100644 --- a/sources/gcp/integration-tests/compute-autoscaler_test.go +++ b/sources/gcp/integration-tests/compute-autoscaler_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-disk_test.go b/sources/gcp/integration-tests/compute-disk_test.go index 7990bb3e..8f93b675 100644 --- a/sources/gcp/integration-tests/compute-disk_test.go +++ b/sources/gcp/integration-tests/compute-disk_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-forwarding-rule_test.go b/sources/gcp/integration-tests/compute-forwarding-rule_test.go index 9958fc8b..9f1bfdf0 100644 --- a/sources/gcp/integration-tests/compute-forwarding-rule_test.go +++ b/sources/gcp/integration-tests/compute-forwarding-rule_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-healthcheck_test.go b/sources/gcp/integration-tests/compute-healthcheck_test.go index 22e589e9..1f749417 100644 --- a/sources/gcp/integration-tests/compute-healthcheck_test.go +++ b/sources/gcp/integration-tests/compute-healthcheck_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-image_test.go b/sources/gcp/integration-tests/compute-image_test.go index 92362562..5a3a58ba 100644 --- a/sources/gcp/integration-tests/compute-image_test.go +++ b/sources/gcp/integration-tests/compute-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group-manager_test.go b/sources/gcp/integration-tests/compute-instance-group-manager_test.go index 077b4805..7ec88bec 100644 --- a/sources/gcp/integration-tests/compute-instance-group-manager_test.go +++ b/sources/gcp/integration-tests/compute-instance-group-manager_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group_test.go b/sources/gcp/integration-tests/compute-instance-group_test.go index 48dce062..2972942b 100644 --- a/sources/gcp/integration-tests/compute-instance-group_test.go +++ b/sources/gcp/integration-tests/compute-instance-group_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance_test.go b/sources/gcp/integration-tests/compute-instance_test.go index 427c40ea..d5845431 100644 --- a/sources/gcp/integration-tests/compute-instance_test.go +++ b/sources/gcp/integration-tests/compute-instance_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instant-snapshot_test.go b/sources/gcp/integration-tests/compute-instant-snapshot_test.go index d9d0a8c6..16c30f5c 100644 --- a/sources/gcp/integration-tests/compute-instant-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-instant-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-machine-image_test.go b/sources/gcp/integration-tests/compute-machine-image_test.go index 8d63a80e..706151ab 100644 --- a/sources/gcp/integration-tests/compute-machine-image_test.go +++ b/sources/gcp/integration-tests/compute-machine-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-network_test.go b/sources/gcp/integration-tests/compute-network_test.go index 1f6b7b23..3df39bb5 100644 --- a/sources/gcp/integration-tests/compute-network_test.go +++ b/sources/gcp/integration-tests/compute-network_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/compute-node-group_test.go b/sources/gcp/integration-tests/compute-node-group_test.go index 0ade92f9..e5431aa0 100644 --- a/sources/gcp/integration-tests/compute-node-group_test.go +++ b/sources/gcp/integration-tests/compute-node-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-reservation_test.go b/sources/gcp/integration-tests/compute-reservation_test.go index a6b7e8a2..916f99f2 100644 --- a/sources/gcp/integration-tests/compute-reservation_test.go +++ b/sources/gcp/integration-tests/compute-reservation_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-snapshot_test.go b/sources/gcp/integration-tests/compute-snapshot_test.go index 77d472d0..5ecad320 100644 --- a/sources/gcp/integration-tests/compute-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-subnetwork_test.go b/sources/gcp/integration-tests/compute-subnetwork_test.go index 4cc4d84b..01f722dd 100644 --- a/sources/gcp/integration-tests/compute-subnetwork_test.go +++ b/sources/gcp/integration-tests/compute-subnetwork_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/computer-instance-template_test.go b/sources/gcp/integration-tests/computer-instance-template_test.go index 5e99a474..ce171501 100644 --- a/sources/gcp/integration-tests/computer-instance-template_test.go +++ b/sources/gcp/integration-tests/computer-instance-template_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/spanner-database_test.go b/sources/gcp/integration-tests/spanner-database_test.go index 6ca486bf..6ea73a4b 100644 --- a/sources/gcp/integration-tests/spanner-database_test.go +++ b/sources/gcp/integration-tests/spanner-database_test.go @@ -14,7 +14,7 @@ import ( "github.com/googleapis/gax-go/v2/apierror" "google.golang.org/grpc/codes" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/integration-tests/spanner-instance_test.go b/sources/gcp/integration-tests/spanner-instance_test.go index 4d524f58..c302c865 100644 --- a/sources/gcp/integration-tests/spanner-instance_test.go +++ b/sources/gcp/integration-tests/spanner-instance_test.go @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc b/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc index 124a1be7..ca06dbab 100644 --- a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc +++ b/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc @@ -43,9 +43,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" // Use specific protobuf imports "google.golang.org/api/iterator" // For handling GCP iterators - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index c43e3f21..8459ab54 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -11,8 +11,8 @@ import ( "golang.org/x/oauth2" "google.golang.org/api/option" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/manual/big-query-dataset.go b/sources/gcp/manual/big-query-dataset.go index 4b435b6c..e541e64a 100644 --- a/sources/gcp/manual/big-query-dataset.go +++ b/sources/gcp/manual/big-query-dataset.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-dataset_test.go b/sources/gcp/manual/big-query-dataset_test.go index dcb28e15..f69e1460 100644 --- a/sources/gcp/manual/big-query-dataset_test.go +++ b/sources/gcp/manual/big-query-dataset_test.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-model.go b/sources/gcp/manual/big-query-model.go index 936abbc8..2c6775ab 100644 --- a/sources/gcp/manual/big-query-model.go +++ b/sources/gcp/manual/big-query-model.go @@ -5,9 +5,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-model_test.go b/sources/gcp/manual/big-query-model_test.go index 5441a4a3..0a96aac8 100644 --- a/sources/gcp/manual/big-query-model_test.go +++ b/sources/gcp/manual/big-query-model_test.go @@ -7,9 +7,9 @@ import ( bigquery "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-routine.go b/sources/gcp/manual/big-query-routine.go index 3548b49f..12bc8d1e 100644 --- a/sources/gcp/manual/big-query-routine.go +++ b/sources/gcp/manual/big-query-routine.go @@ -5,9 +5,9 @@ import ( "strings" "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-routine_test.go b/sources/gcp/manual/big-query-routine_test.go index 8aa325c8..1832f751 100644 --- a/sources/gcp/manual/big-query-routine_test.go +++ b/sources/gcp/manual/big-query-routine_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-table.go b/sources/gcp/manual/big-query-table.go index 1f9f9475..e5caf391 100644 --- a/sources/gcp/manual/big-query-table.go +++ b/sources/gcp/manual/big-query-table.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-table_test.go b/sources/gcp/manual/big-query-table_test.go index 19bb8809..9c7cb922 100644 --- a/sources/gcp/manual/big-query-table_test.go +++ b/sources/gcp/manual/big-query-table_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version.go b/sources/gcp/manual/cloud-kms-crypto-key-version.go index 302d2fdc..780bfcb1 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go index 9a48356f..00e58eba 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key.go b/sources/gcp/manual/cloud-kms-crypto-key.go index 39a67a0f..a680fe9e 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key.go +++ b/sources/gcp/manual/cloud-kms-crypto-key.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index 9d60c092..f0495470 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-key-ring.go b/sources/gcp/manual/cloud-kms-key-ring.go index fcba983a..e0d6bb88 100644 --- a/sources/gcp/manual/cloud-kms-key-ring.go +++ b/sources/gcp/manual/cloud-kms-key-ring.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index db4b413c..cdd5518a 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index 81844d23..f3c120d7 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index 955f81d3..41faf103 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index 8885aeff..ee85938e 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index 9fd6814a..35422502 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index f70cd8a2..01a19395 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 9b32a63f..67adf147 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index 04350018..d3421599 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index f406f199..4d9daff0 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index a8a967a4..fc5fa1eb 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index 56d86fdd..4cca216e 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index 27bbb796..ecaa9709 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 61ccff56..214df98f 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-image.go b/sources/gcp/manual/compute-image.go index 4f6c68e8..4bf32ac4 100644 --- a/sources/gcp/manual/compute-image.go +++ b/sources/gcp/manual/compute-image.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index 9b4e38f5..df87913e 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -13,9 +13,9 @@ import ( "google.golang.org/grpc/status" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance-group-manager-shared.go b/sources/gcp/manual/compute-instance-group-manager-shared.go index 839f70ef..bd03d01e 100644 --- a/sources/gcp/manual/compute-instance-group-manager-shared.go +++ b/sources/gcp/manual/compute-instance-group-manager-shared.go @@ -6,7 +6,7 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index 15242e86..7cea872f 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index 913392ef..d6574624 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index 5665f459..e40b115e 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index 6477324f..694be0ea 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 46fadc07..64b65b46 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 2b60d307..e15fd4ee 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index 60b8ce64..b5812b7d 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index f460edc6..0a0f1b08 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-machine-image.go b/sources/gcp/manual/compute-machine-image.go index bec7063a..fb03630b 100644 --- a/sources/gcp/manual/compute-machine-image.go +++ b/sources/gcp/manual/compute-machine-image.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index 57b3f1c9..d6c53c04 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index b05239aa..6f166bc1 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -10,9 +10,9 @@ import ( "google.golang.org/protobuf/proto" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index 81c2d901..798eb958 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index a862fae9..bd158950 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index 7467652f..dc588cea 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-region-instance-group-manager.go b/sources/gcp/manual/compute-region-instance-group-manager.go index ed159890..1f9ded9d 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager.go +++ b/sources/gcp/manual/compute-region-instance-group-manager.go @@ -8,9 +8,9 @@ import ( "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/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go index 582751cd..edcdbe7e 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index 08f6ac82..4cee42d3 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 85171276..84d76585 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-security-policy.go b/sources/gcp/manual/compute-security-policy.go index b13c5b57..e1d7af7c 100644 --- a/sources/gcp/manual/compute-security-policy.go +++ b/sources/gcp/manual/compute-security-policy.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index 6000c88b..a17e3587 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-snapshot.go b/sources/gcp/manual/compute-snapshot.go index f1581286..48da95fc 100644 --- a/sources/gcp/manual/compute-snapshot.go +++ b/sources/gcp/manual/compute-snapshot.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 602ba9d9..194b1659 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/iam-service-account-key.go b/sources/gcp/manual/iam-service-account-key.go index fd18c5be..ae64792e 100644 --- a/sources/gcp/manual/iam-service-account-key.go +++ b/sources/gcp/manual/iam-service-account-key.go @@ -6,9 +6,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/iam-service-account-key_test.go b/sources/gcp/manual/iam-service-account-key_test.go index 253fbd87..7fbcce52 100644 --- a/sources/gcp/manual/iam-service-account-key_test.go +++ b/sources/gcp/manual/iam-service-account-key_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "go.uber.org/mock/gomock" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/iam-service-account.go b/sources/gcp/manual/iam-service-account.go index 1390b790..3703dca6 100644 --- a/sources/gcp/manual/iam-service-account.go +++ b/sources/gcp/manual/iam-service-account.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/iam-service-account_test.go b/sources/gcp/manual/iam-service-account_test.go index 9f5ab384..c91fa7e8 100644 --- a/sources/gcp/manual/iam-service-account_test.go +++ b/sources/gcp/manual/iam-service-account_test.go @@ -9,9 +9,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/logging-sink.go b/sources/gcp/manual/logging-sink.go index eb22d8d6..9b058ade 100644 --- a/sources/gcp/manual/logging-sink.go +++ b/sources/gcp/manual/logging-sink.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/logging-sink_test.go b/sources/gcp/manual/logging-sink_test.go index 655a6548..764f5888 100644 --- a/sources/gcp/manual/logging-sink_test.go +++ b/sources/gcp/manual/logging-sink_test.go @@ -10,9 +10,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/proc/proc.go b/sources/gcp/proc/proc.go index 49f8c3b0..229732f2 100644 --- a/sources/gcp/proc/proc.go +++ b/sources/gcp/proc/proc.go @@ -19,9 +19,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/api/option" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" _ "github.com/overmindtech/cli/sources/gcp/dynamic/adapters" // Import all adapters to register them "github.com/overmindtech/cli/sources/gcp/manual" diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index 33f3baa3..cf1bf3bd 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" _ "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "google.golang.org/protobuf/types/known/structpb" diff --git a/sources/gcp/shared/adapter-meta.go b/sources/gcp/shared/adapter-meta.go index fbbf299a..dfbf0748 100644 --- a/sources/gcp/shared/adapter-meta.go +++ b/sources/gcp/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/base.go b/sources/gcp/shared/base.go index 723a1ce9..e41c7d3b 100644 --- a/sources/gcp/shared/base.go +++ b/sources/gcp/shared/base.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/big-query-clients.go b/sources/gcp/shared/big-query-clients.go index 4fbe4b4b..69dedb15 100644 --- a/sources/gcp/shared/big-query-clients.go +++ b/sources/gcp/shared/big-query-clients.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/bigquery" "google.golang.org/api/iterator" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" ) type BigQueryRoutineClient interface { @@ -75,7 +75,7 @@ func NewBigQueryRoutineClient(client *bigquery.Client) BigQueryRoutineClient { } } -//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/sdp-go +//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/workspace/sdp-go type BigQueryDatasetClient interface { Get(ctx context.Context, projectID, datasetID string) (*bigquery.DatasetMetadata, error) List(ctx context.Context, projectID string, toSDPItem func(ctx context.Context, dataset *bigquery.DatasetMetadata) (*sdp.Item, *sdp.QueryError)) ([]*sdp.Item, *sdp.QueryError) diff --git a/sources/gcp/shared/blast-propagations.go b/sources/gcp/shared/blast-propagations.go index ee50bcb5..a094f40f 100644 --- a/sources/gcp/shared/blast-propagations.go +++ b/sources/gcp/shared/blast-propagations.go @@ -1,7 +1,7 @@ package shared import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/shared/cross_project_linking_test.go b/sources/gcp/shared/cross_project_linking_test.go index bac61bd6..6a8d3c91 100644 --- a/sources/gcp/shared/cross_project_linking_test.go +++ b/sources/gcp/shared/cross_project_linking_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestProjectBaseLinkedItemQueryByName_CrossProject verifies that project-level diff --git a/sources/gcp/shared/errors.go b/sources/gcp/shared/errors.go index 6ba4092e..dc9a5e6d 100644 --- a/sources/gcp/shared/errors.go +++ b/sources/gcp/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go index af9c99b4..ca9a9d30 100644 --- a/sources/gcp/shared/kms-asset-loader.go +++ b/sources/gcp/shared/kms-asset-loader.go @@ -15,9 +15,9 @@ import ( "golang.org/x/sync/singleflight" "google.golang.org/protobuf/encoding/protojson" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 82c773d0..6e09cf0b 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/shared/linker_test.go b/sources/gcp/shared/linker_test.go index 1abbb627..9ea5c562 100644 --- a/sources/gcp/shared/linker_test.go +++ b/sources/gcp/shared/linker_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index fab136b8..9c0ef402 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" diff --git a/sources/gcp/shared/manual-adapter-links_test.go b/sources/gcp/shared/manual-adapter-links_test.go index 7298b87c..8a0d00d6 100644 --- a/sources/gcp/shared/manual-adapter-links_test.go +++ b/sources/gcp/shared/manual-adapter-links_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go index bc5b175f..9781a187 100644 --- a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go +++ b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/sdp-go +// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/workspace/sdp-go // // Package mocks is a generated GoMock package. @@ -14,8 +14,8 @@ import ( reflect "reflect" bigquery "cloud.google.com/go/bigquery" - discovery "github.com/overmindtech/cli/discovery" - sdp "github.com/overmindtech/cli/sdp-go" + discovery "github.com/overmindtech/workspace/discovery" + sdp "github.com/overmindtech/workspace/sdp-go" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/gcp/shared/terraform-mappings.go b/sources/gcp/shared/terraform-mappings.go index 0d7b2a79..94c4e367 100644 --- a/sources/gcp/shared/terraform-mappings.go +++ b/sources/gcp/shared/terraform-mappings.go @@ -1,7 +1,7 @@ package shared import ( - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/shared/base.go b/sources/shared/base.go index 735cd3cd..d31c37e3 100644 --- a/sources/shared/base.go +++ b/sources/shared/base.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // Base is a struct that holds fundamental pieces for creating an adapter. diff --git a/sources/shared/testing.go b/sources/shared/testing.go index 18c5eac4..dd136a89 100644 --- a/sources/shared/testing.go +++ b/sources/shared/testing.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" ) // RunStaticTests runs static tests on the given adapter and item. diff --git a/sources/shared/util.go b/sources/shared/util.go index 4baf8289..5ae3e010 100644 --- a/sources/shared/util.go +++ b/sources/shared/util.go @@ -3,7 +3,7 @@ package shared import ( "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // ToAttributesWithExclude converts an interface to SDP attributes using the `sdp.ToAttributesSorted` diff --git a/sources/transformer.go b/sources/transformer.go index 4d673366..76a2cfed 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -8,9 +8,9 @@ import ( "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/transformer_test.go b/sources/transformer_test.go index acbed0c0..00a9d0c6 100644 --- a/sources/transformer_test.go +++ b/sources/transformer_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" aws "github.com/overmindtech/cli/sources/aws/shared" gcp "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/stdlib-source/adapters/certificate.go b/stdlib-source/adapters/certificate.go index 406af2f2..27dbf6cd 100644 --- a/stdlib-source/adapters/certificate.go +++ b/stdlib-source/adapters/certificate.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // CertToName Returns the name of a cert as a string. This is in the format of: diff --git a/stdlib-source/adapters/certificate_test.go b/stdlib-source/adapters/certificate_test.go index 976e6e81..a00ad011 100644 --- a/stdlib-source/adapters/certificate_test.go +++ b/stdlib-source/adapters/certificate_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" ) var chain = `-----BEGIN CERTIFICATE----- diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index c5811aa6..6b464398 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -11,8 +11,8 @@ import ( "github.com/cenkalti/backoff/v5" "github.com/miekg/dns" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) diff --git a/stdlib-source/adapters/dns_test.go b/stdlib-source/adapters/dns_test.go index ca72183d..5fc5c9b1 100644 --- a/stdlib-source/adapters/dns_test.go +++ b/stdlib-source/adapters/dns_test.go @@ -6,9 +6,9 @@ import ( "net" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestSearch(t *testing.T) { diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 90ec4aef..e2971503 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index 362b8ff0..9de2ec95 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) const TestHTTPTimeout = 3 * time.Second diff --git a/stdlib-source/adapters/ip.go b/stdlib-source/adapters/ip.go index fa5d6982..9dcac1ef 100644 --- a/stdlib-source/adapters/ip.go +++ b/stdlib-source/adapters/ip.go @@ -5,7 +5,7 @@ import ( "fmt" "net" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // IPAdapter struct on which all methods are registered diff --git a/stdlib-source/adapters/ip_test.go b/stdlib-source/adapters/ip_test.go index 05ceee6e..0db5e634 100644 --- a/stdlib-source/adapters/ip_test.go +++ b/stdlib-source/adapters/ip_test.go @@ -5,8 +5,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" ) func TestIPGet(t *testing.T) { diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index a85612eb..23287f3e 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -9,9 +9,9 @@ import ( "time" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" "github.com/overmindtech/cli/stdlib-source/adapters/test" _ "embed" diff --git a/stdlib-source/adapters/rdap-asn.go b/stdlib-source/adapters/rdap-asn.go index b60f21ee..15d712f5 100644 --- a/stdlib-source/adapters/rdap-asn.go +++ b/stdlib-source/adapters/rdap-asn.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type RdapASNAdapter struct { diff --git a/stdlib-source/adapters/rdap-asn_test.go b/stdlib-source/adapters/rdap-asn_test.go index 51065960..6a03f0c3 100644 --- a/stdlib-source/adapters/rdap-asn_test.go +++ b/stdlib-source/adapters/rdap-asn_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestASNAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-domain.go b/stdlib-source/adapters/rdap-domain.go index 2fe4421c..5e61b8bf 100644 --- a/stdlib-source/adapters/rdap-domain.go +++ b/stdlib-source/adapters/rdap-domain.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type RdapDomainAdapter struct { diff --git a/stdlib-source/adapters/rdap-domain_test.go b/stdlib-source/adapters/rdap-domain_test.go index 27fe878e..7a95b4ec 100644 --- a/stdlib-source/adapters/rdap-domain_test.go +++ b/stdlib-source/adapters/rdap-domain_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestDomainAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-entity.go b/stdlib-source/adapters/rdap-entity.go index f8deb17c..a986230c 100644 --- a/stdlib-source/adapters/rdap-entity.go +++ b/stdlib-source/adapters/rdap-entity.go @@ -6,8 +6,8 @@ import ( "net/url" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type RdapEntityAdapter struct { diff --git a/stdlib-source/adapters/rdap-entity_test.go b/stdlib-source/adapters/rdap-entity_test.go index eb90b64b..a4f2825b 100644 --- a/stdlib-source/adapters/rdap-entity_test.go +++ b/stdlib-source/adapters/rdap-entity_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) func TestEntityAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-ip-network.go b/stdlib-source/adapters/rdap-ip-network.go index 94258234..d06bea06 100644 --- a/stdlib-source/adapters/rdap-ip-network.go +++ b/stdlib-source/adapters/rdap-ip-network.go @@ -6,8 +6,8 @@ import ( "net" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type RdapIPNetworkAdapter struct { diff --git a/stdlib-source/adapters/rdap-ip-network_test.go b/stdlib-source/adapters/rdap-ip-network_test.go index b45b91af..7edcff51 100644 --- a/stdlib-source/adapters/rdap-ip-network_test.go +++ b/stdlib-source/adapters/rdap-ip-network_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestIpNetworkAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-nameserver.go b/stdlib-source/adapters/rdap-nameserver.go index e5cf2b53..faa46d6a 100644 --- a/stdlib-source/adapters/rdap-nameserver.go +++ b/stdlib-source/adapters/rdap-nameserver.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdp-go" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" ) type RdapNameserverAdapter struct { diff --git a/stdlib-source/adapters/rdap-nameserver_test.go b/stdlib-source/adapters/rdap-nameserver_test.go index 46882a5d..0a57e28e 100644 --- a/stdlib-source/adapters/rdap-nameserver_test.go +++ b/stdlib-source/adapters/rdap-nameserver_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/cli/sdpcache" + "github.com/overmindtech/workspace/sdpcache" ) func TestNameserverAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/test/data.go b/stdlib-source/adapters/test/data.go index f286f7a1..ca1567df 100644 --- a/stdlib-source/adapters/test/data.go +++ b/stdlib-source/adapters/test/data.go @@ -5,7 +5,7 @@ import ( "sync/atomic" "time" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/stdlib-source/adapters/test/testdog.go b/stdlib-source/adapters/test/testdog.go index a18ea676..96776474 100644 --- a/stdlib-source/adapters/test/testdog.go +++ b/stdlib-source/adapters/test/testdog.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestDogAdapter An adapter of `dog` items for automated tests. diff --git a/stdlib-source/adapters/test/testfood.go b/stdlib-source/adapters/test/testfood.go index 8b8611e8..6b4ccd5c 100644 --- a/stdlib-source/adapters/test/testfood.go +++ b/stdlib-source/adapters/test/testfood.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestFoodAdapter A adapter of `food` items for automated tests. diff --git a/stdlib-source/adapters/test/testgroup.go b/stdlib-source/adapters/test/testgroup.go index 3678a380..6bd2d96f 100644 --- a/stdlib-source/adapters/test/testgroup.go +++ b/stdlib-source/adapters/test/testgroup.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestGroupAdapter A adapter of `group` items for automated tests. diff --git a/stdlib-source/adapters/test/testhobby.go b/stdlib-source/adapters/test/testhobby.go index 3b2c10d8..29037d90 100644 --- a/stdlib-source/adapters/test/testhobby.go +++ b/stdlib-source/adapters/test/testhobby.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestHobbyAdapter A adapter of `hobby` items for automated tests. diff --git a/stdlib-source/adapters/test/testlocation.go b/stdlib-source/adapters/test/testlocation.go index 33ae082e..91f644b0 100644 --- a/stdlib-source/adapters/test/testlocation.go +++ b/stdlib-source/adapters/test/testlocation.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestLocationAdapter A adapter of `location` items for automated tests. diff --git a/stdlib-source/adapters/test/testperson.go b/stdlib-source/adapters/test/testperson.go index 604591e7..4d25b960 100644 --- a/stdlib-source/adapters/test/testperson.go +++ b/stdlib-source/adapters/test/testperson.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestPersonAdapter A adapter of `person` items for automated tests. diff --git a/stdlib-source/adapters/test/testregion.go b/stdlib-source/adapters/test/testregion.go index bf4432e2..7ad32442 100644 --- a/stdlib-source/adapters/test/testregion.go +++ b/stdlib-source/adapters/test/testregion.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" ) // TestRegionAdapter A adapter of `region` items for automated tests. diff --git a/stdlib-source/build/package/Dockerfile b/stdlib-source/build/package/Dockerfile index 7bd7e09b..c7f6d7f5 100644 --- a/stdlib-source/build/package/Dockerfile +++ b/stdlib-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/stdlib-source/cmd/root.go b/stdlib-source/cmd/root.go index b65ecfe1..6c2e5582 100644 --- a/stdlib-source/cmd/root.go +++ b/stdlib-source/cmd/root.go @@ -9,10 +9,10 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/cli/discovery" - "github.com/overmindtech/cli/logging" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/logging" "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/cli/tracing" + "github.com/overmindtech/workspace/tracing" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/tfutils/plan_mapper.go b/tfutils/plan_mapper.go index f0b2311c..da444e53 100644 --- a/tfutils/plan_mapper.go +++ b/tfutils/plan_mapper.go @@ -13,7 +13,7 @@ import ( "github.com/google/uuid" awsAdapters "github.com/overmindtech/cli/aws-source/adapters" k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" diff --git a/tfutils/plan_mapper_test.go b/tfutils/plan_mapper_test.go index 2e93aa20..2d242d7f 100644 --- a/tfutils/plan_mapper_test.go +++ b/tfutils/plan_mapper_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/overmindtech/cli/sdp-go" + "github.com/overmindtech/workspace/sdp-go" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/xiam/dig" diff --git a/tracing/deferlog.go b/tracing/deferlog.go deleted file mode 100644 index b2fb04c7..00000000 --- a/tracing/deferlog.go +++ /dev/null @@ -1,77 +0,0 @@ -package tracing - -import ( - "context" - "fmt" - "os" - "runtime/debug" - - "github.com/getsentry/sentry-go" - log "github.com/sirupsen/logrus" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// LogRecoverToReturn Recovers from a panic, logs and forwards it sentry and -// otel, then returns. Does nothing when there is no panic. -func LogRecoverToReturn(ctx context.Context, loc string) { - err := recover() - if err == nil { - return - } - - stack := string(debug.Stack()) - HandleError(ctx, loc, err, stack) -} - -// LogRecoverToError Recovers from a panic, logs and forwards it sentry and -// otel, then returns a new error describing the panic. Does nothing when there -// is no panic. -func LogRecoverToError(ctx context.Context, loc string) error { - err := recover() - if err == nil { - return nil - } - - stack := string(debug.Stack()) - HandleError(ctx, loc, err, stack) - - return fmt.Errorf("panic recovered: %v", err) -} - -// LogRecoverToExit Recovers from a panic, logs and forwards it sentry and otel, -// then exits the entire process. Does nothing when there is no panic. -func LogRecoverToExit(ctx context.Context, loc string) { - err := recover() - if err == nil { - return - } - - stack := string(debug.Stack()) - HandleError(ctx, loc, err, stack) - - // ensure that errors still get sent out - ShutdownTracer(ctx) - - os.Exit(1) -} - -func HandleError(ctx context.Context, loc string, err interface{}, stack string) { - msg := fmt.Sprintf("unhandled panic in %v, exiting: %v", loc, err) - - hub := sentry.CurrentHub() - if hub != nil { - hub.Recover(err) - } - - // always log to stderr (no WithContext!) - log.WithFields(log.Fields{"loc": loc, "stack": stack}).Error(msg) - - // if we have a context, try attaching additional info to the span - if ctx != nil { - log.WithContext(ctx).WithFields(log.Fields{"loc": loc, "stack": stack}).Error(msg) - span := trace.SpanFromContext(ctx) - span.SetAttributes(attribute.String("ovm.panic.loc", loc)) - span.SetAttributes(attribute.String("ovm.panic.stack", stack)) - } -} diff --git a/tracing/header_carrier.go b/tracing/header_carrier.go deleted file mode 100644 index de2b9fd6..00000000 --- a/tracing/header_carrier.go +++ /dev/null @@ -1,31 +0,0 @@ -package tracing - -import "github.com/nats-io/nats.go" - -// HeaderCarrier is a custom wrapper on top of nats.Headers for otel's TextMapCarrier. -type HeaderCarrier struct { - headers nats.Header -} - -// NewNatsHeaderCarrier creates a new HeaderCarrier. -func NewNatsHeaderCarrier(h nats.Header) *HeaderCarrier { - return &HeaderCarrier{ - headers: h, - } -} - -func (c *HeaderCarrier) Get(key string) string { - return c.headers.Get(key) -} - -func (c *HeaderCarrier) Set(key, value string) { - c.headers.Set(key, value) -} - -func (c *HeaderCarrier) Keys() []string { - keys := make([]string, 0, len(c.headers)) - for key := range c.headers { - keys = append(keys, key) - } - return keys -} diff --git a/tracing/main.go b/tracing/main.go deleted file mode 100644 index b379c972..00000000 --- a/tracing/main.go +++ /dev/null @@ -1,383 +0,0 @@ -package tracing - -import ( - "context" - "fmt" - "net/http" - "os" - "path/filepath" - "slices" - "time" - - _ "embed" - - "github.com/MrAlias/otel-schema-utils/schema" - "github.com/getsentry/sentry-go" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "go.opentelemetry.io/contrib/detectors/aws/ec2/v2" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" - "go.opentelemetry.io/otel/trace" -) - -const instrumentationName = "github.com/overmindtech/workspace" - -// the following vars will be set during the build using `ldflags`, eg: -// -// go build -ldflags "-X github.com/overmindtech/cli/tracing.version=$VERSION" -o your-app -// -// This allows caching to work for dev and removes the last `go generate` -// requirement from the build. If we were embedding the version here each time -// we would always produce a slightly different compiled binary, and therefore -// it would look like there was a change each time -var ( - version = "dev" - commit = "none" -) - -var ( - tracer = otel.GetTracerProvider().Tracer( - instrumentationName, - trace.WithInstrumentationVersion(version), - trace.WithInstrumentationAttributes( - attribute.String("build.commit", commit), - ), - trace.WithSchemaURL(semconv.SchemaURL), - ) -) - -func Tracer() trace.Tracer { - return tracer -} - -// hasGitDir returns true if the current directory or any parent directory contains a .git directory -func hasGitDir() bool { - // Start with the current working directory - dir, err := os.Getwd() - if err != nil { - return false - } - - // Check the current directory and all parent directories - for { - // Check if .git exists in this directory - _, err := os.Stat(filepath.Join(dir, ".git")) - if err == nil { - return true // Found a .git directory - } - - // Get the parent directory - parentDir := filepath.Dir(dir) - - // If we've reached the root directory, stop searching - if parentDir == dir { - break - } - - // Move up to the parent directory - dir = parentDir - } - - return false // No .git directory found -} - -func tracingResource(component string) *resource.Resource { - // Identify your application using resource detection - resources := []*resource.Resource{} - - // the EC2 detector takes ~10s to time out outside EC2 - // disable it if we're running from a git checkout - if !hasGitDir() { - ec2Res, err := resource.New(context.Background(), resource.WithDetectors(ec2.NewResourceDetector())) - if err != nil { - log.WithError(err).Error("error initialising EC2 resource detector") - return nil - } - resources = append(resources, ec2Res) - } - - // Needs https://github.com/open-telemetry/opentelemetry-go-contrib/issues/1856 fixed first - // // the EKS detector is temperamental and doesn't like running outside of kube - // // hence we need to keep it from running when we know there's no kube - // if !viper.GetBool("disable-kube") { - // // Use the AWS resource detector to detect information about the runtime environment - // detectors = append(detectors, eks.NewResourceDetector()) - // } - - hostRes, err := resource.New(context.Background(), - resource.WithHost(), - resource.WithOS(), - resource.WithProcess(), - resource.WithContainer(), - resource.WithTelemetrySDK(), - ) - if err != nil { - log.WithError(err).Error("error initialising host resource") - return nil - } - resources = append(resources, hostRes) - - localRes, err := resource.New(context.Background(), - resource.WithSchemaURL(semconv.SchemaURL), - // Add your own custom attributes to identify your application - resource.WithAttributes( - semconv.ServiceNameKey.String(component), - semconv.ServiceVersionKey.String(version), - attribute.String("build.commit", commit), - ), - ) - if err != nil { - log.WithError(err).Error("error initialising local resource") - return nil - } - resources = append(resources, localRes) - - conv := schema.NewConverter(schema.DefaultClient) - res, err := conv.MergeResources(context.Background(), semconv.SchemaURL, resources...) - - if err != nil { - log.WithError(err).Error("error merging resource") - return nil - } - return res -} - -var tp *sdktrace.TracerProvider -var healthTp *sdktrace.TracerProvider - -// HealthCheckTracerProvider returns the tracer provider used for health checks. This has a built-in 1:100 sampler for health checks that are not captured by the default UserAgentSampler for ELB and kube-probe requests. -func HealthCheckTracerProvider() *sdktrace.TracerProvider { - if healthTp == nil { - panic("tracer providers not initialised") - } - return healthTp -} - -// healthCheckTracer is the tracer used for health checks. This is heavily sampled to avoid getting spammed by k8s or ELBs -func HealthCheckTracer() trace.Tracer { - return HealthCheckTracerProvider().Tracer( - instrumentationName, - trace.WithInstrumentationVersion(version), - trace.WithSchemaURL(semconv.SchemaURL), - trace.WithInstrumentationAttributes( - attribute.Bool("ovm.healthCheck", true), - ), - ) -} - -// InitTracerWithUpstreams initialises the tracer with uploading directly to Honeycomb and sentry if `honeycombApiKey` and `sentryDSN` is set respectively. `component` is used as the service name. -func InitTracerWithUpstreams(component, honeycombApiKey, sentryDSN string, opts ...otlptracehttp.Option) error { - if sentryDSN != "" { - var environment string - switch viper.GetString("run-mode") { - case "release": - environment = "prod" - case "test": - environment = "dogfood" - case "debug": - environment = "local" - default: - // Fallback to dev for backward compatibility - environment = "dev" - } - err := sentry.Init(sentry.ClientOptions{ - Dsn: sentryDSN, - AttachStacktrace: true, - EnableTracing: false, - Environment: environment, - // Set TracesSampleRate to 1.0 to capture 100% - // of transactions for performance monitoring. - // We recommend adjusting this value in production, - TracesSampleRate: 1.0, - }) - if err != nil { - log.Errorf("sentry.Init: %s", err) - } - // setup recovery for an unexpected panic in this function - defer sentry.Flush(2 * time.Second) - defer sentry.Recover() - log.Trace("sentry configured") - } - - if honeycombApiKey != "" { - opts = append(opts, - otlptracehttp.WithEndpoint("api.honeycomb.io"), - otlptracehttp.WithHeaders(map[string]string{"x-honeycomb-team": honeycombApiKey}), - ) - } else { - // If no Honeycomb API key is provided, use the hardcoded OTLP collector - // endpoint, which is provided by the otel-collector service in the otel - // namespace. Since this a node-local service, it does not use TLS. - opts = append(opts, - otlptracehttp.WithEndpoint("otelcol-node-opentelemetry-collector.otel.svc.cluster.local:4318"), - otlptracehttp.WithInsecure(), - ) - } - - return InitTracer(component, opts...) -} - -func InitTracer(component string, opts ...otlptracehttp.Option) error { - client := otlptracehttp.NewClient(opts...) - otlpExp, err := otlptrace.New(context.Background(), client) - if err != nil { - return fmt.Errorf("creating OTLP trace exporter: %w", err) - } - - // Create unified sampler for health checks and otelpgx spans - overmindSampler := NewOvermindSampler() - - tracerOpts := []sdktrace.TracerProviderOption{ - sdktrace.WithBatcher(otlpExp), - sdktrace.WithResource(tracingResource(component)), - sdktrace.WithSampler(sdktrace.ParentBased(overmindSampler)), - } - if viper.GetBool("stdout-trace-dump") { - stdoutExp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) - if err != nil { - return err - } - tracerOpts = append(tracerOpts, sdktrace.WithBatcher(stdoutExp)) - } - tp = sdktrace.NewTracerProvider(tracerOpts...) - - // Set the default tracer provider for all the libraries - otel.SetTracerProvider(tp) - - tracerOpts = append(tracerOpts, sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased((0.1))))) - healthTp = sdktrace.NewTracerProvider(tracerOpts...) - - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - return nil -} - -func ShutdownTracer(ctx context.Context) { - // Flush buffered events before the program terminates. - defer sentry.Flush(5 * time.Second) - - // detach from the parent's cancellation, and ensure that we do not wait - // indefinitely on the trace provider shutdown - ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) - defer cancel() - if tp != nil { - if err := tp.ForceFlush(ctx); err != nil { - log.WithContext(ctx).WithError(err).Error("Error flushing tracer provider") - } - if err := tp.Shutdown(ctx); err != nil { - log.WithContext(ctx).WithError(err).Error("Error shutting down tracer provider") - } - } - log.WithContext(ctx).Trace("tracing has shut down") -} - -// SamplingRule defines a single sampling rule with a rate and matching function -type SamplingRule struct { - SampleRate int - ShouldSample func(sdktrace.SamplingParameters) bool -} - -// OvermindSampler is a unified sampler that evaluates multiple sampling rules in order -type OvermindSampler struct { - rules []SamplingRule - ruleSamplers []sdktrace.Sampler -} - -// NewOvermindSampler creates a new unified sampler with the default rules -func NewOvermindSampler() *OvermindSampler { - rules := []SamplingRule{ - { - SampleRate: 200, - ShouldSample: UserAgentMatcher("ELB-HealthChecker/2.0", "kube-probe/1.27+"), - }, - { - SampleRate: 10, - ShouldSample: SpanNameMatcher("pool.acquire"), - }, - } - - // Pre-allocate samplers for each rule - ruleSamplers := make([]sdktrace.Sampler, 0, len(rules)) - for _, rule := range rules { - var sampler sdktrace.Sampler - switch { - case rule.SampleRate <= 0: - sampler = sdktrace.NeverSample() - case rule.SampleRate == 1: - sampler = sdktrace.AlwaysSample() - default: - sampler = sdktrace.TraceIDRatioBased(1.0 / float64(rule.SampleRate)) - } - ruleSamplers = append(ruleSamplers, sampler) - } - - return &OvermindSampler{ - rules: rules, - ruleSamplers: ruleSamplers, - } -} - -// UserAgentMatcher returns a function that matches specific user agents -func UserAgentMatcher(userAgents ...string) func(sdktrace.SamplingParameters) bool { - return func(parameters sdktrace.SamplingParameters) bool { - for _, attr := range parameters.Attributes { - if (attr.Key == "http.user_agent" || attr.Key == "user_agent.original") && - slices.Contains(userAgents, attr.Value.AsString()) { - return true - } - } - return false - } -} - -// SpanNameMatcher returns a function that matches specific span names -func SpanNameMatcher(spanNames ...string) func(sdktrace.SamplingParameters) bool { - return func(parameters sdktrace.SamplingParameters) bool { - return slices.Contains(spanNames, parameters.Name) - } -} - -// ShouldSample evaluates rules in order and returns the first matching decision -func (o *OvermindSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { - for i, rule := range o.rules { - if rule.ShouldSample(parameters) { - // Use the pre-allocated sampler for this rule - result := o.ruleSamplers[i].ShouldSample(parameters) - if result.Decision == sdktrace.RecordAndSample { - result.Attributes = append(result.Attributes, - attribute.Int("SampleRate", rule.SampleRate)) - } - return result - } - } - - // Default to AlwaysSample if no rules match - return sdktrace.AlwaysSample().ShouldSample(parameters) -} - -// Description returns information describing the Sampler -func (o *OvermindSampler) Description() string { - return "Unified Overmind sampler combining multiple sampling strategies" -} - -// Version returns the version baked into the binary at build time. -func Version() string { - return version -} - -// HTTPClient returns an HTTP client with OpenTelemetry instrumentation. -// This replaces the deprecated otelhttp.DefaultClient and should be used -// throughout the codebase for HTTP requests that need tracing. -func HTTPClient() *http.Client { - return &http.Client{ - Transport: otelhttp.NewTransport(http.DefaultTransport), - } -} diff --git a/tracing/main_test.go b/tracing/main_test.go deleted file mode 100644 index 26bf36cf..00000000 --- a/tracing/main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package tracing - -import ( - "testing" -) - -func TestTracingResource(t *testing.T) { - resource := tracingResource("test-component") - if resource == nil { - t.Error("Could not initialize tracing resource. Check the log!") - } -} diff --git a/tracing/memory.go b/tracing/memory.go deleted file mode 100644 index 22123307..00000000 --- a/tracing/memory.go +++ /dev/null @@ -1,69 +0,0 @@ -package tracing - -import ( - "runtime" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// safeUint64ToInt64 safely converts uint64 to int64 for OpenTelemetry attributes -// Returns int64 max value if the uint64 exceeds int64 maximum to prevent overflow -func safeUint64ToInt64(val uint64) int64 { - const maxInt64 = 9223372036854775807 // 2^63 - 1 - if val > maxInt64 { // Check if val exceeds int64 max - return maxInt64 // Return int64 max value - } - return int64(val) -} - -// MemoryStats represents memory statistics at a point in time, converted to int64 for safe use -type MemoryStats struct { - Alloc int64 // bytes allocated and not yet freed - HeapAlloc int64 // bytes allocated and not yet freed (same as Alloc above but specifically for heap objects) - Sys int64 // total bytes of memory obtained from the OS - NumGC int64 // number of completed GC cycles - PauseTotal int64 // cumulative nanoseconds in GC stop-the-world pauses -} - -// ReadMemoryStats captures current memory statistics and converts them to int64 -func ReadMemoryStats() MemoryStats { - var memStats runtime.MemStats - runtime.ReadMemStats(&memStats) - return MemoryStats{ - Alloc: safeUint64ToInt64(memStats.Alloc), - HeapAlloc: safeUint64ToInt64(memStats.HeapAlloc), - Sys: safeUint64ToInt64(memStats.Sys), - NumGC: int64(memStats.NumGC), - PauseTotal: safeUint64ToInt64(memStats.PauseTotalNs), - } -} - -// SetMemoryAttributes sets memory-related attributes on a span with the given prefix -func SetMemoryAttributes(span trace.Span, prefix string, memStats MemoryStats) { - span.SetAttributes( - attribute.Int64(prefix+".memoryBytes", memStats.Alloc), - attribute.Int64(prefix+".memoryHeapBytes", memStats.HeapAlloc), - attribute.Int64(prefix+".memorySysBytes", memStats.Sys), - attribute.Int64(prefix+".memoryNumGC", memStats.NumGC), - attribute.Int64(prefix+".memoryPauseTotalNs", memStats.PauseTotal), - ) -} - -// SetMemoryDeltaAttributes sets memory delta attributes on a span with the given prefix -// It calculates the difference between before and after memory stats -func SetMemoryDeltaAttributes(span trace.Span, prefix string, before, after MemoryStats) { - deltaAlloc := after.Alloc - before.Alloc - deltaHeapAlloc := after.HeapAlloc - before.HeapAlloc - deltaSys := after.Sys - before.Sys - deltaNumGC := after.NumGC - before.NumGC - deltaPauseTotal := after.PauseTotal - before.PauseTotal - - span.SetAttributes( - attribute.Int64(prefix+".memoryDeltaBytes", deltaAlloc), - attribute.Int64(prefix+".memoryDeltaHeapBytes", deltaHeapAlloc), - attribute.Int64(prefix+".memoryDeltaSysBytes", deltaSys), - attribute.Int64(prefix+".memoryDeltaNumGC", deltaNumGC), - attribute.Int64(prefix+".memoryDeltaPauseTotalNs", deltaPauseTotal), - ) -} diff --git a/tracing/memory_test.go b/tracing/memory_test.go deleted file mode 100644 index 4624548d..00000000 --- a/tracing/memory_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package tracing - -import ( - "testing" -) - -func TestSafeUint64ToInt64(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input uint64 - expected int64 - }{ - { - name: "small value", - input: 1000, - expected: 1000, - }, - { - name: "int64 max value", - input: 9223372036854775807, // 2^63 - 1 - expected: 9223372036854775807, - }, - { - name: "int64 max + 1", - input: 9223372036854775808, // 2^63 - expected: 9223372036854775807, // Should be clamped to int64 max - }, - { - name: "very large value", - input: 18446744073709551615, // uint64 max - expected: 9223372036854775807, // Should be clamped to int64 max - }, - } - - for _, tt := range tests { - test := tt // capture loop variable - t.Run(test.name, func(t *testing.T) { - t.Parallel() - result := safeUint64ToInt64(test.input) - if result != test.expected { - t.Errorf("safeUint64ToInt64(%d) = %d, expected %d", test.input, result, test.expected) - } - }) - } -} - -func TestReadMemoryStats(t *testing.T) { - t.Parallel() - - stats := ReadMemoryStats() - - // Basic sanity checks - these values should be reasonable - if stats.Alloc <= 0 { - t.Errorf("Alloc should be greater than 0, got %d", stats.Alloc) - } - if stats.HeapAlloc <= 0 { - t.Errorf("HeapAlloc should be greater than 0, got %d", stats.HeapAlloc) - } - if stats.Sys <= 0 { - t.Errorf("Sys should be greater than 0, got %d", stats.Sys) - } - - // Verify that values are within int64 range (they should be since we convert them) - if stats.Alloc < 0 { - t.Errorf("Alloc should not be negative, got %d", stats.Alloc) - } - if stats.HeapAlloc < 0 { - t.Errorf("HeapAlloc should not be negative, got %d", stats.HeapAlloc) - } - if stats.Sys < 0 { - t.Errorf("Sys should not be negative, got %d", stats.Sys) - } -} From 31a51886aa503bb2982cc291e479073a9988eb9c Mon Sep 17 00:00:00 2001 From: TP Honey Date: Tue, 10 Feb 2026 11:06:42 +0000 Subject: [PATCH 02/51] fix, remove unneeded \healthz startup probe from sources (#3832) > [!NOTE] > **Medium Risk** > Medium risk because it removes the legacy `/healthz` endpoint; any deployments or monitors still hitting `/healthz` will start failing until updated, potentially impacting Kubernetes probe behavior and rollout health. > > **Overview** > **Removes the legacy `/healthz` endpoint** from the discovery engine health probe server, leaving only `/healthz/alive` (liveness) and `/healthz/ready` (readiness), and updates the startup log messaging accordingly. > > Updates source-facing docs and CLI help text (AWS/stdlib READMEs, Azure docs, `k8s-source` flag description, `srcman` README) to reference the new probe URLs/semantics and document HTTP `503` on unhealthy responses; also drops backward-compatibility mentions from `srcman` probe constants/tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 93f8eb8c5be85a9b6ceff45878abbb90f850b86e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 3d59df20dc735a39b58ca9b493932af745493abe --- k8s-source/cmd/root.go | 2 +- sources/azure/docs/testing-federated-auth.md | 10 +++++----- sources/azure/docs/usage.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/k8s-source/cmd/root.go b/k8s-source/cmd/root.go index f64a5306..1f4dcbf9 100644 --- a/k8s-source/cmd/root.go +++ b/k8s-source/cmd/root.go @@ -383,7 +383,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "/etc/srcman/config/source.yaml", "config file path") rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace") - rootCmd.PersistentFlags().Int("health-check-port", 8080, "The port on which to serve health check endpoints (/healthz/alive, /healthz/ready, /healthz)") + rootCmd.PersistentFlags().Int("health-check-port", 8080, "The port on which to serve health check endpoints (/healthz/alive, /healthz/ready)") // engine flags discovery.AddEngineFlags(rootCmd) diff --git a/sources/azure/docs/testing-federated-auth.md b/sources/azure/docs/testing-federated-auth.md index f19470c4..e1376808 100644 --- a/sources/azure/docs/testing-federated-auth.md +++ b/sources/azure/docs/testing-federated-auth.md @@ -102,7 +102,7 @@ INFO Sources initialized ```bash # Check health endpoint -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Expected: "ok" # Check logs for authentication method @@ -187,7 +187,7 @@ INFO Successfully verified subscription access # Should use environment variables, not Azure CLI # Verify it still works after Azure CLI logout -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive ``` ### Cleanup @@ -388,7 +388,7 @@ kubectl logs -l app=azure-source --tail=50 # Test health endpoint kubectl port-forward deployment/azure-source 8080:8080 & -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive ``` ### Troubleshooting @@ -580,7 +580,7 @@ kubectl logs -l app=azure-source --tail=50 # Check health kubectl port-forward deployment/azure-source 8080:8080 & -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Verify GCP token is available kubectl exec -it deployment/azure-source -- env | grep GOOGLE @@ -755,7 +755,7 @@ After completing any test scenario, perform these verification steps: kubectl port-forward deployment/azure-source 8080:8080 & # Check health -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Expected: "ok" ``` diff --git a/sources/azure/docs/usage.md b/sources/azure/docs/usage.md index 2afcafdb..796b90ed 100644 --- a/sources/azure/docs/usage.md +++ b/sources/azure/docs/usage.md @@ -331,10 +331,10 @@ The source exposes a health check endpoint: ```bash # Check health -curl http://localhost:8080/healthz +curl http://localhost:8080/healthz/alive # Response: "ok" (HTTP 200) if healthy -# Response: Error message (HTTP 500) if unhealthy +# Response: Error message (HTTP 503 Service Unavailable) if unhealthy ``` The health check verifies: From d79c99a048af953b30dc4896f1b2e461a86879c8 Mon Sep 17 00:00:00 2001 From: carabasdaniel Date: Tue, 10 Feb 2026 15:08:06 +0200 Subject: [PATCH 03/51] Stdlib DNS adapter timeout (#3828) Add a 30-second maximum timeout to the stdlib DNS adapter to prevent performance degradation from slow DNS lookups, especially during revlink warmup. --- Linear Issue: [ENG-2400](https://linear.app/overmind/issue/ENG-2400/add-a-max-timeout-to-stdlib-dns-adapter)

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Touches core DNS lookup paths and adds context-deadline enforcement, which could change failure/latency characteristics for callers; new tests include timing-based behavior that may be flaky across network/CI environments. > > **Overview** > Adds a **hard maximum timeout** (`maxOperationTimeout`, 30s) to `stdlib-source` DNS adapter `Get` and `Search` by wrapping the incoming context, preventing slow DNS lookups from stalling callers. > > Expands `dns_test.go` with new coverage for timeout precedence (adapter cap vs long caller deadline, and short caller deadline still winning), plus small behavioral assertions for `Search` and `List`. Updates the `stdlib-source` CI job `go test` timeout from 30s to 1m to accommodate the new timeout-focused tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6d79988df820275555650fdbe81a1b17680c3763. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor Agent Co-authored-by: carabasdaniel GitOrigin-RevId: 5cb33abc09e16b0c8ba8c7511ff15f01e80fc40a --- stdlib-source/adapters/dns.go | 19 ++++- stdlib-source/adapters/dns_test.go | 121 +++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index 6b464398..b3463ee4 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -41,6 +41,10 @@ func NewDNSAdapterForHealthCheck() *DNSAdapter { const dnsCacheDuration = 5 * time.Minute +// maxOperationTimeout is the maximum time any single DNS Get/Search operation can take. +// This prevents slow DNS queries from degrading overall system performance. +const maxOperationTimeout = 30 * time.Second + var DefaultServers = []string{ "169.254.169.253:53", // Route 53 default resolver. See https://docs.aws.amazon.com/vpc/latest/userguide/AmazonDNS-concepts.html#AmazonDNS "1.1.1.1:53", @@ -102,8 +106,13 @@ func (d *DNSAdapter) Scopes() []string { } } -// Gets a single item. This expects a DNS name +// Get retrieves a single DNS item by name. +// The operation is capped at maxOperationTimeout (30s) regardless of the caller's context deadline. func (d *DNSAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + // Enforce maximum timeout for this operation + ctx, cancel := context.WithTimeout(ctx, maxOperationTimeout) + defer cancel() + if scope != "global" { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, @@ -186,12 +195,20 @@ type DNSRecord struct { Type string } +// Search performs a DNS lookup for a name or reverse lookup for an IP. +// The operation is capped at maxOperationTimeout (30s) regardless of the caller's context deadline. func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { + // Enforce maximum timeout for this operation + ctx, cancel := context.WithTimeout(ctx, maxOperationTimeout) + defer cancel() + if scope != "global" { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, ErrorString: "DNS queries only supported in global scope", Scope: scope, + SourceName: d.Name(), + ItemType: d.Type(), } } diff --git a/stdlib-source/adapters/dns_test.go b/stdlib-source/adapters/dns_test.go index 5fc5c9b1..802d1d54 100644 --- a/stdlib-source/adapters/dns_test.go +++ b/stdlib-source/adapters/dns_test.go @@ -5,6 +5,7 @@ import ( "errors" "net" "testing" + "time" "github.com/overmindtech/workspace/discovery" "github.com/overmindtech/workspace/sdp-go" @@ -171,3 +172,123 @@ func TestDnsGet(t *testing.T) { t.Log(item) }) } + +// TestGetTimeout verifies that Get enforces the maximum timeout by checking +// that the adapter's timeout takes precedence over a longer caller timeout +func TestGetTimeout(t *testing.T) { + if testing.Short() { + t.Skip("Skipping timeout test in short mode") + } + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + // Use a non-existent DNS server to force timeout + Servers: []string{"192.0.2.1:53"}, // TEST-NET-1, guaranteed to be unroutable + } + + // Create a context with a very long deadline to verify adapter's internal timeout takes precedence + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + start := time.Now() + _, err := src.Get(ctx, "global", "test.example.com", false) + elapsed := time.Since(start) + + // The operation should fail (no response from DNS server) + if err == nil { + t.Error("expected error but got nil") + } + + // The operation should complete around the maxOperationTimeout (30s), not the caller's 10 minutes + // Allow generous buffer for CI variance and different network behaviors + if elapsed > 35*time.Second { + t.Errorf("Get took %v, expected around 30s (max 35s for variance), timeout may not be properly enforced", elapsed) + } + + // Don't assert minimum duration as TEST-NET may fail fast in some environments + // The key assertion is that it completes in ~30s, not 10 minutes +} + +// TestSearchTimeoutContext verifies that Search properly wraps the context with a timeout +func TestSearchTimeoutContext(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + } + + // Create a context with a very long deadline to ensure Search creates its own timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Use a valid, fast DNS query to verify the timeout wrapper doesn't break normal operation + items, err := src.Search(ctx, "global", "one.one.one.one", false) + + // Should succeed with the fast query + if err != nil { + t.Errorf("expected no error for valid query, got: %v", err) + } + + // Should return at least one item for this known DNS name + if len(items) == 0 { + t.Error("expected at least one DNS item for one.one.one.one") + } +} + +// TestListBehavior verifies that List returns an empty slice without making DNS queries +func TestListBehavior(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + } + + ctx := context.Background() + + // List should return an empty slice without making any DNS queries + items, err := src.List(ctx, "global", false) + + // List should succeed with empty results + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + + if len(items) != 0 { + t.Errorf("expected empty list, got %d items", len(items)) + } +} + +// TestTimeoutShorterThanCaller verifies that a short caller timeout is respected +func TestTimeoutShorterThanCaller(t *testing.T) { + t.Parallel() + + src := DNSAdapter{ + cache: sdpcache.NewNoOpCache(), + // Use a non-existent DNS server to force timeout + Servers: []string{"192.0.2.1:53"}, // TEST-NET-1, guaranteed to be unroutable + } + + // Create a context with a 2s deadline (shorter than the adapter's 30s max) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + start := time.Now() + _, err := src.Get(ctx, "global", "test.example.com", false) + elapsed := time.Since(start) + + // The operation should fail (no response from DNS server) + if err == nil { + t.Error("expected error but got nil") + } + + // The operation should complete in roughly 2 seconds (the caller's timeout), not 30s + // Allow some buffer for processing time (4s max) + if elapsed > 4*time.Second { + t.Errorf("Get took %v, expected around 2s (max 4s)", elapsed) + } + + // Verify it's a context deadline exceeded error + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expected context.DeadlineExceeded error, got: %v", err) + } +} From 84181e021932eba9eefa3675c0d0eef3d70781f8 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:55:39 +0000 Subject: [PATCH 04/51] create dedicated host groups adapter (#3844) Passing Tests: image image --- > [!NOTE] > **Medium Risk** > Adds a new Azure resource discovery path and registers it in adapter initialization, which could affect runtime discovery performance/behavior and linked-item graph output; changes are additive and covered by unit/integration tests. > > **Overview** > Adds a new Azure compute adapter for **Dedicated Host Groups** that supports `Get`, `List`, and `ListStream`, and emits linked-item queries to `ComputeDedicatedHost` when host references are present. > > Wires the adapter into `manual/adapters.go` by initializing an `armcompute.DedicatedHostGroupsClient` and registering the wrapper for both real and metadata-only adapter initialization. > > Introduces a small `DedicatedHostGroupsClient` interface (plus generated gomock) to wrap the Azure SDK client for testability, adds a new `ComputeDedicatedHost` item/resource type constant, and includes comprehensive unit tests plus an Azure integration test that provisions a host group and validates `Get`/`List` behavior and item/link correctness. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 647f5c3e3d0ab4d90febee294cf29f7715a8b70a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: bbbb65a63c6a4e5c038f6a941226494ca1be9248 --- .../clients/dedicated-host-groups-client.go | 36 ++ .../compute-dedicated-host-group_test.go | 296 +++++++++++++ sources/azure/manual/adapters.go | 10 + .../manual/compute-dedicated-host-group.go | 185 ++++++++ .../compute-dedicated-host-group_test.go | 413 ++++++++++++++++++ sources/azure/shared/item-types.go | 1 + .../mock_dedicated_host_groups_client.go | 72 +++ sources/azure/shared/models.go | 1 + 8 files changed, 1014 insertions(+) create mode 100644 sources/azure/clients/dedicated-host-groups-client.go create mode 100644 sources/azure/integration-tests/compute-dedicated-host-group_test.go create mode 100644 sources/azure/manual/compute-dedicated-host-group.go create mode 100644 sources/azure/manual/compute-dedicated-host-group_test.go create mode 100644 sources/azure/shared/mocks/mock_dedicated_host_groups_client.go diff --git a/sources/azure/clients/dedicated-host-groups-client.go b/sources/azure/clients/dedicated-host-groups-client.go new file mode 100644 index 00000000..88b4c46a --- /dev/null +++ b/sources/azure/clients/dedicated-host-groups-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_dedicated_host_groups_client.go -package=mocks -source=dedicated-host-groups-client.go + +// DedicatedHostGroupsPager is a type alias for the generic Pager interface with dedicated host group response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type DedicatedHostGroupsPager = Pager[armcompute.DedicatedHostGroupsClientListByResourceGroupResponse] + +// DedicatedHostGroupsClient is an interface for interacting with Azure dedicated host groups +type DedicatedHostGroupsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) DedicatedHostGroupsPager + Get(ctx context.Context, resourceGroupName string, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) +} + +type dedicatedHostGroupsClient struct { + client *armcompute.DedicatedHostGroupsClient +} + +func (a *dedicatedHostGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) DedicatedHostGroupsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *dedicatedHostGroupsClient) Get(ctx context.Context, resourceGroupName string, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, dedicatedHostGroupName, options) +} + +// NewDedicatedHostGroupsClient creates a new DedicatedHostGroupsClient from the Azure SDK client +func NewDedicatedHostGroupsClient(client *armcompute.DedicatedHostGroupsClient) DedicatedHostGroupsClient { + return &dedicatedHostGroupsClient{client: client} +} diff --git a/sources/azure/integration-tests/compute-dedicated-host-group_test.go b/sources/azure/integration-tests/compute-dedicated-host-group_test.go new file mode 100644 index 00000000..eb012459 --- /dev/null +++ b/sources/azure/integration-tests/compute-dedicated-host-group_test.go @@ -0,0 +1,296 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestDedicatedHostGroupName = "ovm-integ-test-dedicated-host-group" +) + +func TestComputeDedicatedHostGroupIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + dedicatedHostGroupsClient, err := armcompute.NewDedicatedHostGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Dedicated Host Groups client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createDedicatedHostGroup(ctx, dedicatedHostGroupsClient, integrationTestResourceGroup, integrationTestDedicatedHostGroupName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create dedicated host group: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetDedicatedHostGroup", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving dedicated host group %s in subscription %s, resource group %s", + integrationTestDedicatedHostGroupName, subscriptionID, integrationTestResourceGroup) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestDedicatedHostGroupName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestDedicatedHostGroupName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved dedicated host group %s", integrationTestDedicatedHostGroupName) + }) + + t.Run("ListDedicatedHostGroups", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing dedicated host groups in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + + listable, ok := dedicatedHostGroupAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list dedicated host groups: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one dedicated host group, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestDedicatedHostGroupName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find dedicated host group %s in the list of dedicated host groups", integrationTestDedicatedHostGroupName) + } + + log.Printf("Found %d dedicated host groups in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for dedicated host group %s", integrationTestDedicatedHostGroupName) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDedicatedHostGroup.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeDedicatedHostGroup.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for dedicated host group %s", integrationTestDedicatedHostGroupName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for dedicated host group %s", integrationTestDedicatedHostGroupName) + + dedicatedHostGroupWrapper := manual.NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := dedicatedHostGroupWrapper.Scopes()[0] + + dedicatedHostGroupAdapter := sources.WrapperToAdapter(dedicatedHostGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := dedicatedHostGroupAdapter.Get(ctx, scope, integrationTestDedicatedHostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for dedicated host group %s", len(linkedQueries), integrationTestDedicatedHostGroupName) + + // Dedicated host group may have zero or more linked queries (ComputeDedicatedHost) depending on whether hosts exist + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + bp := liq.GetBlastPropagation() + if bp == nil { + t.Error("Linked item query has nil BlastPropagation") + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) + } else { + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), + bp.GetIn(), bp.GetOut()) + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteDedicatedHostGroup(ctx, dedicatedHostGroupsClient, integrationTestResourceGroup, integrationTestDedicatedHostGroupName) + if err != nil { + t.Fatalf("Failed to delete dedicated host group: %v", err) + } + }) +} + +// createDedicatedHostGroup creates an Azure dedicated host group resource (idempotent). +func createDedicatedHostGroup(ctx context.Context, client *armcompute.DedicatedHostGroupsClient, resourceGroupName, hostGroupName, location string) error { + _, err := client.Get(ctx, resourceGroupName, hostGroupName, nil) + if err == nil { + log.Printf("Dedicated host group %s already exists, skipping creation", hostGroupName) + return nil + } + + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("unexpected error checking dedicated host group: %w", err) + } + + _, err = client.CreateOrUpdate(ctx, resourceGroupName, hostGroupName, armcompute.DedicatedHostGroup{ + Location: ptr.To(location), + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: ptr.To[int32](1), + }, + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-dedicated-host-group"), + }, + }, nil) + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Dedicated host group %s already exists (conflict), skipping creation", hostGroupName) + return nil + } + return fmt.Errorf("failed to create dedicated host group: %w", err) + } + + log.Printf("Dedicated host group %s created successfully", hostGroupName) + return nil +} + +// deleteDedicatedHostGroup deletes an Azure dedicated host group resource. +func deleteDedicatedHostGroup(ctx context.Context, client *armcompute.DedicatedHostGroupsClient, resourceGroupName, hostGroupName string) error { + _, err := client.Delete(ctx, resourceGroupName, hostGroupName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Dedicated host group %s not found, skipping deletion", hostGroupName) + return nil + } + return fmt.Errorf("failed to delete dedicated host group: %w", err) + } + + log.Printf("Dedicated host group %s deleted successfully", hostGroupName) + return nil +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index a8a3f0aa..a6540dd2 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -243,6 +243,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create disk accesses client: %w", err) } + dedicatedHostGroupsClient, err := armcompute.NewDedicatedHostGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create dedicated host groups client: %w", err) + } + // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { adapters = append(adapters, @@ -382,6 +387,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewDiskAccessesClient(diskAccessesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeDedicatedHostGroup( + clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), + resourceGroupScopes, + ), cache), ) } @@ -431,6 +440,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeVirtualMachineExtension(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeProximityPlacementGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDiskAccess(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/compute-dedicated-host-group.go b/sources/azure/manual/compute-dedicated-host-group.go new file mode 100644 index 00000000..d4d331db --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host-group.go @@ -0,0 +1,185 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeDedicatedHostGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeDedicatedHostGroup) + +type computeDedicatedHostGroupWrapper struct { + client clients.DedicatedHostGroupsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeDedicatedHostGroup(client clients.DedicatedHostGroupsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeDedicatedHostGroupWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeDedicatedHostGroup, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-host-groups/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the dedicated host group name"), scope, c.Type()) + } + dedicatedHostGroupName := queryParts[0] + if dedicatedHostGroupName == "" { + return nil, azureshared.QueryError(errors.New("dedicated host group name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + dedicatedHostGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, dedicatedHostGroupName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureDedicatedHostGroupToSDPItem(&dedicatedHostGroup.DedicatedHostGroup, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-host-groups/list-by-resource-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, dedicatedHostGroup := range page.Value { + if dedicatedHostGroup.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostGroupToSDPItem(dedicatedHostGroup, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeDedicatedHostGroupWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, dedicatedHostGroup := range page.Value { + if dedicatedHostGroup.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostGroupToSDPItem(dedicatedHostGroup, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeDedicatedHostGroupWrapper) azureDedicatedHostGroupToSDPItem(dedicatedHostGroup *armcompute.DedicatedHostGroup, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(dedicatedHostGroup, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + if dedicatedHostGroup.Properties != nil && dedicatedHostGroup.Properties.Hosts != nil && dedicatedHostGroup.Name != nil { + hostGroupName := *dedicatedHostGroup.Name + for _, hostRef := range dedicatedHostGroup.Properties.Hosts { + if hostRef == nil || hostRef.ID == nil || *hostRef.ID == "" { + continue + } + hostName := azureshared.ExtractResourceName(*hostRef.ID) + if hostName == "" { + continue + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDedicatedHost.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(hostGroupName, hostName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Hosts are children of the group; host failure affects group view (In). Group deletion removes hosts (Out). + In: true, + Out: true, + }, + }) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeDedicatedHostGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(dedicatedHostGroup.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c *computeDedicatedHostGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeDedicatedHostGroupLookupByName, + } +} + +func (c *computeDedicatedHostGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeDedicatedHost: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/dedicated_host_group +func (c *computeDedicatedHostGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_dedicated_host_group.name", + }, + } +} + +func (c *computeDedicatedHostGroupWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/hostGroups/read", + } +} + +func (c *computeDedicatedHostGroupWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-dedicated-host-group_test.go b/sources/azure/manual/compute-dedicated-host-group_test.go new file mode 100644 index 00000000..6f760ac0 --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host-group_test.go @@ -0,0 +1,413 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeDedicatedHostGroup(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + hostGroupName := "test-host-group" + dedicatedHostGroup := createAzureDedicatedHostGroup(hostGroupName) + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{ + DedicatedHostGroup: *dedicatedHostGroup, + }, nil) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, hostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDedicatedHostGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeDedicatedHostGroup.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != hostGroupName { + t.Errorf("Expected unique attribute value %s, got %s", hostGroupName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithHosts", func(t *testing.T) { + hostGroupName := "test-host-group-with-hosts" + dedicatedHostGroup := createAzureDedicatedHostGroupWithHosts(hostGroupName, subscriptionID, resourceGroup, "host-1", "host-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{ + DedicatedHostGroup: *dedicatedHostGroup, + }, nil) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, hostGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeDedicatedHost.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-1"), + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + ExpectedType: azureshared.ComputeDedicatedHost.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-2"), + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("dedicated host group not found") + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + armcompute.DedicatedHostGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroup2 := createAzureDedicatedHostGroup("test-host-group-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroup2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroup2 := createAzureDedicatedHostGroup("test-host-group-2") + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroup2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") + hostGroupNilName := &armcompute.DedicatedHostGroup{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + }, + } + + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + mockPager := newMockDedicatedHostGroupsPager(ctrl, []*armcompute.DedicatedHostGroup{hostGroup1, hostGroupNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + errorPager := newErrorDedicatedHostGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostGroupsClient(ctrl) + errorPager := newErrorDedicatedHostGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeDedicatedHostGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +// createAzureDedicatedHostGroup creates a mock Azure Dedicated Host Group for testing. +func createAzureDedicatedHostGroup(hostGroupName string) *armcompute.DedicatedHostGroup { + return &armcompute.DedicatedHostGroup{ + Name: to.Ptr(hostGroupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + SupportAutomaticPlacement: to.Ptr(false), + AdditionalCapabilities: nil, + Hosts: nil, + InstanceView: nil, + }, + } +} + +// createAzureDedicatedHostGroupWithHosts creates a mock Azure Dedicated Host Group with host references. +func createAzureDedicatedHostGroupWithHosts(hostGroupName, subscriptionID, resourceGroup string, hostNames ...string) *armcompute.DedicatedHostGroup { + hosts := make([]*armcompute.SubResourceReadOnly, 0, len(hostNames)) + for _, name := range hostNames { + hosts = append(hosts, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/hostGroups/" + hostGroupName + "/hosts/" + name), + }) + } + return &armcompute.DedicatedHostGroup{ + Name: to.Ptr(hostGroupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.DedicatedHostGroupProperties{ + PlatformFaultDomainCount: to.Ptr(int32(2)), + Hosts: hosts, + }, + } +} + +// mockDedicatedHostGroupsPager is a mock pager for DedicatedHostGroupsClientListByResourceGroupResponse. +type mockDedicatedHostGroupsPager struct { + ctrl *gomock.Controller + items []*armcompute.DedicatedHostGroup + index int + more bool +} + +func newMockDedicatedHostGroupsPager(ctrl *gomock.Controller, items []*armcompute.DedicatedHostGroup) clients.DedicatedHostGroupsPager { + return &mockDedicatedHostGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockDedicatedHostGroupsPager) More() bool { + return m.more +} + +func (m *mockDedicatedHostGroupsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostGroupsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{ + DedicatedHostGroupListResult: armcompute.DedicatedHostGroupListResult{ + Value: []*armcompute.DedicatedHostGroup{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{ + DedicatedHostGroupListResult: armcompute.DedicatedHostGroupListResult{ + Value: []*armcompute.DedicatedHostGroup{item}, + }, + }, nil +} + +// errorDedicatedHostGroupsPager is a mock pager that always returns an error. +type errorDedicatedHostGroupsPager struct { + ctrl *gomock.Controller +} + +func newErrorDedicatedHostGroupsPager(ctrl *gomock.Controller) clients.DedicatedHostGroupsPager { + return &errorDedicatedHostGroupsPager{ctrl: ctrl} +} + +func (e *errorDedicatedHostGroupsPager) More() bool { + return true +} + +func (e *errorDedicatedHostGroupsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostGroupsClientListByResourceGroupResponse, error) { + return armcompute.DedicatedHostGroupsClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index f27e2f86..0ea4fc7b 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -16,6 +16,7 @@ var ( ComputeDiskEncryptionSet = shared.NewItemType(Azure, Compute, DiskEncryptionSet) ComputeProximityPlacementGroup = shared.NewItemType(Azure, Compute, ProximityPlacementGroup) ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) + ComputeDedicatedHost = shared.NewItemType(Azure, Compute, DedicatedHost) ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) ComputeImage = shared.NewItemType(Azure, Compute, Image) ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) diff --git a/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go b/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go new file mode 100644 index 00000000..67632dc2 --- /dev/null +++ b/sources/azure/shared/mocks/mock_dedicated_host_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dedicated-host-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_dedicated_host_groups_client.go -package=mocks -source=dedicated-host-groups-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDedicatedHostGroupsClient is a mock of DedicatedHostGroupsClient interface. +type MockDedicatedHostGroupsClient struct { + ctrl *gomock.Controller + recorder *MockDedicatedHostGroupsClientMockRecorder + isgomock struct{} +} + +// MockDedicatedHostGroupsClientMockRecorder is the mock recorder for MockDedicatedHostGroupsClient. +type MockDedicatedHostGroupsClientMockRecorder struct { + mock *MockDedicatedHostGroupsClient +} + +// NewMockDedicatedHostGroupsClient creates a new mock instance. +func NewMockDedicatedHostGroupsClient(ctrl *gomock.Controller) *MockDedicatedHostGroupsClient { + mock := &MockDedicatedHostGroupsClient{ctrl: ctrl} + mock.recorder = &MockDedicatedHostGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDedicatedHostGroupsClient) EXPECT() *MockDedicatedHostGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDedicatedHostGroupsClient) Get(ctx context.Context, resourceGroupName, dedicatedHostGroupName string, options *armcompute.DedicatedHostGroupsClientGetOptions) (armcompute.DedicatedHostGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, dedicatedHostGroupName, options) + ret0, _ := ret[0].(armcompute.DedicatedHostGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDedicatedHostGroupsClientMockRecorder) Get(ctx, resourceGroupName, dedicatedHostGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDedicatedHostGroupsClient)(nil).Get), ctx, resourceGroupName, dedicatedHostGroupName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockDedicatedHostGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.DedicatedHostGroupsClientListByResourceGroupOptions) clients.DedicatedHostGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.DedicatedHostGroupsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockDedicatedHostGroupsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockDedicatedHostGroupsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 744c215d..4b67c29d 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -61,6 +61,7 @@ const ( DiskEncryptionSet shared.Resource = "disk-encryption-set" ProximityPlacementGroup shared.Resource = "proximity-placement-group" DedicatedHostGroup shared.Resource = "dedicated-host-group" + DedicatedHost shared.Resource = "dedicated-host" CapacityReservationGroup shared.Resource = "capacity-reservation-group" Image shared.Resource = "image" Snapshot shared.Resource = "snapshot" From b51125cecb1be6596ec272c516490865355b63da Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:06:47 +0000 Subject: [PATCH 05/51] Eng 2366 create computecapacityreservationgroup adapter (#3845) > [!NOTE] > **Medium Risk** > Introduces a new Azure compute adapter that changes discovery coverage and link graph generation; risk is mainly around correctness of scope parsing, linked queries, and Azure API pagination/edge cases rather than security. > > **Overview** > Adds first-class discovery support for Azure **Capacity Reservation Groups** via a new `NewComputeCapacityReservationGroup` wrapper that implements `Get`, `List`, and `ListStream`, converting ARM responses into `sdp.Item`s and emitting linked queries to associated capacity reservations and virtual machines. > > Plumbs the new adapter into Azure manual adapter initialization (including a new ARM `CapacityReservationGroupsClient`), adds an interface wrapper + generated GoMock for the client, extends Azure shared type/model enums with `ComputeCapacityReservation`, and introduces both unit tests (pager + link behavior) and an end-to-end integration test that creates/reads/lists/tears down a real capacity reservation group. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c3df38389329b9c2baa1d6d96f5b4ff5f988191e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: a807098afd4da2ee9e772fd767b085906ca05b89 --- .../capacity-reservation-groups-client.go | 36 ++ ...compute-capacity-reservation-group_test.go | 301 +++++++++++++ sources/azure/manual/adapters.go | 10 + .../compute-capacity-reservation-group.go | 225 ++++++++++ ...compute-capacity-reservation-group_test.go | 419 ++++++++++++++++++ sources/azure/shared/item-types.go | 1 + ...mock_capacity_reservation_groups_client.go | 72 +++ sources/azure/shared/models.go | 4 + 8 files changed, 1068 insertions(+) create mode 100644 sources/azure/clients/capacity-reservation-groups-client.go create mode 100644 sources/azure/integration-tests/compute-capacity-reservation-group_test.go create mode 100644 sources/azure/manual/compute-capacity-reservation-group.go create mode 100644 sources/azure/manual/compute-capacity-reservation-group_test.go create mode 100644 sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go diff --git a/sources/azure/clients/capacity-reservation-groups-client.go b/sources/azure/clients/capacity-reservation-groups-client.go new file mode 100644 index 00000000..2b0e2497 --- /dev/null +++ b/sources/azure/clients/capacity-reservation-groups-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_capacity_reservation_groups_client.go -package=mocks -source=capacity-reservation-groups-client.go + +// CapacityReservationGroupsPager is a type alias for the generic Pager interface with capacity reservation group response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type CapacityReservationGroupsPager = Pager[armcompute.CapacityReservationGroupsClientListByResourceGroupResponse] + +// CapacityReservationGroupsClient is an interface for interacting with Azure capacity reservation groups +type CapacityReservationGroupsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) CapacityReservationGroupsPager + Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) +} + +type capacityReservationGroupsClient struct { + client *armcompute.CapacityReservationGroupsClient +} + +func (a *capacityReservationGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) CapacityReservationGroupsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *capacityReservationGroupsClient) Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, capacityReservationGroupName, options) +} + +// NewCapacityReservationGroupsClient creates a new CapacityReservationGroupsClient from the Azure SDK client +func NewCapacityReservationGroupsClient(client *armcompute.CapacityReservationGroupsClient) CapacityReservationGroupsClient { + return &capacityReservationGroupsClient{client: client} +} diff --git a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go new file mode 100644 index 00000000..c71097fe --- /dev/null +++ b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go @@ -0,0 +1,301 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestCapacityReservationGroupName = "ovm-integ-test-capacity-reservation-group" +) + +func TestComputeCapacityReservationGroupIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + capacityReservationGroupsClient, err := armcompute.NewCapacityReservationGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Capacity Reservation Groups client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + err = createCapacityReservationGroup(ctx, capacityReservationGroupsClient, integrationTestResourceGroup, integrationTestCapacityReservationGroupName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create capacity reservation group: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + t.Run("GetCapacityReservationGroup", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving capacity reservation group %s in subscription %s, resource group %s", + integrationTestCapacityReservationGroupName, subscriptionID, integrationTestResourceGroup) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestCapacityReservationGroupName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestCapacityReservationGroupName, uniqueAttrValue) + } + + log.Printf("Successfully retrieved capacity reservation group %s", integrationTestCapacityReservationGroupName) + }) + + t.Run("ListCapacityReservationGroups", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing capacity reservation groups in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + + listable, ok := capacityReservationGroupAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list capacity reservation groups: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one capacity reservation group, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestCapacityReservationGroupName { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find capacity reservation group %s in the list of capacity reservation groups", integrationTestCapacityReservationGroupName) + } + + log.Printf("Found %d capacity reservation groups in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for capacity reservation group %s", integrationTestCapacityReservationGroupName) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeCapacityReservationGroup.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeCapacityReservationGroup.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for capacity reservation group %s", integrationTestCapacityReservationGroupName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for capacity reservation group %s", integrationTestCapacityReservationGroupName) + + capacityReservationGroupWrapper := manual.NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := capacityReservationGroupWrapper.Scopes()[0] + + capacityReservationGroupAdapter := sources.WrapperToAdapter(capacityReservationGroupWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := capacityReservationGroupAdapter.Get(ctx, scope, integrationTestCapacityReservationGroupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for capacity reservation group %s", len(linkedQueries), integrationTestCapacityReservationGroupName) + + // Capacity reservation group may have zero or more linked queries (capacity reservations, VMs) depending on configuration + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + bp := liq.GetBlastPropagation() + if bp == nil { + t.Error("Linked item query has nil BlastPropagation") + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) + } else { + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), + bp.GetIn(), bp.GetOut()) + } + } + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + err := deleteCapacityReservationGroup(ctx, capacityReservationGroupsClient, integrationTestResourceGroup, integrationTestCapacityReservationGroupName) + if err != nil { + t.Fatalf("Failed to delete capacity reservation group: %v", err) + } + }) +} + +// createCapacityReservationGroup creates an Azure capacity reservation group resource (idempotent). +func createCapacityReservationGroup(ctx context.Context, client *armcompute.CapacityReservationGroupsClient, resourceGroupName, groupName, location string) error { + _, err := client.Get(ctx, resourceGroupName, groupName, nil) + if err == nil { + log.Printf("Capacity reservation group %s already exists, skipping creation", groupName) + return nil + } + + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("unexpected error checking capacity reservation group: %w", err) + } + + _, err = client.CreateOrUpdate(ctx, resourceGroupName, groupName, armcompute.CapacityReservationGroup{ + Location: ptr.To(location), + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-capacity-reservation-group"), + }, + }, nil) + if err != nil { + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Capacity reservation group %s already exists (conflict), skipping creation", groupName) + return nil + } + return fmt.Errorf("failed to create capacity reservation group: %w", err) + } + + log.Printf("Capacity reservation group %s created successfully", groupName) + return nil +} + +// deleteCapacityReservationGroup deletes an Azure capacity reservation group resource. +// Azure may return 202 Accepted for async delete; treat that as success. +func deleteCapacityReservationGroup(ctx context.Context, client *armcompute.CapacityReservationGroupsClient, resourceGroupName, groupName string) error { + _, err := client.Delete(ctx, resourceGroupName, groupName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + switch respErr.StatusCode { + case http.StatusNotFound: + log.Printf("Capacity reservation group %s not found, skipping deletion", groupName) + return nil + case http.StatusAccepted: + // Async delete accepted; resource deletion is in progress + log.Printf("Capacity reservation group %s delete accepted (202), teardown complete", groupName) + return nil + } + } + return fmt.Errorf("failed to delete capacity reservation group: %w", err) + } + + log.Printf("Capacity reservation group %s deleted successfully", groupName) + return nil +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index a6540dd2..abdd8f82 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -248,6 +248,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create dedicated host groups client: %w", err) } + capacityReservationGroupsClient, err := armcompute.NewCapacityReservationGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create capacity reservation groups client: %w", err) + } + // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { adapters = append(adapters, @@ -391,6 +396,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeCapacityReservationGroup( + clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), + resourceGroupScopes, + ), cache), ) } @@ -441,6 +450,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeProximityPlacementGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDiskAccess(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/compute-capacity-reservation-group.go b/sources/azure/manual/compute-capacity-reservation-group.go new file mode 100644 index 00000000..3773d533 --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation-group.go @@ -0,0 +1,225 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeCapacityReservationGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeCapacityReservationGroup) + +type computeCapacityReservationGroupWrapper struct { + client clients.CapacityReservationGroupsClient + *azureshared.MultiResourceGroupBase +} + +// NewComputeCapacityReservationGroup creates a new computeCapacityReservationGroupWrapper instance. +func NewComputeCapacityReservationGroup(client clients.CapacityReservationGroupsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeCapacityReservationGroupWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeCapacityReservationGroup, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservation-groups/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the capacity reservation group name"), scope, c.Type()) + } + capacityReservationGroupName := queryParts[0] + if capacityReservationGroupName == "" { + return nil, azureshared.QueryError(errors.New("capacity reservation group name cannot be empty"), scope, c.Type()) + } + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + capacityReservationGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, capacityReservationGroupName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureCapacityReservationGroupToSDPItem(&capacityReservationGroup.CapacityReservationGroup, scope) +} + +// ref:https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservation-groups/list-by-resource-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, 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 _, capacityReservationGroup := range page.Value { + if capacityReservationGroup.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationGroupToSDPItem(capacityReservationGroup, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeCapacityReservationGroupWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, capacityReservationGroup := range page.Value { + if capacityReservationGroup.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationGroupToSDPItem(capacityReservationGroup, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeCapacityReservationGroupWrapper) azureCapacityReservationGroupToSDPItem(capacityReservationGroup *armcompute.CapacityReservationGroup, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(capacityReservationGroup, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + if capacityReservationGroup.Properties != nil { + groupName := "" + if capacityReservationGroup.Name != nil { + groupName = *capacityReservationGroup.Name + } + + // Child resource: capacity reservations in this group (have their own GET/LIST endpoints) + if capacityReservationGroup.Properties.CapacityReservations != nil && groupName != "" { + for _, ref := range capacityReservationGroup.Properties.CapacityReservations { + if ref == nil || ref.ID == nil || *ref.ID == "" { + continue + } + reservationName := azureshared.ExtractResourceName(*ref.ID) + if reservationName == "" { + continue + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeCapacityReservation.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(groupName, reservationName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Reservations are children of the group; group view depends on them (In). Group deletion removes reservations (Out). + In: true, + Out: true, + }, + }) + } + } + + // External resource: VMs associated with this capacity reservation group + if capacityReservationGroup.Properties.VirtualMachinesAssociated != nil { + for _, ref := range capacityReservationGroup.Properties.VirtualMachinesAssociated { + if ref == nil || ref.ID == nil || *ref.ID == "" { + continue + } + vmName := azureshared.ExtractResourceName(*ref.ID) + if vmName == "" { + continue + } + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ref.ID); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: linkScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Group view lists associated VMs (In). VM deletion affects group's association list (Out). + In: true, + Out: true, + }, + }) + } + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeCapacityReservationGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(capacityReservationGroup.Tags), + LinkedItemQueries: linkedItemQueries, + } + + return sdpItem, nil +} +func (c *computeCapacityReservationGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeCapacityReservationGroupLookupByName, + } +} + +func (c *computeCapacityReservationGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeCapacityReservation: true, + azureshared.ComputeVirtualMachine: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/capacity_reservation_group +func (c *computeCapacityReservationGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_capacity_reservation_group.name", + }, + } +} + +func (c *computeCapacityReservationGroupWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/capacityReservationGroups/read", + } +} + +func (c *computeCapacityReservationGroupWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go new file mode 100644 index 00000000..4164d058 --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -0,0 +1,419 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeCapacityReservationGroup(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + groupName := "test-crg" + crg := createAzureCapacityReservationGroup(groupName) + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, nil).Return( + armcompute.CapacityReservationGroupsClientGetResponse{ + CapacityReservationGroup: *crg, + }, nil) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, groupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeCapacityReservationGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeCapacityReservationGroup.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != groupName { + t.Errorf("Expected unique attribute value %s, got %s", groupName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithLinkedResources", func(t *testing.T) { + groupName := "test-crg-with-links" + crg := createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, resourceGroup, []string{"res-1", "res-2"}, []string{"vm-1", "vm-2"}) + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, nil).Return( + armcompute.CapacityReservationGroupsClientGetResponse{ + CapacityReservationGroup: *crg, + }, nil) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, groupName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-1"), + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + { + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-2"), + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + { + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-1", + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + { + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-2", + ExpectedScope: scope, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting with no query parts, but got nil") + } + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting with empty name, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("capacity reservation group not found") + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + armcompute.CapacityReservationGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent", true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crg2 := createAzureCapacityReservationGroup("test-crg-2") + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crg2 := createAzureCapacityReservationGroup("test-crg-2") + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + crg1 := createAzureCapacityReservationGroup("test-crg-1") + crgNilName := &armcompute.CapacityReservationGroup{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{}, + } + + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crgNilName}) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ListWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + errorPager := newErrorCapacityReservationGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + _, err := listable.List(ctx, scope, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("ListStreamWithPagerError", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) + errorPager := newErrorCapacityReservationGroupsPager(ctrl) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + + wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(func(item *sdp.Item) {}, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + + if len(errs) == 0 { + t.Error("Expected error when pager returns error, but got none") + } + }) +} + +// createAzureCapacityReservationGroup creates a mock Azure Capacity Reservation Group for testing. +func createAzureCapacityReservationGroup(groupName string) *armcompute.CapacityReservationGroup { + return &armcompute.CapacityReservationGroup{ + Name: to.Ptr(groupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{}, + } +} + +// createAzureCapacityReservationGroupWithLinks creates a mock group with capacity reservation and VM links. +func createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, resourceGroup string, reservationNames, vmNames []string) *armcompute.CapacityReservationGroup { + reservations := make([]*armcompute.SubResourceReadOnly, 0, len(reservationNames)) + for _, name := range reservationNames { + reservations = append(reservations, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/capacityReservationGroups/" + groupName + "/capacityReservations/" + name), + }) + } + vms := make([]*armcompute.SubResourceReadOnly, 0, len(vmNames)) + for _, name := range vmNames { + vms = append(vms, &armcompute.SubResourceReadOnly{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + name), + }) + } + return &armcompute.CapacityReservationGroup{ + Name: to.Ptr(groupName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.CapacityReservationGroupProperties{ + CapacityReservations: reservations, + VirtualMachinesAssociated: vms, + }, + } +} + +// mockCapacityReservationGroupsPager is a mock pager for CapacityReservationGroupsClientListByResourceGroupResponse. +type mockCapacityReservationGroupsPager struct { + ctrl *gomock.Controller + items []*armcompute.CapacityReservationGroup + index int + more bool +} + +func newMockCapacityReservationGroupsPager(ctrl *gomock.Controller, items []*armcompute.CapacityReservationGroup) clients.CapacityReservationGroupsPager { + return &mockCapacityReservationGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockCapacityReservationGroupsPager) More() bool { + return m.more +} + +func (m *mockCapacityReservationGroupsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationGroupsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{ + CapacityReservationGroupListResult: armcompute.CapacityReservationGroupListResult{ + Value: []*armcompute.CapacityReservationGroup{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{ + CapacityReservationGroupListResult: armcompute.CapacityReservationGroupListResult{ + Value: []*armcompute.CapacityReservationGroup{item}, + }, + }, nil +} + +// errorCapacityReservationGroupsPager is a mock pager that always returns an error. +type errorCapacityReservationGroupsPager struct { + ctrl *gomock.Controller +} + +func newErrorCapacityReservationGroupsPager(ctrl *gomock.Controller) clients.CapacityReservationGroupsPager { + return &errorCapacityReservationGroupsPager{ctrl: ctrl} +} + +func (e *errorCapacityReservationGroupsPager) More() bool { + return true +} + +func (e *errorCapacityReservationGroupsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationGroupsClientListByResourceGroupResponse, error) { + return armcompute.CapacityReservationGroupsClientListByResourceGroupResponse{}, errors.New("pager error") +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 0ea4fc7b..6a2cd83a 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -18,6 +18,7 @@ var ( ComputeDedicatedHostGroup = shared.NewItemType(Azure, Compute, DedicatedHostGroup) ComputeDedicatedHost = shared.NewItemType(Azure, Compute, DedicatedHost) ComputeCapacityReservationGroup = shared.NewItemType(Azure, Compute, CapacityReservationGroup) + ComputeCapacityReservation = shared.NewItemType(Azure, Compute, CapacityReservation) ComputeImage = shared.NewItemType(Azure, Compute, Image) ComputeSnapshot = shared.NewItemType(Azure, Compute, Snapshot) ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) diff --git a/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go b/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go new file mode 100644 index 00000000..bc4ccdad --- /dev/null +++ b/sources/azure/shared/mocks/mock_capacity_reservation_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: capacity-reservation-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_capacity_reservation_groups_client.go -package=mocks -source=capacity-reservation-groups-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockCapacityReservationGroupsClient is a mock of CapacityReservationGroupsClient interface. +type MockCapacityReservationGroupsClient struct { + ctrl *gomock.Controller + recorder *MockCapacityReservationGroupsClientMockRecorder + isgomock struct{} +} + +// MockCapacityReservationGroupsClientMockRecorder is the mock recorder for MockCapacityReservationGroupsClient. +type MockCapacityReservationGroupsClientMockRecorder struct { + mock *MockCapacityReservationGroupsClient +} + +// NewMockCapacityReservationGroupsClient creates a new mock instance. +func NewMockCapacityReservationGroupsClient(ctrl *gomock.Controller) *MockCapacityReservationGroupsClient { + mock := &MockCapacityReservationGroupsClient{ctrl: ctrl} + mock.recorder = &MockCapacityReservationGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapacityReservationGroupsClient) EXPECT() *MockCapacityReservationGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockCapacityReservationGroupsClient) Get(ctx context.Context, resourceGroupName, capacityReservationGroupName string, options *armcompute.CapacityReservationGroupsClientGetOptions) (armcompute.CapacityReservationGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, capacityReservationGroupName, options) + ret0, _ := ret[0].(armcompute.CapacityReservationGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCapacityReservationGroupsClientMockRecorder) Get(ctx, resourceGroupName, capacityReservationGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCapacityReservationGroupsClient)(nil).Get), ctx, resourceGroupName, capacityReservationGroupName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockCapacityReservationGroupsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions) clients.CapacityReservationGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.CapacityReservationGroupsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockCapacityReservationGroupsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockCapacityReservationGroupsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 4b67c29d..dbb3ea47 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -46,6 +46,9 @@ const ( // Maintenance Maintenance shared.API = "maintenance" // Microsoft.Maintenance + + // Resources (subscriptions, resource groups) + Resources shared.API = "resources" // Microsoft.Resources ) // Resources @@ -63,6 +66,7 @@ const ( DedicatedHostGroup shared.Resource = "dedicated-host-group" DedicatedHost shared.Resource = "dedicated-host" CapacityReservationGroup shared.Resource = "capacity-reservation-group" + CapacityReservation shared.Resource = "capacity-reservation" Image shared.Resource = "image" Snapshot shared.Resource = "snapshot" DiskAccess shared.Resource = "disk-access" From d13dfd404110b2f33d1679b288550500304a15e7 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:06:36 +0000 Subject: [PATCH 06/51] Eng 2434 create computesharedgalleryapplicationversion adapter (#3848) > [!NOTE] > **Medium Risk** > Introduces a new Azure discovery adapter and changes how several compute resources emit linked-item relationships (including URL/blob parsing), which could affect graph completeness and link correctness across environments. > > **Overview** > Adds a new Azure manual adapter for `ComputeGalleryApplicationVersion`, including a dedicated SDK client wrapper, unit + integration tests, and registration in `manual/adapters.go` so it is discovered across resource groups. > > The new adapter supports `Get` and `Search` and enriches items with linked queries to parent gallery/application plus referenced artifact URLs (HTTP/DNS/IP) and related Azure resources (storage accounts/containers and disk encryption sets), with deduping and cross-scope handling. > > Updates existing compute adapters to link to the new `ComputeGalleryApplicationVersion` type (replacing the previous shared-gallery application version reference), and tweaks Capacity Reservation Group calls to pass `Expand` options for VM association data. > > Improves `ExtractStorageAccountNameFromBlobURI`/`ExtractContainerNameFromBlobURI` to recognize blob endpoints in sovereign clouds via host-based matching, and adjusts VM Run Command link generation to rely on the updated blob detection (emitting storage links when applicable, otherwise HTTP/DNS). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ca3ef78852cc5cf4b28581e031897c95c4fe2d6f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 882529baf9812c6a2aa12b30a2c265493b0429fc --- .../gallery-application-versions-client.go | 36 ++ ...ompute-gallery-application-version_test.go | 259 ++++++++++ sources/azure/manual/adapters.go | 10 + .../compute-capacity-reservation-group.go | 20 +- ...compute-capacity-reservation-group_test.go | 34 +- .../compute-gallery-application-version.go | 414 ++++++++++++++++ ...ompute-gallery-application-version_test.go | 452 ++++++++++++++++++ .../compute-virtual-machine-run-command.go | 65 ++- .../compute-virtual-machine-scale-set.go | 4 +- .../azure/manual/compute-virtual-machine.go | 4 +- ...allery_application_versions_client_test.go | 72 +++ sources/azure/shared/item-types.go | 4 +- ...ock_gallery_application_versions_client.go | 72 +++ sources/azure/shared/models.go | 4 +- sources/azure/shared/utils.go | 45 +- sources/azure/shared/utils_test.go | 53 ++ 16 files changed, 1473 insertions(+), 75 deletions(-) create mode 100644 sources/azure/clients/gallery-application-versions-client.go create mode 100644 sources/azure/integration-tests/compute-gallery-application-version_test.go create mode 100644 sources/azure/manual/compute-gallery-application-version.go create mode 100644 sources/azure/manual/compute-gallery-application-version_test.go create mode 100644 sources/azure/manual/mock_gallery_application_versions_client_test.go create mode 100644 sources/azure/shared/mocks/mock_gallery_application_versions_client.go diff --git a/sources/azure/clients/gallery-application-versions-client.go b/sources/azure/clients/gallery-application-versions-client.go new file mode 100644 index 00000000..c59744b1 --- /dev/null +++ b/sources/azure/clients/gallery-application-versions-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_gallery_application_versions_client.go -package=mocks -source=gallery-application-versions-client.go + +// GalleryApplicationVersionsPager is a type alias for the generic Pager interface with gallery application version response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type GalleryApplicationVersionsPager = Pager[armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse] + +// GalleryApplicationVersionsClient is an interface for interacting with Azure gallery application versions +type GalleryApplicationVersionsClient interface { + NewListByGalleryApplicationPager(resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) GalleryApplicationVersionsPager + Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) +} + +type galleryApplicationVersionsClient struct { + client *armcompute.GalleryApplicationVersionsClient +} + +func (c *galleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) GalleryApplicationVersionsPager { + return c.client.NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options) +} + +func (c *galleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewGalleryApplicationVersionsClient creates a new GalleryApplicationVersionsClient from the Azure SDK client +func NewGalleryApplicationVersionsClient(client *armcompute.GalleryApplicationVersionsClient) GalleryApplicationVersionsClient { + return &galleryApplicationVersionsClient{client: client} +} diff --git a/sources/azure/integration-tests/compute-gallery-application-version_test.go b/sources/azure/integration-tests/compute-gallery-application-version_test.go new file mode 100644 index 00000000..88d2b400 --- /dev/null +++ b/sources/azure/integration-tests/compute-gallery-application-version_test.go @@ -0,0 +1,259 @@ +package integrationtests + +import ( + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +// Gallery application version integration tests require pre-existing Azure resources +// (gallery, gallery application, and gallery application version) because creating +// a version requires a source blob URL. Set these env vars to run the tests: +// +// AZURE_TEST_GALLERY_NAME - name of the gallery +// AZURE_TEST_GALLERY_APPLICATION_NAME - name of the gallery application +// AZURE_TEST_GALLERY_APPLICATION_VERSION - name of the gallery application version +// +// Optional: AZURE_TEST_GALLERY_RESOURCE_GROUP (defaults to overmind-integration-tests) +func getGalleryApplicationVersionTestConfig(t *testing.T) (resourceGroup, galleryName, applicationName, versionName string, skip bool) { + galleryName = os.Getenv("AZURE_TEST_GALLERY_NAME") + applicationName = os.Getenv("AZURE_TEST_GALLERY_APPLICATION_NAME") + versionName = os.Getenv("AZURE_TEST_GALLERY_APPLICATION_VERSION") + resourceGroup = os.Getenv("AZURE_TEST_GALLERY_RESOURCE_GROUP") + if resourceGroup == "" { + resourceGroup = integrationTestResourceGroup + } + if galleryName == "" || applicationName == "" || versionName == "" { + t.Skip("Skipping gallery application version integration test: set AZURE_TEST_GALLERY_NAME, AZURE_TEST_GALLERY_APPLICATION_NAME, and AZURE_TEST_GALLERY_APPLICATION_VERSION to run") + return "", "", "", "", true + } + return resourceGroup, galleryName, applicationName, versionName, false +} + +func TestComputeGalleryApplicationVersionIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + resourceGroup, galleryName, applicationName, versionName, skip := getGalleryApplicationVersionTestConfig(t) + if skip { + return + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + galleryApplicationVersionsClient, err := armcompute.NewGalleryApplicationVersionsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Gallery Application Versions client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Run", func(t *testing.T) { + ctx := t.Context() + + // Ensure resource group exists (may be used for pre-created gallery) + err := createResourceGroup(ctx, rgClient, resourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create/verify resource group: %v", err) + } + + t.Run("GetGalleryApplicationVersion", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving gallery application version %s/%s/%s in subscription %s, resource group %s", + galleryName, applicationName, versionName, subscriptionID, resourceGroup) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + expectedUniqueAttr := shared.CompositeLookupKey(galleryName, applicationName, versionName) + if uniqueAttrValue != expectedUniqueAttr { + t.Fatalf("Expected unique attribute value to be %s, got %s", expectedUniqueAttr, uniqueAttrValue) + } + + log.Printf("Successfully retrieved gallery application version %s", versionName) + }) + + t.Run("SearchGalleryApplicationVersions", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Searching gallery application versions for gallery %s, application %s in subscription %s, resource group %s", + galleryName, applicationName, subscriptionID, resourceGroup) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := galleryName + shared.QuerySeparator + applicationName + sdpItems, err := searchable.Search(ctx, scope, searchQuery, true) + if err != nil { + t.Fatalf("Failed to search gallery application versions: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one gallery application version, got %d", len(sdpItems)) + } + + var found bool + expectedUniqueAttr := shared.CompositeLookupKey(galleryName, applicationName, versionName) + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == expectedUniqueAttr { + found = true + break + } + } + + if !found { + t.Fatalf("Expected to find gallery application version %s in the search results", versionName) + } + + log.Printf("Found %d gallery application versions in resource group %s", len(sdpItems), resourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for gallery application version %s", versionName) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryApplicationVersion.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeGalleryApplicationVersion.String(), sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for gallery application version %s", versionName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for gallery application version %s", versionName) + + wrapper := manual.NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}, + ) + scope := wrapper.Scopes()[0] + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + query := shared.CompositeLookupKey(galleryName, applicationName, versionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for gallery application version %s", len(linkedQueries), versionName) + + // Should have at least Gallery and Gallery Application parent links + if len(linkedQueries) < 2 { + t.Fatalf("Expected at least 2 linked item queries (Gallery, Gallery Application), got %d", len(linkedQueries)) + } + + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + bp := liq.GetBlastPropagation() + if bp == nil { + t.Error("Linked item query has nil BlastPropagation") + } else { + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), + bp.GetIn(), bp.GetOut()) + } + } + }) + }) +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index abdd8f82..386c10b2 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -253,6 +253,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create capacity reservation groups client: %w", err) } + galleryApplicationVersionsClient, err := armcompute.NewGalleryApplicationVersionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create gallery application versions client: %w", err) + } + // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { adapters = append(adapters, @@ -400,6 +405,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + resourceGroupScopes, + ), cache), ) } @@ -451,6 +460,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeDiskAccess(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/compute-capacity-reservation-group.go b/sources/azure/manual/compute-capacity-reservation-group.go index 3773d533..e813c7cf 100644 --- a/sources/azure/manual/compute-capacity-reservation-group.go +++ b/sources/azure/manual/compute-capacity-reservation-group.go @@ -33,6 +33,20 @@ func NewComputeCapacityReservationGroup(client clients.CapacityReservationGroups } } +func capacityReservationGroupGetOptions() *armcompute.CapacityReservationGroupsClientGetOptions { + expand := armcompute.CapacityReservationGroupInstanceViewTypes(armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef) + return &armcompute.CapacityReservationGroupsClientGetOptions{ + Expand: &expand, + } +} + +func capacityReservationGroupListOptions() *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions { + expand := armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef + return &armcompute.CapacityReservationGroupsClientListByResourceGroupOptions{ + Expand: &expand, + } +} + // ref: https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservation-groups/get?view=rest-compute-2025-04-01&tabs=HTTP func (c *computeCapacityReservationGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { if len(queryParts) != 1 { @@ -46,7 +60,7 @@ func (c *computeCapacityReservationGroupWrapper) Get(ctx context.Context, scope if err != nil { return nil, azureshared.QueryError(err, scope, c.Type()) } - capacityReservationGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, capacityReservationGroupName, nil) + capacityReservationGroup, err := c.client.Get(ctx, rgScope.ResourceGroup, capacityReservationGroupName, capacityReservationGroupGetOptions()) if err != nil { return nil, azureshared.QueryError(err, scope, c.Type()) } @@ -59,7 +73,7 @@ func (c *computeCapacityReservationGroupWrapper) List(ctx context.Context, scope if err != nil { return nil, azureshared.QueryError(err, scope, c.Type()) } - pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, capacityReservationGroupListOptions()) var items []*sdp.Item for pager.More() { @@ -87,7 +101,7 @@ func (c *computeCapacityReservationGroupWrapper) ListStream(ctx context.Context, stream.SendError(azureshared.QueryError(err, scope, c.Type())) return } - pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, capacityReservationGroupListOptions()) for pager.More() { page, err := pager.NextPage(ctx) if err != nil { diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go index 4164d058..68a92376 100644 --- a/sources/azure/manual/compute-capacity-reservation-group_test.go +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -35,7 +35,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { crg := createAzureCapacityReservationGroup(groupName) mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) - mockClient.EXPECT().Get(ctx, resourceGroup, groupName, nil).Return( + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, gomock.Eq(capacityReservationGroupGetOptions())).Return( armcompute.CapacityReservationGroupsClientGetResponse{ CapacityReservationGroup: *crg, }, nil) @@ -75,7 +75,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { crg := createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, resourceGroup, []string{"res-1", "res-2"}, []string{"vm-1", "vm-2"}) mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) - mockClient.EXPECT().Get(ctx, resourceGroup, groupName, nil).Return( + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, gomock.Eq(capacityReservationGroupGetOptions())).Return( armcompute.CapacityReservationGroupsClientGetResponse{ CapacityReservationGroup: *crg, }, nil) @@ -148,7 +148,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { t.Run("Get_ClientError", func(t *testing.T) { expectedErr := errors.New("capacity reservation group not found") mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) - mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", nil).Return( + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent", gomock.Eq(capacityReservationGroupGetOptions())).Return( armcompute.CapacityReservationGroupsClientGetResponse{}, expectedErr) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -166,7 +166,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) - mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -201,7 +201,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crg2}) - mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -252,7 +252,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) mockPager := newMockCapacityReservationGroupsPager(ctrl, []*armcompute.CapacityReservationGroup{crg1, crgNilName}) - mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(mockPager) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -275,7 +275,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { t.Run("ListWithPagerError", func(t *testing.T) { mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) errorPager := newErrorCapacityReservationGroupsPager(ctrl) - mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(errorPager) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -294,7 +294,7 @@ func TestComputeCapacityReservationGroup(t *testing.T) { t.Run("ListStreamWithPagerError", func(t *testing.T) { mockClient := mocks.NewMockCapacityReservationGroupsClient(ctrl) errorPager := newErrorCapacityReservationGroupsPager(ctrl) - mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(errorPager) + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, gomock.Eq(capacityReservationGroupListOptions())).Return(errorPager) wrapper := manual.NewComputeCapacityReservationGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -319,6 +319,20 @@ func TestComputeCapacityReservationGroup(t *testing.T) { }) } +func capacityReservationGroupGetOptions() *armcompute.CapacityReservationGroupsClientGetOptions { + expand := armcompute.CapacityReservationGroupInstanceViewTypes(armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef) + return &armcompute.CapacityReservationGroupsClientGetOptions{ + Expand: &expand, + } +} + +func capacityReservationGroupListOptions() *armcompute.CapacityReservationGroupsClientListByResourceGroupOptions { + expand := armcompute.ExpandTypesForGetCapacityReservationGroupsVirtualMachinesRef + return &armcompute.CapacityReservationGroupsClientListByResourceGroupOptions{ + Expand: &expand, + } +} + // createAzureCapacityReservationGroup creates a mock Azure Capacity Reservation Group for testing. func createAzureCapacityReservationGroup(groupName string) *armcompute.CapacityReservationGroup { return &armcompute.CapacityReservationGroup{ @@ -353,8 +367,8 @@ func createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, res "env": to.Ptr("test"), }, Properties: &armcompute.CapacityReservationGroupProperties{ - CapacityReservations: reservations, - VirtualMachinesAssociated: vms, + CapacityReservations: reservations, + VirtualMachinesAssociated: vms, }, } } diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go new file mode 100644 index 00000000..07c72fbb --- /dev/null +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -0,0 +1,414 @@ +package manual + +import ( + "context" + "errors" + "net" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/workspace/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 ( + ComputeGalleryApplicationVersionLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplicationVersion) + ComputeGalleryLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGallery) //todo: move to its adapter file when created, this is just a placeholder + ComputeGalleryApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplication) //todo: move to its adapter file when created, this is just a placeholder +) + +type computeGalleryApplicationVersionWrapper struct { + client clients.GalleryApplicationVersionsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGalleryApplicationVersion(client clients.GalleryApplicationVersionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeGalleryApplicationVersionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGalleryApplicationVersion, + ), + } +} + +func (c computeGalleryApplicationVersionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 3 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 3 and be the gallery name, gallery application name, and gallery application version name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + return nil, azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type()) + } + galleryApplicationVersionName := queryParts[2] + if galleryApplicationVersionName == "" { + return nil, azureshared.QueryError(errors.New("gallery application version name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryApplicationVersionToSDPItem(&resp.GalleryApplicationVersion, galleryName, galleryApplicationName, scope) +} + +func (c computeGalleryApplicationVersionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery application name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + return nil, azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByGalleryApplicationPager(rgScope.ResourceGroup, galleryName, galleryApplicationName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, galleryApplicationVersion := range page.Value { + if galleryApplicationVersion == nil || galleryApplicationVersion.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationVersionToSDPItem(galleryApplicationVersion, galleryName, galleryApplicationName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionToSDPItem( + galleryApplicationVersion *armcompute.GalleryApplicationVersion, + galleryName, + galleryApplicationName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(galleryApplicationVersion, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if galleryApplicationVersion.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery application version name is nil"), scope, c.Type()) + } + galleryApplicationVersionName := *galleryApplicationVersion.Name + if galleryApplicationVersionName == "" { + return nil, azureshared.QueryError(errors.New("gallery application version name cannot be empty"), scope, c.Type()) + } + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Gallery: version depends on gallery; deleting version does not delete gallery + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGallery.String(), + Method: sdp.QueryMethod_GET, + Query: galleryName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If gallery is deleted → version is deleted (In: true) + Out: false, // If version is deleted → gallery remains (Out: false) + }, + }) + + // Parent Gallery Application: version depends on application; deleting version does not delete application + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryApplication.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, galleryApplicationName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If application is deleted → version is deleted (In: true) + Out: false, // If version is deleted → application remains (Out: false) + }, + }) + + // MediaLink and DefaultConfigurationLink: add stdlib.NetworkHTTP, stdlib.NetworkDNS (hostname), stdlib.NetworkIP (when host is IP), azureshared.StorageAccount and azureshared.StorageBlobContainer (when Azure Blob) links. + // Dedupe DNS by hostname, IP by address, StorageAccount by account name, and StorageBlobContainer by (account, container) so the same resource is not linked twice. + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + seenStorageAccounts := make(map[string]struct{}) + seenBlobContainers := make(map[string]struct{}) + if galleryApplicationVersion.Properties != nil && galleryApplicationVersion.Properties.PublishingProfile != nil && galleryApplicationVersion.Properties.PublishingProfile.Source != nil { + src := galleryApplicationVersion.Properties.PublishingProfile.Source + addBlobLinks := func(link string) { + if link == "" || (!strings.HasPrefix(link, "http://") && !strings.HasPrefix(link, "https://")) { + return + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: link, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If endpoint is unavailable → version artifact cannot be accessed (In: true) + Out: true, // Endpoint may be used by other resources (Out: true) + }, + }) + hostFromURL := azureshared.ExtractDNSFromURL(link) + if hostFromURL != "" { + hostOnly := hostFromURL + if h, _, err := net.SplitHostPort(hostFromURL); err == nil { + hostOnly = h + } + if net.ParseIP(hostOnly) != nil { + if _, seen := seenIPs[hostOnly]; !seen { + seenIPs[hostOnly] = struct{}{} + linkedItemQueries = append(linkedItemQueries, networkIPQuery(hostOnly)) + } + } else { + if _, seen := linkedDNSHostnames[hostOnly]; !seen { + linkedDNSHostnames[hostOnly] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: hostOnly, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + } + if accountName := azureshared.ExtractStorageAccountNameFromBlobURI(link); accountName != "" { + if _, seen := seenStorageAccounts[accountName]; !seen { + seenStorageAccounts[accountName] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If storage account is unavailable → version artifact cannot be accessed (In: true) + Out: false, // If version is deleted → storage account remains (Out: false) + }, + }) + } + containerName := azureshared.ExtractContainerNameFromBlobURI(link) + if containerName != "" { + containerKey := shared.CompositeLookupKey(accountName, containerName) + if _, seen := seenBlobContainers[containerKey]; !seen { + seenBlobContainers[containerKey] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageBlobContainer.String(), + Method: sdp.QueryMethod_GET, + Query: containerKey, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If blob container is unavailable → version artifact cannot be accessed (In: true) + Out: false, // If version is deleted → blob container remains (Out: false) + }, + }) + } + } + } + } + if src.MediaLink != nil && *src.MediaLink != "" { + addBlobLinks(*src.MediaLink) + } + if src.DefaultConfigurationLink != nil && *src.DefaultConfigurationLink != "" { + defaultConfigLink := *src.DefaultConfigurationLink + if strings.HasPrefix(defaultConfigLink, "http://") || strings.HasPrefix(defaultConfigLink, "https://") { + sameAsMedia := src.MediaLink != nil && *src.MediaLink == defaultConfigLink + if !sameAsMedia { + addBlobLinks(defaultConfigLink) + } + } + } + } + + // Disk encryption sets from TargetRegions[].Encryption (OS and data disk); dedupe by ID + seenEncryptionSetIDs := make(map[string]struct{}) + if galleryApplicationVersion.Properties != nil && galleryApplicationVersion.Properties.PublishingProfile != nil && galleryApplicationVersion.Properties.PublishingProfile.TargetRegions != nil { + for _, tr := range galleryApplicationVersion.Properties.PublishingProfile.TargetRegions { + if tr == nil || tr.Encryption == nil { + continue + } + if tr.Encryption.OSDiskImage != nil && tr.Encryption.OSDiskImage.DiskEncryptionSetID != nil && *tr.Encryption.OSDiskImage.DiskEncryptionSetID != "" { + id := *tr.Encryption.OSDiskImage.DiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If encryption set changes → replication encryption affected (In: true) + Out: false, // If version is deleted → encryption set remains (Out: false) + }, + }) + } + } + } + if tr.Encryption.OSDiskImage != nil && tr.Encryption.OSDiskImage.SecurityProfile != nil && tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID != nil && *tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID != "" { + id := *tr.Encryption.OSDiskImage.SecurityProfile.SecureVMDiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + if tr.Encryption.DataDiskImages != nil { + for _, ddi := range tr.Encryption.DataDiskImages { + if ddi != nil && ddi.DiskEncryptionSetID != nil && *ddi.DiskEncryptionSetID != "" { + id := *ddi.DiskEncryptionSetID + if _, seen := seenEncryptionSetIDs[id]; !seen { + seenEncryptionSetIDs[id] = struct{}{} + name := azureshared.ExtractResourceName(id) + if name != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(id); extractedScope != "" { + linkScope = extractedScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: name, + Scope: linkScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + } + } + } + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGalleryApplicationVersion.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(galleryApplicationVersion.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeGalleryApplicationVersionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + ComputeGalleryApplicationLookupByName, + ComputeGalleryApplicationVersionLookupByName, + } +} + +func (c computeGalleryApplicationVersionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeGalleryLookupByName, + ComputeGalleryApplicationLookupByName, + }, + } +} + +func (c computeGalleryApplicationVersionWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGallery, + azureshared.ComputeGalleryApplication, + azureshared.ComputeDiskEncryptionSet, + azureshared.StorageAccount, + azureshared.StorageBlobContainer, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/gallery_application_version +func (c computeGalleryApplicationVersionWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + //example id: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Compute/galleries/gallery1/applications/galleryApplication1/versions/galleryApplicationVersion1 + TerraformQueryMap: "azurerm_gallery_application_version.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryApplicationVersionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/applications/versions/read", + } +} + +func (c computeGalleryApplicationVersionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go new file mode 100644 index 00000000..fffb7bd5 --- /dev/null +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -0,0 +1,452 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// mockGalleryApplicationVersionsPager is a mock pager for ListByGalleryApplication. +type mockGalleryApplicationVersionsPager struct { + pages []armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse + index int +} + +func (m *mockGalleryApplicationVersionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockGalleryApplicationVersionsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse, error) { + if m.index >= len(m.pages) { + return armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorGalleryApplicationVersionsPager is a mock pager that always returns an error. +type errorGalleryApplicationVersionsPager struct{} + +func (e *errorGalleryApplicationVersionsPager) More() bool { + return true +} + +func (e *errorGalleryApplicationVersionsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse, error) { + return armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{}, errors.New("pager error") +} + +// testGalleryApplicationVersionsClient wraps the mock and returns a pager from NewListByGalleryApplicationPager. +type testGalleryApplicationVersionsClient struct { + *MockGalleryApplicationVersionsClient + pager clients.GalleryApplicationVersionsPager +} + +// NewListByGalleryApplicationPager returns the test pager so we don't need to mock this call. +func (t *testGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + if t.pager != nil { + return t.pager + } + return t.MockGalleryApplicationVersionsClient.NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options) +} + +func createAzureGalleryApplicationVersion(versionName string) *armcompute.GalleryApplicationVersion { + return &armcompute.GalleryApplicationVersion{ + Name: to.Ptr(versionName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.GalleryApplicationVersionProperties{ + PublishingProfile: &armcompute.GalleryApplicationVersionPublishingProfile{ + Source: &armcompute.UserArtifactSource{ + MediaLink: to.Ptr("https://mystorageaccount.blob.core.windows.net/packages/app.zip"), + }, + }, + }, + } +} + +func createAzureGalleryApplicationVersionWithLinks(versionName, subscriptionID, resourceGroup string) *armcompute.GalleryApplicationVersion { + v := createAzureGalleryApplicationVersion(versionName) + v.Properties.PublishingProfile.Source.DefaultConfigurationLink = to.Ptr("https://mystorageaccount.blob.core.windows.net/config/default.json") + desID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-des" + v.Properties.PublishingProfile.TargetRegions = []*armcompute.TargetRegion{ + { + Name: to.Ptr("eastus"), + Encryption: &armcompute.EncryptionImages{ + OSDiskImage: &armcompute.OSDiskImageEncryption{ + DiskEncryptionSetID: to.Ptr(desID), + }, + }, + }, + } + return v +} + +func TestComputeGalleryApplicationVersion(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + galleryName := "test-gallery" + galleryApplicationName := "test-app" + galleryApplicationVersionName := "1.0.0" + + t.Run("Get", func(t *testing.T) { + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryApplicationVersion.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGalleryApplicationVersion.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag env=test, got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithLinkedResources", func(t *testing.T) { + version := createAzureGalleryApplicationVersionWithLinks(galleryApplicationVersionName, subscriptionID, resourceGroup) + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "config"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/config/default.json", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + {ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Adapter expects query to split into 3 parts (gallery, application, version); single part is invalid + _, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryApplicationName, galleryApplicationVersionName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("version not found") + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, "nonexistent", nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{}, expectedErr) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, "nonexistent") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Get_NonBlobURL_NoStorageLinks", func(t *testing.T) { + // MediaLink that is not Azure Blob Storage must not create StorageAccount/StorageBlobContainer links. + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + version.Properties.PublishingProfile.Source.MediaLink = to.Ptr("https://example.com/artifacts/app.zip") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query == nil { + continue + } + typ := query.GetType() + if typ == azureshared.StorageAccount.String() || typ == azureshared.StorageBlobContainer.String() { + t.Errorf("Non-blob URL must not create storage links; found linked query type %s with query %s", typ, query.GetQuery()) + } + } + // Should still have NetworkHTTP and NetworkDNS for the URL + hasHTTP := false + hasDNS := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil { + if query.GetType() == stdlib.NetworkHTTP.String() { + hasHTTP = true + } + if query.GetType() == stdlib.NetworkDNS.String() { + hasDNS = true + } + } + } + if !hasHTTP { + t.Error("Expected NetworkHTTP linked query for the media URL") + } + if !hasDNS { + t.Error("Expected NetworkDNS linked query for the media URL hostname") + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + // When MediaLink or DefaultConfigurationLink has a literal IP host, emit stdlib.NetworkIP link (GET, global), not DNS. + version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) + version.Properties.PublishingProfile.Source.MediaLink = to.Ptr("https://192.168.1.10:8443/artifacts/app.zip") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( + armcompute.GalleryApplicationVersionsClientGetResponse{ + GalleryApplicationVersion: *version, + }, nil) + + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName, galleryApplicationVersionName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil && query.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", query.GetMethod()) + } + if query.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", query.GetScope()) + } + if query.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", query.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when MediaLink host is an IP address") + } + }) + + t.Run("Search", func(t *testing.T) { + v1 := createAzureGalleryApplicationVersion("1.0.0") + v2 := createAzureGalleryApplicationVersion("1.0.1") + + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + pages := []armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{ + { + GalleryApplicationVersionList: armcompute.GalleryApplicationVersionList{ + Value: []*armcompute.GalleryApplicationVersion{v1, v2}, + }, + }, + } + mockPager := &mockGalleryApplicationVersionsPager{pages: pages} + testClient := &testGalleryApplicationVersionsClient{ + MockGalleryApplicationVersionsClient: mockClient, + pager: mockPager, + } + + wrapper := NewComputeGalleryApplicationVersion(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(galleryName, galleryApplicationName) + sdpItems, err := searchable.Search(ctx, scope, searchQuery, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + _, err := searchable.Search(ctx, scope, galleryName, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "", galleryApplicationName) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + errorPager := &errorGalleryApplicationVersionsPager{} + testClient := &testGalleryApplicationVersionsClient{ + MockGalleryApplicationVersionsClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeGalleryApplicationVersion(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(galleryName, galleryApplicationName) + _, err := searchable.Search(ctx, scope, searchQuery, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeGallery: true, + azureshared.ComputeGalleryApplication: true, + azureshared.ComputeDiskEncryptionSet: true, + azureshared.StorageAccount: true, + azureshared.StorageBlobContainer: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index 2197a1a0..a1cdde51 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -153,53 +153,48 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS } uri := *blobURI - isBlobURI := strings.Contains(uri, ".blob.core.windows.net") - - // Check if it's an Azure blob URI (contains .blob.core.windows.net) - if isBlobURI { - storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(uri) - if storageAccountName != "" { - // Link to Storage Account - // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2025-06-01&tabs=HTTP - // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2025-06-01 + storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(uri) + if storageAccountName != "" { + // Link to Storage Account + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties?view=rest-storagerp-2025-06-01&tabs=HTTP + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2025-06-01 + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Storage Account is deleted/modified → blob becomes inaccessible (In: true) + Out: false, // If Run Command is deleted → Storage Account remains (Out: false) + }, // Run Command depends on Storage Account for blob access + }) + + // Extract container name and link to Blob Container + containerName := azureshared.ExtractContainerNameFromBlobURI(uri) + if containerName != "" { + // Link to Blob Container + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/blob-containers/get?view=rest-storagerp-2025-06-01&tabs=HTTP + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2025-06-01 sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.StorageAccount.String(), + Type: azureshared.StorageBlobContainer.String(), Method: sdp.QueryMethod_GET, - Query: storageAccountName, + Query: shared.CompositeLookupKey(storageAccountName, containerName), Scope: scope, }, BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Storage Account remains (Out: false) - }, // Run Command depends on Storage Account for blob access + In: true, // If Blob Container is deleted/modified → blob becomes inaccessible (In: true) + Out: false, // If Run Command is deleted → Blob Container remains (Out: false) + }, // Run Command depends on Blob Container for blob access }) - - // Extract container name and link to Blob Container - containerName := azureshared.ExtractContainerNameFromBlobURI(uri) - if containerName != "" { - // Link to Blob Container - // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/blob-containers/get?view=rest-storagerp-2025-06-01&tabs=HTTP - // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2025-06-01 - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.StorageBlobContainer.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(storageAccountName, containerName), - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Blob Container is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Blob Container remains (Out: false) - }, // Run Command depends on Blob Container for blob access - }) - } } } // Link to stdlib.NetworkHTTP and DNS only for non-blob URIs // For blob URIs, the StorageBlobContainer already has these links - if !isBlobURI && (strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")) { + if storageAccountName == "" && (strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")) { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: stdlib.NetworkHTTP.String(), diff --git a/sources/azure/manual/compute-virtual-machine-scale-set.go b/sources/azure/manual/compute-virtual-machine-scale-set.go index 601dca50..7a9a7f56 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set.go @@ -698,7 +698,7 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.ComputeSharedGalleryApplicationVersion.String(), + Type: azureshared.ComputeGalleryApplicationVersion.String(), Method: sdp.QueryMethod_GET, Query: shared.CompositeLookupKey(galleryName, applicationName, version), Scope: extractedScope, @@ -990,7 +990,7 @@ func (c computeVirtualMachineScaleSetWrapper) PotentialLinks() map[shared.ItemTy azureshared.ComputeImage, azureshared.ComputeSharedGalleryImage, azureshared.ComputeCommunityGalleryImage, - azureshared.ComputeSharedGalleryApplicationVersion, + azureshared.ComputeGalleryApplicationVersion, // Storage resources azureshared.StorageAccount, // Identity resources diff --git a/sources/azure/manual/compute-virtual-machine.go b/sources/azure/manual/compute-virtual-machine.go index 710577b0..67c7b8d7 100644 --- a/sources/azure/manual/compute-virtual-machine.go +++ b/sources/azure/manual/compute-virtual-machine.go @@ -59,7 +59,7 @@ func (c computeVirtualMachineWrapper) PotentialLinks() map[shared.ItemType]bool azureshared.ComputeVirtualMachineScaleSet, azureshared.ComputeImage, azureshared.ComputeSharedGalleryImage, - azureshared.ComputeSharedGalleryApplicationVersion, + azureshared.ComputeGalleryApplicationVersion, azureshared.ComputeVirtualMachineExtension, azureshared.ComputeVirtualMachineRunCommand, azureshared.ManagedIdentityUserAssignedIdentity, @@ -662,7 +662,7 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.ComputeSharedGalleryApplicationVersion.String(), + Type: azureshared.ComputeGalleryApplicationVersion.String(), Method: sdp.QueryMethod_GET, Query: shared.CompositeLookupKey(galleryName, appName, versionName), Scope: linkScope, diff --git a/sources/azure/manual/mock_gallery_application_versions_client_test.go b/sources/azure/manual/mock_gallery_application_versions_client_test.go new file mode 100644 index 00000000..e86f52f3 --- /dev/null +++ b/sources/azure/manual/mock_gallery_application_versions_client_test.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sources/azure/clients/gallery-application-versions-client.go +// +// Generated by this command: +// +// mockgen -destination=sources/azure/manual/mock_gallery_application_versions_client_test.go -package=manual -source=sources/azure/clients/gallery-application-versions-client.go +// + +// Package manual is a generated GoMock package. +package manual + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryApplicationVersionsClient is a mock of GalleryApplicationVersionsClient interface. +type MockGalleryApplicationVersionsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationVersionsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationVersionsClientMockRecorder is the mock recorder for MockGalleryApplicationVersionsClient. +type MockGalleryApplicationVersionsClientMockRecorder struct { + mock *MockGalleryApplicationVersionsClient +} + +// NewMockGalleryApplicationVersionsClient creates a new mock instance. +func NewMockGalleryApplicationVersionsClient(ctrl *gomock.Controller) *MockGalleryApplicationVersionsClient { + mock := &MockGalleryApplicationVersionsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationVersionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationVersionsClient) EXPECT() *MockGalleryApplicationVersionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationVersionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewListByGalleryApplicationPager mocks base method. +func (m *MockGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryApplicationPager", resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(clients.GalleryApplicationVersionsPager) + return ret0 +} + +// NewListByGalleryApplicationPager indicates an expected call of NewListByGalleryApplicationPager. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryApplicationPager", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).NewListByGalleryApplicationPager), resourceGroupName, galleryName, galleryApplicationName, options) +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 6a2cd83a..09953d9a 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -25,7 +25,9 @@ var ( ComputeDiskAccessPrivateEndpointConnection = shared.NewItemType(Azure, Compute, DiskAccessPrivateEndpointConnection) ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) - ComputeSharedGalleryApplicationVersion = shared.NewItemType(Azure, Compute, SharedGalleryApplicationVersion) + ComputeGalleryApplication = shared.NewItemType(Azure, Compute, GalleryApplication) + ComputeGalleryApplicationVersion = shared.NewItemType(Azure, Compute, GalleryApplicationVersion) + ComputeGallery = shared.NewItemType(Azure, Compute, Gallery) // Network item types NetworkVirtualNetwork = shared.NewItemType(Azure, Network, VirtualNetwork) diff --git a/sources/azure/shared/mocks/mock_gallery_application_versions_client.go b/sources/azure/shared/mocks/mock_gallery_application_versions_client.go new file mode 100644 index 00000000..e88ce82b --- /dev/null +++ b/sources/azure/shared/mocks/mock_gallery_application_versions_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gallery-application-versions-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_gallery_application_versions_client.go -package=mocks -source=gallery-application-versions-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryApplicationVersionsClient is a mock of GalleryApplicationVersionsClient interface. +type MockGalleryApplicationVersionsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationVersionsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationVersionsClientMockRecorder is the mock recorder for MockGalleryApplicationVersionsClient. +type MockGalleryApplicationVersionsClientMockRecorder struct { + mock *MockGalleryApplicationVersionsClient +} + +// NewMockGalleryApplicationVersionsClient creates a new mock instance. +func NewMockGalleryApplicationVersionsClient(ctrl *gomock.Controller) *MockGalleryApplicationVersionsClient { + mock := &MockGalleryApplicationVersionsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationVersionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationVersionsClient) EXPECT() *MockGalleryApplicationVersionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationVersionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) +} + +// NewListByGalleryApplicationPager mocks base method. +func (m *MockGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryApplicationPager", resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(clients.GalleryApplicationVersionsPager) + return ret0 +} + +// NewListByGalleryApplicationPager indicates an expected call of NewListByGalleryApplicationPager. +func (mr *MockGalleryApplicationVersionsClientMockRecorder) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryApplicationPager", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).NewListByGalleryApplicationPager), resourceGroupName, galleryName, galleryApplicationName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index dbb3ea47..a081da54 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -73,7 +73,9 @@ const ( DiskAccessPrivateEndpointConnection shared.Resource = "disk-access-private-endpoint-connection" SharedGalleryImage shared.Resource = "shared-gallery-image" CommunityGalleryImage shared.Resource = "community-gallery-image" - SharedGalleryApplicationVersion shared.Resource = "shared-gallery-application-version" + GalleryApplicationVersion shared.Resource = "gallery-application-version" + GalleryApplication shared.Resource = "gallery-application" + Gallery shared.Resource = "gallery" // Network resources VirtualNetwork shared.Resource = "virtual-network" diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index adb4746e..fdc634a5 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -22,12 +22,13 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-storage-blob-container": {"storageAccounts", "containers"}, "azure-storage-file-share": {"storageAccounts", "shares"}, "azure-storage-table": {"storageAccounts", "tables"}, - "azure-sql-database": {"servers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-SQL-SouthEastAsia/providers/Microsoft.Sql/servers/testsvr/databases/testdb", - "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-PostgreSQL-SouthEastAsia/providers/Microsoft.DBforPostgreSQL/flexibleServers/testsvr/databases/testdb", - "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", - "azure-authorization-role-assignment": {"roleAssignments"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}", - "azure-compute-virtual-machine-run-command": {"virtualMachines", "runCommands"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/runCommands/{runCommandName}", - "azure-compute-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", + "azure-sql-database": {"servers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-SQL-SouthEastAsia/providers/Microsoft.Sql/servers/testsvr/databases/testdb", + "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-PostgreSQL-SouthEastAsia/providers/Microsoft.DBforPostgreSQL/flexibleServers/testsvr/databases/testdb", + "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", + "azure-authorization-role-assignment": {"roleAssignments"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}", + "azure-compute-virtual-machine-run-command": {"virtualMachines", "runCommands"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/runCommands/{runCommandName}", + "azure-compute-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", + "azure-compute-gallery-application-version": {"galleries", "applications", "versions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}/versions/{versionName}", } if keys, ok := pathKeysMap[resourceType]; ok { @@ -406,20 +407,25 @@ func ExtractDNSFromURL(urlStr string) string { return urlStr } -// ExtractStorageAccountNameFromBlobURI extracts the storage account name from an Azure blob URI -// Blob URIs follow the format: https://{accountName}.blob.core.windows.net/{container}/{blob} +// ExtractStorageAccountNameFromBlobURI extracts the storage account name from an Azure blob URI. +// Blob URIs use the host format {accountName}.blob.core.{suffix} in all Azure clouds, e.g.: +// - Public: https://{accountName}.blob.core.windows.net/{container}/{blob} +// - China: https://{accountName}.blob.core.chinacloudapi.cn/... +// - US Government: https://{accountName}.blob.core.usgovcloudapi.net/... func ExtractStorageAccountNameFromBlobURI(blobURI string) string { if blobURI == "" { return "" } - parsedURL, err := url.Parse(blobURI) if err != nil { return "" } - host := parsedURL.Host - // Extract account name from hostname: {accountName}.blob.core.windows.net + // Accept any Azure blob endpoint (public and sovereign clouds); check host only to avoid matching path/query + if !strings.Contains(host, ".blob.core.") { + return "" + } + // Account name is the first label of the host in all Azure blob endpoints parts := strings.Split(host, ".") if len(parts) > 0 && parts[0] != "" { return parts[0] @@ -428,24 +434,21 @@ func ExtractStorageAccountNameFromBlobURI(blobURI string) string { return "" } -// ExtractContainerNameFromBlobURI extracts the container name from an Azure blob URI -// Blob URIs follow the format: https://{accountName}.blob.core.windows.net/{container}/{blob} -// Returns the first path segment which is the container name +// ExtractContainerNameFromBlobURI extracts the container name from an Azure blob URI. +// Blob URIs use the same path layout in all Azure clouds; the first path segment is the container. +// Returns the first path segment which is the container name. func ExtractContainerNameFromBlobURI(blobURI string) string { if blobURI == "" { return "" } - - // Defensive check: ensure this is actually a blob URI - if !strings.Contains(blobURI, ".blob.core.windows.net") { - return "" - } - parsedURL, err := url.Parse(blobURI) if err != nil { return "" } - + // Ensure this is an Azure blob host (public or sovereign cloud); check host only to avoid matching path/query + if !strings.Contains(parsedURL.Host, ".blob.core.") { + return "" + } path := strings.Trim(parsedURL.Path, "/") if path == "" { return "" diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index e90dc62e..4cf9bbb6 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -971,3 +971,56 @@ func TestExtractDNSFromURL(t *testing.T) { }) } } + +func TestExtractStorageAccountNameFromBlobURI(t *testing.T) { + tests := []struct { + name string + blobURI string + expected string + }{ + { + name: "valid blob URI", + blobURI: "https://mystorageaccount.blob.core.windows.net/container/blob", + expected: "mystorageaccount", + }, + { + name: "blob URI with path only", + blobURI: "https://account.blob.core.windows.net/packages/app.zip", + expected: "account", + }, + { + name: "sovereign cloud China blob URI", + blobURI: "https://myaccount.blob.core.chinacloudapi.cn/container/blob", + expected: "myaccount", + }, + { + name: "sovereign cloud US Government blob URI", + blobURI: "https://myaccount.blob.core.usgovcloudapi.net/container/blob", + expected: "myaccount", + }, + { + name: "non-blob HTTPS URL must return empty", + blobURI: "https://example.com/artifacts/app.zip", + expected: "", + }, + { + name: "non-blob HTTP URL must return empty", + blobURI: "http://cdn.example.com/foo", + expected: "", + }, + { + name: "empty URI", + blobURI: "", + expected: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := azureshared.ExtractStorageAccountNameFromBlobURI(tc.blobURI) + if actual != tc.expected { + t.Errorf("ExtractStorageAccountNameFromBlobURI(%q) = %q; want %q", tc.blobURI, actual, tc.expected) + } + }) + } +} From 608c14c3dd7acfbdfbec2763c667bbf7a26b50b7 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:03:51 +0000 Subject: [PATCH 07/51] Eng 2463 create dedicated adapter for google certificate manager certificate (#3853) > [!NOTE] > **Medium Risk** > Introduces new API calls and dependency surface (Certificate Manager) and changes the adapter initialization path, which could impact discovery behavior or startup if the new client fails to initialize. > > **Overview** > Adds a dedicated GCP manual adapter for **Certificate Manager Certificates**, supporting `Get` and location-scoped `Search`, Terraform ID mapping, and link generation to related DNS names plus Certificate Manager DNS authorization and issuance config resources. > > Wires the adapter into `sources/gcp/manual/adapters.go` by initializing a Certificate Manager API client and registering the new wrapper, and extends shared GCP metadata with new item/resource types, a client interface + generated mocks for testing, and the `roles/certificatemanager.viewer` predefined role; also updates `go.mod`/`go.sum` to include the `cloud.google.com/go/certificatemanager` dependency. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 318118d2228eab34c83072ff37f1b0b8e9e8270f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 525fc8c1a5ab977973dea3c76964eb190de6f0ed --- go.mod | 1 + go.sum | 2 + sources/gcp/manual/adapters.go | 19 +- .../manual/certificate-manager-certificate.go | 322 ++++++++++++++++++ .../certificate-manager-certificate_test.go | 220 ++++++++++++ .../gcp/shared/certificate-manager-clients.go | 40 +++ sources/gcp/shared/item-types.go | 5 + ..._certificate_manager_certificate_client.go | 122 +++++++ sources/gcp/shared/models.go | 5 + sources/gcp/shared/predefined-roles.go | 9 + 10 files changed, 740 insertions(+), 5 deletions(-) create mode 100644 sources/gcp/manual/certificate-manager-certificate.go create mode 100644 sources/gcp/manual/certificate-manager-certificate_test.go create mode 100644 sources/gcp/shared/certificate-manager-clients.go create mode 100644 sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go diff --git a/go.mod b/go.mod index 1438a278..4e96f31c 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( cloud.google.com/go/auth v0.18.1 cloud.google.com/go/bigquery v1.73.1 cloud.google.com/go/bigtable v1.42.0 + cloud.google.com/go/certificatemanager v1.9.6 cloud.google.com/go/compute v1.54.0 cloud.google.com/go/compute/metadata v0.9.0 cloud.google.com/go/container v1.46.0 diff --git a/go.sum b/go.sum index 39c77868..93b86f51 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ cloud.google.com/go/bigquery v1.73.1 h1:v//GZwdhtmCbZ87rOnxz7pectOGFS1GNRvrGTvLz cloud.google.com/go/bigquery v1.73.1/go.mod h1:KSLx1mKP/yGiA8U+ohSrqZM1WknUnjZAxHAQZ51/b1k= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= cloud.google.com/go/bigtable v1.42.0/go.mod h1:oZ30nofVB6/UYGg7lBwGLWSea7NZUvw/WvBBgLY07xU= +cloud.google.com/go/certificatemanager v1.9.6 h1:v5X8X+THKrS9OFZb6k0GRDP1WQxLXTdMko7OInBliw4= +cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index 8459ab54..d927173b 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -5,6 +5,7 @@ import ( "fmt" "cloud.google.com/go/bigquery" + certificatemanager "cloud.google.com/go/certificatemanager/apiv1" compute "cloud.google.com/go/compute/apiv1" iamAdmin "cloud.google.com/go/iam/admin/apiv1" logging "cloud.google.com/go/logging/apiv2" @@ -39,11 +40,12 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati instanceGroupManagerCli *compute.InstanceGroupManagersClient regionInstanceGroupManagerCli *compute.RegionInstanceGroupManagersClient diskCli *compute.DisksClient - iamServiceAccountKeyCli *iamAdmin.IamClient - iamServiceAccountCli *iamAdmin.IamClient - kmsLoader *shared.CloudKMSAssetLoader - bigQueryDatasetCli *bigquery.Client - loggingConfigCli *logging.ConfigClient + iamServiceAccountKeyCli *iamAdmin.IamClient + iamServiceAccountCli *iamAdmin.IamClient + certificateManagerCli *certificatemanager.Client + kmsLoader *shared.CloudKMSAssetLoader + bigQueryDatasetCli *bigquery.Client + loggingConfigCli *logging.ConfigClient nodeGroupCli *compute.NodeGroupsClient nodeTemplateCli *compute.NodeTemplatesClient regionBackendServiceCli *compute.RegionBackendServicesClient @@ -147,6 +149,12 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati return nil, fmt.Errorf("failed to create IAM service account client: %w", err) } + // Certificate Manager + certificateManagerCli, err = certificatemanager.NewClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create certificate manager client: %w", err) + } + // Extract project ID from projectLocations for BigQuery client initialization. // // IMPORTANT: The project ID passed to bigquery.NewClient() is used for: @@ -275,6 +283,7 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati sources.WrapperToAdapter(NewComputeSnapshot(shared.NewComputeSnapshotsClient(computeSnapshotCli), projectLocations), cache), sources.WrapperToAdapter(NewIAMServiceAccountKey(shared.NewIAMServiceAccountKeyClient(iamServiceAccountKeyCli), projectLocations), cache), sources.WrapperToAdapter(NewIAMServiceAccount(shared.NewIAMServiceAccountClient(iamServiceAccountCli), projectLocations), cache), + sources.WrapperToAdapter(NewCertificateManagerCertificate(shared.NewCertificateManagerCertificateClient(certificateManagerCli), projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSKeyRing(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSCryptoKey(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSCryptoKeyVersion(kmsLoader, projectLocations), cache), diff --git a/sources/gcp/manual/certificate-manager-certificate.go b/sources/gcp/manual/certificate-manager-certificate.go new file mode 100644 index 00000000..adeb568a --- /dev/null +++ b/sources/gcp/manual/certificate-manager-certificate.go @@ -0,0 +1,322 @@ +package manual + +import ( + "context" + "errors" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "google.golang.org/api/iterator" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + CertificateManagerCertificateLookupByLocation = shared.NewItemTypeLookup("location", gcpshared.CertificateManagerCertificate) + CertificateManagerCertificateLookupByName = shared.NewItemTypeLookup("name", gcpshared.CertificateManagerCertificate) +) + +type certificateManagerCertificateWrapper struct { + client gcpshared.CertificateManagerCertificateClient + *gcpshared.ProjectBase +} + +// NewCertificateManagerCertificate creates a new certificateManagerCertificateWrapper. +func NewCertificateManagerCertificate(client gcpshared.CertificateManagerCertificateClient, locations []gcpshared.LocationInfo) sources.SearchableWrapper { + return &certificateManagerCertificateWrapper{ + client: client, + ProjectBase: gcpshared.NewProjectBase( + locations, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + gcpshared.CertificateManagerCertificate, + ), + } +} + +func (c certificateManagerCertificateWrapper) IAMPermissions() []string { + return []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + } +} + +func (c certificateManagerCertificateWrapper) PredefinedRole() string { + return "roles/certificatemanager.viewer" +} + +func (c certificateManagerCertificateWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + gcpshared.CertificateManagerDnsAuthorization, + gcpshared.CertificateManagerCertificateIssuanceConfig, + stdlib.NetworkDNS, + ) +} + +func (c certificateManagerCertificateWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/certificate_manager_certificate + // ID format: projects/{{project}}/locations/{{location}}/certificates/{{name}} + // The framework automatically intercepts queries starting with "projects/" and converts + // them to GET operations by extracting the last N path parameters (based on GetLookups count). + TerraformQueryMap: "google_certificate_manager_certificate.id", + }, + } +} + +func (c certificateManagerCertificateWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + CertificateManagerCertificateLookupByLocation, + CertificateManagerCertificateLookupByName, + } +} + +// Get retrieves a Certificate Manager Certificate by its unique attribute (location|certificateName). +func (c certificateManagerCertificateWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + location, err := c.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + + if len(queryParts) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires exactly 2 query parts: location and certificate name", + } + } + + locationName := queryParts[0] + certificateName := queryParts[1] + + // Construct the full resource name + // Format: projects/{project}/locations/{location}/certificates/{certificate} + name := "projects/" + location.ProjectID + "/locations/" + locationName + "/certificates/" + certificateName + + req := &certificatemanagerpb.GetCertificateRequest{ + Name: name, + } + + certificate, getErr := c.client.GetCertificate(ctx, req) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, c.Type()) + } + + item, sdpErr := c.gcpCertificateToSDPItem(certificate, location) + if sdpErr != nil { + return nil, sdpErr + } + + return item, nil +} + +func (c certificateManagerCertificateWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + CertificateManagerCertificateLookupByLocation, + }, + } +} + +// Search searches Certificate Manager Certificates by location. +func (c certificateManagerCertificateWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + return gcpshared.CollectFromStream(ctx, func(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { + c.SearchStream(ctx, stream, cache, cacheKey, scope, queryParts...) + }) +} + +// SearchStream streams certificates matching the search criteria (location). +func (c certificateManagerCertificateWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + location, err := c.LocationFromScope(scope) + if err != nil { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + }) + return + } + + if len(queryParts) != 1 { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: location", + }) + return + } + + locationName := queryParts[0] + + // Construct the parent path + // Format: projects/{project}/locations/{location} + parent := "projects/" + location.ProjectID + "/locations/" + locationName + + req := &certificatemanagerpb.ListCertificatesRequest{ + Parent: parent, + } + + results := c.client.ListCertificates(ctx, req) + + for { + cert, iterErr := results.Next() + if errors.Is(iterErr, iterator.Done) { + break + } + if iterErr != nil { + stream.SendError(gcpshared.QueryError(iterErr, scope, c.Type())) + return + } + + item, sdpErr := c.gcpCertificateToSDPItem(cert, location) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } +} + +func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificate *certificatemanagerpb.Certificate, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(certificate, "labels") + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + // Extract location and certificate name from the resource name + // Format: projects/{project}/locations/{location}/certificates/{certificate} + values := gcpshared.ExtractPathParams(certificate.GetName(), "locations", "certificates") + if len(values) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "invalid certificate name format: " + certificate.GetName(), + } + } + + locationName := values[0] + certificateName := values[1] + + // Set composite unique attribute + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(locationName, certificateName)) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + sdpItem := &sdp.Item{ + Type: gcpshared.CertificateManagerCertificate.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: location.ToScope(), + Tags: certificate.GetLabels(), + } + + // Link to DNS names from sanDnsNames (covers both managed and self-managed certificates) + for _, dnsName := range certificate.GetSanDnsnames() { + if dnsName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + // Certificate depends on DNS resolution + // DNS changes affect certificate validity + In: true, + Out: true, + }, + }) + } + } + + // Link to DNS Authorizations used for managed certificate domain validation + if managed := certificate.GetManaged(); managed != nil { + // Link to DNS names from managed.domains + for _, domain := range managed.GetDomains() { + if domain != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: domain, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + // Certificate depends on DNS resolution for domain validation + // DNS changes affect certificate provisioning + In: true, + Out: true, + }, + }) + } + } + for _, dnsAuthURI := range managed.GetDnsAuthorizations() { + // Extract location and dnsAuthorization name from URI + // Format: projects/{project}/locations/{location}/dnsAuthorizations/{dnsAuthorization} + values := gcpshared.ExtractPathParams(dnsAuthURI, "locations", "dnsAuthorizations") + if len(values) == 2 && values[0] != "" && values[1] != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.CertificateManagerDnsAuthorization.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(values[0], values[1]), + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Certificate depends on DNS authorization for domain validation + // If DNS authorization is deleted, certificate provisioning fails + // Deleting certificate doesn't affect the DNS authorization + In: true, + Out: false, + }, + }) + } + } + + // Link to Certificate Issuance Config for private PKI certificates + if issuanceConfigURI := managed.GetIssuanceConfig(); issuanceConfigURI != "" { + // Extract location and issuanceConfig name from URI + // Format: projects/{project}/locations/{location}/certificateIssuanceConfigs/{certificateIssuanceConfig} + values := gcpshared.ExtractPathParams(issuanceConfigURI, "locations", "certificateIssuanceConfigs") + if len(values) == 2 && values[0] != "" && values[1] != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.CertificateManagerCertificateIssuanceConfig.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(values[0], values[1]), + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Certificate depends on issuance config for private PKI + // If issuance config is deleted, certificate provisioning fails + // Deleting certificate doesn't affect the issuance config + In: true, + Out: false, + }, + }) + } + } + } + + // Note: The Certificate resource's UsedBy field (which lists resources using this certificate) + // is not available in the Go SDK protobuf. The reverse links from CertificateMap, + // CertificateMapEntry, and TargetHttpsProxy to Certificate will be established + // when those adapters are created. + + return sdpItem, nil +} diff --git a/sources/gcp/manual/certificate-manager-certificate_test.go b/sources/gcp/manual/certificate-manager-certificate_test.go new file mode 100644 index 00000000..2b2d23b6 --- /dev/null +++ b/sources/gcp/manual/certificate-manager-certificate_test.go @@ -0,0 +1,220 @@ +package manual_test + +import ( + "context" + "testing" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "go.uber.org/mock/gomock" + "google.golang.org/api/iterator" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/gcp/manual" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/gcp/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func createCertificate(projectID, location, name string) *certificatemanagerpb.Certificate { + return &certificatemanagerpb.Certificate{ + Name: "projects/" + projectID + "/locations/" + location + "/certificates/" + name, + Description: "Test certificate", + CreateTime: timestamppb.Now(), + UpdateTime: timestamppb.Now(), + Labels: map[string]string{ + "env": "test", + }, + Scope: certificatemanagerpb.Certificate_DEFAULT, + } +} + +func TestCertificateManagerCertificate(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockCertificateManagerCertificateClient(ctrl) + projectID := "test-project-id" + location := "us-central1" + certificateName := "test-certificate" + + t.Run("Get", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + mockClient.EXPECT().GetCertificate(ctx, gomock.Any()).Return(createCertificate(projectID, location, certificateName), nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], location+shared.QuerySeparator+certificateName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatal("Expected item, got nil") + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + // Verify the item type + if sdpItem.GetType() != gcpshared.CertificateManagerCertificate.String() { + t.Errorf("Expected type %s, got: %s", gcpshared.CertificateManagerCertificate.String(), sdpItem.GetType()) + } + + // Verify the unique attribute + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got: %s", sdpItem.GetUniqueAttribute()) + } + + // Verify the scope + expectedScope := projectID + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got: %s", expectedScope, sdpItem.GetScope()) + } + }) + + t.Run("Search", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + mockIterator := mocks.NewMockCertificateIterator(ctrl) + + mockIterator.EXPECT().Next().Return(createCertificate(projectID, location, "cert1"), nil) + mockIterator.EXPECT().Next().Return(createCertificate(projectID, location, "cert2"), nil) + mockIterator.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().ListCertificates(ctx, gomock.Any()).Return(mockIterator) + + // Check if adapter supports searching + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], location, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + expectedCount := 2 + actualCount := len(sdpItems) + if actualCount != expectedCount { + t.Fatalf("Expected %d items, got: %d", expectedCount, actualCount) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + } + }) + + t.Run("GetLookups", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + lookups := wrapper.GetLookups() + if len(lookups) != 2 { + t.Errorf("Expected 2 lookups, got: %d", len(lookups)) + } + + // Verify the lookup types + expectedTypes := []string{"location", "name"} + for i, lookup := range lookups { + if lookup.By != expectedTypes[i] { + t.Errorf("Expected lookup by %s, got: %s", expectedTypes[i], lookup.By) + } + } + }) + + t.Run("SearchLookups", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + searchLookups := wrapper.SearchLookups() + if len(searchLookups) != 1 { + t.Errorf("Expected 1 search lookup, got: %d", len(searchLookups)) + } + + // Verify the search lookup has only location + if len(searchLookups[0]) != 1 { + t.Errorf("Expected 1 lookup in search lookup, got: %d", len(searchLookups[0])) + } + }) + + t.Run("TerraformMappings", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + mappings := wrapper.TerraformMappings() + if len(mappings) != 1 { + t.Errorf("Expected 1 terraform mapping, got: %d", len(mappings)) + } + + mapping := mappings[0] + if mapping.GetTerraformMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Expected SEARCH method, got: %v", mapping.GetTerraformMethod()) + } + + expectedQueryMap := "google_certificate_manager_certificate.id" + if mapping.GetTerraformQueryMap() != expectedQueryMap { + t.Errorf("Expected query map %s, got: %s", expectedQueryMap, mapping.GetTerraformQueryMap()) + } + }) + + t.Run("IAMPermissions", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + permissions := wrapper.IAMPermissions() + expectedPermissions := []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + } + + if len(permissions) != len(expectedPermissions) { + t.Errorf("Expected %d permissions, got: %d", len(expectedPermissions), len(permissions)) + } + + for i, perm := range permissions { + if perm != expectedPermissions[i] { + t.Errorf("Expected permission %s, got: %s", expectedPermissions[i], perm) + } + } + }) + + t.Run("PredefinedRole", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + // PredefinedRole is available on the wrapper, not the adapter + role := wrapper.(interface{ PredefinedRole() string }).PredefinedRole() + expectedRole := "roles/certificatemanager.viewer" + if role != expectedRole { + t.Errorf("Expected role %s, got: %s", expectedRole, role) + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + + links := wrapper.PotentialLinks() + expectedLinks := map[shared.ItemType]bool{ + gcpshared.CertificateManagerDnsAuthorization: true, + gcpshared.CertificateManagerCertificateIssuanceConfig: true, + stdlib.NetworkDNS: true, + } + + if len(links) != len(expectedLinks) { + t.Errorf("Expected %d potential links, got: %d", len(expectedLinks), len(links)) + } + + for expectedLink := range expectedLinks { + if !links[expectedLink] { + t.Errorf("Expected link to %s", expectedLink) + } + } + }) +} diff --git a/sources/gcp/shared/certificate-manager-clients.go b/sources/gcp/shared/certificate-manager-clients.go new file mode 100644 index 00000000..1aff5b95 --- /dev/null +++ b/sources/gcp/shared/certificate-manager-clients.go @@ -0,0 +1,40 @@ +package shared + +//go:generate mockgen -destination=./mocks/mock_certificate_manager_certificate_client.go -package=mocks -source=certificate-manager-clients.go + +import ( + "context" + + certificatemanager "cloud.google.com/go/certificatemanager/apiv1" + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + "github.com/googleapis/gax-go/v2" +) + +// CertificateManagerCertificateClient interface for Certificate Manager Certificate operations +type CertificateManagerCertificateClient interface { + GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...gax.CallOption) (*certificatemanagerpb.Certificate, error) + ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...gax.CallOption) CertificateIterator +} + +type CertificateIterator interface { + Next() (*certificatemanagerpb.Certificate, error) +} + +type certificateManagerCertificateClient struct { + client *certificatemanager.Client +} + +func (c *certificateManagerCertificateClient) GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...gax.CallOption) (*certificatemanagerpb.Certificate, error) { + return c.client.GetCertificate(ctx, req, opts...) +} + +func (c *certificateManagerCertificateClient) ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...gax.CallOption) CertificateIterator { + return c.client.ListCertificates(ctx, req, opts...) +} + +// NewCertificateManagerCertificateClient creates a new CertificateManagerCertificateClient +func NewCertificateManagerCertificateClient(client *certificatemanager.Client) CertificateManagerCertificateClient { + return &certificateManagerCertificateClient{ + client: client, + } +} diff --git a/sources/gcp/shared/item-types.go b/sources/gcp/shared/item-types.go index 434637b0..0ebcd2a7 100644 --- a/sources/gcp/shared/item-types.go +++ b/sources/gcp/shared/item-types.go @@ -173,6 +173,7 @@ var ( ComputeInterconnectAttachment = shared.NewItemType(GCP, Compute, InterconnectAttachment) ComputeServiceAttachment = shared.NewItemType(GCP, Compute, ServiceAttachment) ComputeTargetHttpsProxy = shared.NewItemType(GCP, Compute, TargetHttpsProxy) + ComputeRegionTargetHttpsProxy = shared.NewItemType(GCP, Compute, RegionTargetHttpsProxy) ComputeSSLPolicy = shared.NewItemType(GCP, Compute, SSLPolicy) ComputeTargetHttpProxy = shared.NewItemType(GCP, Compute, TargetHttpProxy) ComputeTargetTcpProxy = shared.NewItemType(GCP, Compute, TargetTcpProxy) @@ -191,6 +192,10 @@ var ( FileInstance = shared.NewItemType(GCP, File, Instance) FileBackup = shared.NewItemType(GCP, File, Backup) CertificateManagerCertificateMap = shared.NewItemType(GCP, CertificateManager, CertificateMap) + CertificateManagerCertificateMapEntry = shared.NewItemType(GCP, CertificateManager, CertificateMapEntry) + CertificateManagerCertificate = shared.NewItemType(GCP, CertificateManager, Certificate) + CertificateManagerDnsAuthorization = shared.NewItemType(GCP, CertificateManager, DnsAuthorization) + CertificateManagerCertificateIssuanceConfig = shared.NewItemType(GCP, CertificateManager, CertificateIssuanceConfig) ComputeRoutePolicy = shared.NewItemType(GCP, Compute, RoutePolicy) // Router Route Policy child resource ComputeBgpRoute = shared.NewItemType(GCP, Compute, BgpRoute) // Router BGP Route child resource NetworkServicesMesh = shared.NewItemType(GCP, NetworkServices, Mesh) // https://cloud.google.com/service-mesh/docs/reference/network-services/rest/v1/projects.locations.meshes/get diff --git a/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go b/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go new file mode 100644 index 00000000..e271e814 --- /dev/null +++ b/sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go @@ -0,0 +1,122 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./sources/gcp/shared/certificate-manager-clients.go +// +// Generated by this command: +// +// mockgen -destination=./sources/gcp/shared/mocks/mock_certificate_manager_certificate_client.go -package=mocks -source=./sources/gcp/shared/certificate-manager-clients.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" + v2 "github.com/googleapis/gax-go/v2" + shared "github.com/overmindtech/cli/sources/gcp/shared" + gomock "go.uber.org/mock/gomock" +) + +// MockCertificateManagerCertificateClient is a mock of CertificateManagerCertificateClient interface. +type MockCertificateManagerCertificateClient struct { + ctrl *gomock.Controller + recorder *MockCertificateManagerCertificateClientMockRecorder + isgomock struct{} +} + +// MockCertificateManagerCertificateClientMockRecorder is the mock recorder for MockCertificateManagerCertificateClient. +type MockCertificateManagerCertificateClientMockRecorder struct { + mock *MockCertificateManagerCertificateClient +} + +// NewMockCertificateManagerCertificateClient creates a new mock instance. +func NewMockCertificateManagerCertificateClient(ctrl *gomock.Controller) *MockCertificateManagerCertificateClient { + mock := &MockCertificateManagerCertificateClient{ctrl: ctrl} + mock.recorder = &MockCertificateManagerCertificateClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCertificateManagerCertificateClient) EXPECT() *MockCertificateManagerCertificateClientMockRecorder { + return m.recorder +} + +// GetCertificate mocks base method. +func (m *MockCertificateManagerCertificateClient) GetCertificate(ctx context.Context, req *certificatemanagerpb.GetCertificateRequest, opts ...v2.CallOption) (*certificatemanagerpb.Certificate, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetCertificate", varargs...) + ret0, _ := ret[0].(*certificatemanagerpb.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCertificate indicates an expected call of GetCertificate. +func (mr *MockCertificateManagerCertificateClientMockRecorder) GetCertificate(ctx, req any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificate", reflect.TypeOf((*MockCertificateManagerCertificateClient)(nil).GetCertificate), varargs...) +} + +// ListCertificates mocks base method. +func (m *MockCertificateManagerCertificateClient) ListCertificates(ctx context.Context, req *certificatemanagerpb.ListCertificatesRequest, opts ...v2.CallOption) shared.CertificateIterator { + m.ctrl.T.Helper() + varargs := []any{ctx, req} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListCertificates", varargs...) + ret0, _ := ret[0].(shared.CertificateIterator) + return ret0 +} + +// ListCertificates indicates an expected call of ListCertificates. +func (mr *MockCertificateManagerCertificateClientMockRecorder) ListCertificates(ctx, req any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, req}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificates", reflect.TypeOf((*MockCertificateManagerCertificateClient)(nil).ListCertificates), varargs...) +} + +// MockCertificateIterator is a mock of CertificateIterator interface. +type MockCertificateIterator struct { + ctrl *gomock.Controller + recorder *MockCertificateIteratorMockRecorder + isgomock struct{} +} + +// MockCertificateIteratorMockRecorder is the mock recorder for MockCertificateIterator. +type MockCertificateIteratorMockRecorder struct { + mock *MockCertificateIterator +} + +// NewMockCertificateIterator creates a new mock instance. +func NewMockCertificateIterator(ctrl *gomock.Controller) *MockCertificateIterator { + mock := &MockCertificateIterator{ctrl: ctrl} + mock.recorder = &MockCertificateIteratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCertificateIterator) EXPECT() *MockCertificateIteratorMockRecorder { + return m.recorder +} + +// Next mocks base method. +func (m *MockCertificateIterator) Next() (*certificatemanagerpb.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Next") + ret0, _ := ret[0].(*certificatemanagerpb.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Next indicates an expected call of Next. +func (mr *MockCertificateIteratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockCertificateIterator)(nil).Next)) +} diff --git a/sources/gcp/shared/models.go b/sources/gcp/shared/models.go index b897d26e..23d62ca5 100644 --- a/sources/gcp/shared/models.go +++ b/sources/gcp/shared/models.go @@ -194,6 +194,7 @@ const ( InterconnectAttachment shared.Resource = "interconnect-attachment" ServiceAttachment shared.Resource = "service-attachment" TargetHttpsProxy shared.Resource = "target-https-proxy" + RegionTargetHttpsProxy shared.Resource = "region-target-https-proxy" SSLPolicy shared.Resource = "ssl-policy" TargetHttpProxy shared.Resource = "target-http-proxy" TargetTcpProxy shared.Resource = "target-tcp-proxy" @@ -216,6 +217,10 @@ const ( EffectiveSecurityHealthAnalyticsCustomModule shared.Resource = "effective-security-health-analytics-custom-module" // Security Center Management Effective Security Health Analytics Custom Module EffectiveEventThreatDetectionCustomModule shared.Resource = "effective-event-threat-detection-custom-module" // Security Center Management Effective Event Threat Detection Custom Module CertificateMap shared.Resource = "certificate-map" // Certificate Manager Certificate Map + CertificateMapEntry shared.Resource = "certificate-map-entry" // Certificate Manager Certificate Map Entry + Certificate shared.Resource = "certificate" // Certificate Manager Certificate + DnsAuthorization shared.Resource = "dns-authorization" // Certificate Manager DNS Authorization + CertificateIssuanceConfig shared.Resource = "certificate-issuance-config" // Certificate Manager Certificate Issuance Config InternalRange shared.Resource = "internal-range" // Network Connectivity API Internal Range RoutePolicy shared.Resource = "route-policy" // Router Route Policy child resource BgpRoute shared.Resource = "bgp-route" // Router BGP Route child resource diff --git a/sources/gcp/shared/predefined-roles.go b/sources/gcp/shared/predefined-roles.go index e1778af1..4d481652 100644 --- a/sources/gcp/shared/predefined-roles.go +++ b/sources/gcp/shared/predefined-roles.go @@ -83,6 +83,15 @@ var PredefinedRoles = map[string]role{ "bigtable.backups.list", }, }, + "roles/certificatemanager.viewer": { + Role: "roles/certificatemanager.viewer", + // Read-only access to Certificate Manager resources. + Link: "https://cloud.google.com/iam/docs/roles-permissions/certificatemanager#certificatemanager.viewer", + IAMPermissions: []string{ + "certificatemanager.certs.get", + "certificatemanager.certs.list", + }, + }, "roles/cloudfunctions.viewer": { Role: "roles/cloudfunctions.viewer", // Read-only access to functions and locations. From e118fd3c27e1715a0b444b4ac5d85b2d6f99cf6e Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Thu, 12 Feb 2026 10:43:23 +0100 Subject: [PATCH 08/51] [Stdlib + AWS] Remove BlastPropagation from stdlib and AWS adapters (#3835) > [!NOTE] > **Medium Risk** > Removes blast-radius directionality metadata from many link edges, which could change impact analysis or traversal behavior if no equivalent default/alternative exists. > > **Overview** > **Removes `BlastPropagation` metadata from all `LinkedItemQuery` definitions** across AWS API Gateway adapters (`apigateway-api-key`, `apigateway-stage`) and stdlib adapters (HTTP/DNS/IP/Certificate + RDAP + test fixtures). > > Link creation behavior (which items query/link to) is unchanged, but blast-radius directionality is no longer expressed at the adapter layer, implying downstream graph/blast computations must now rely on defaults or a different mechanism. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3735c1afb5956744d65156bf210a22537fa8c91d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: ec1a2a9dc20aa1d3fdacbc2daf7ed61eeaa510ae --- sources/aws/apigateway-api-key.go | 4 -- sources/aws/apigateway-stage.go | 8 ---- stdlib-source/adapters/certificate.go | 6 --- stdlib-source/adapters/dns.go | 15 -------- stdlib-source/adapters/http.go | 21 ---------- stdlib-source/adapters/ip.go | 10 ----- stdlib-source/adapters/main.go | 6 --- stdlib-source/adapters/rdap-domain.go | 11 ------ stdlib-source/adapters/rdap-entity.go | 7 ---- stdlib-source/adapters/rdap-nameserver.go | 10 ----- stdlib-source/adapters/test/data.go | 47 ----------------------- 11 files changed, 145 deletions(-) diff --git a/sources/aws/apigateway-api-key.go b/sources/aws/apigateway-api-key.go index cf3e5a78..185ebc1e 100644 --- a/sources/aws/apigateway-api-key.go +++ b/sources/aws/apigateway-api-key.go @@ -142,10 +142,6 @@ func (d *apiGatewayKeyWrapper) awsToSdpItem(apiKey types.ApiKey, scope string) ( Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/aws/apigateway-stage.go b/sources/aws/apigateway-stage.go index 8d9fbb32..2a0b34e8 100644 --- a/sources/aws/apigateway-stage.go +++ b/sources/aws/apigateway-stage.go @@ -167,10 +167,6 @@ func (d *apiGatewayStageWrapper) awsToSdpItem(stage types.Stage, scope, query st Query: restAPIID + "/" + *stage.DeploymentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -182,10 +178,6 @@ func (d *apiGatewayStageWrapper) awsToSdpItem(stage types.Stage, scope, query st Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) return item, nil diff --git a/stdlib-source/adapters/certificate.go b/stdlib-source/adapters/certificate.go index 27dbf6cd..b072dab8 100644 --- a/stdlib-source/adapters/certificate.go +++ b/stdlib-source/adapters/certificate.go @@ -252,12 +252,6 @@ func (s *CertificateAdapter) Search(ctx context.Context, scope string, query str Query: cert.Issuer.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing issuer will affect the child - In: true, - // The child can't affect the issuer - Out: false, - }, }) } } diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index b3463ee4..0269254c 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -486,10 +486,6 @@ func (d *DNSAdapter) makeQueryImpl(ctx context.Context, query string, server str Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -583,11 +579,6 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -621,12 +612,6 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the domain will affect the DNS entry - In: true, - // Changes to the DNS entry won't affect the domain - Out: false, - }, }) return &item, nil diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index e2971503..dae3d8e5 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -274,11 +274,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always linked - In: true, - Out: true, - }, }) } else { // If the host is not an ip, try to resolve via DNS @@ -289,11 +284,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: req.URL.Hostname(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } @@ -340,12 +330,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: strings.Join(certs, "\n"), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the cert will affect the HTTP endpoint - In: true, - // The HTTP endpoint won't affect the cert - Out: false, - }, }) } } @@ -391,11 +375,6 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor Query: resolvedURL.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Redirects are tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/stdlib-source/adapters/ip.go b/stdlib-source/adapters/ip.go index 9dcac1ef..0ac8feb4 100644 --- a/stdlib-source/adapters/ip.go +++ b/stdlib-source/adapters/ip.go @@ -159,11 +159,6 @@ func (bc *IPAdapter) Get(ctx context.Context, scope string, query string, ignore Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }, { // RDAP @@ -173,11 +168,6 @@ func (bc *IPAdapter) Get(ctx context.Context, scope string, query string, ignore Query: ip.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }, }, }, nil diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index 23287f3e..3f2eb9da 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -131,12 +131,6 @@ func extractEntityLinks(entities []rdap.Entity) []*sdp.LinkedItemQuery { Query: selfLink, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // The Entity isn't a "real" component, so no matter what - // changes it won't actually "affect" anything - In: false, - Out: false, - }, }) } } diff --git a/stdlib-source/adapters/rdap-domain.go b/stdlib-source/adapters/rdap-domain.go index 5e61b8bf..00ace4b4 100644 --- a/stdlib-source/adapters/rdap-domain.go +++ b/stdlib-source/adapters/rdap-domain.go @@ -197,12 +197,6 @@ func (s *RdapDomainAdapter) Search(ctx context.Context, scope string, query stri Query: newURL.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // A change in a name server could affect the domains - In: true, - // Domains won't affect the name server - Out: false, - }, }) } @@ -221,11 +215,6 @@ func (s *RdapDomainAdapter) Search(ctx context.Context, scope string, query stri Query: network.StartAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } diff --git a/stdlib-source/adapters/rdap-entity.go b/stdlib-source/adapters/rdap-entity.go index a986230c..13adc8de 100644 --- a/stdlib-source/adapters/rdap-entity.go +++ b/stdlib-source/adapters/rdap-entity.go @@ -188,13 +188,6 @@ func (s *RdapEntityAdapter) runEntityRequest(ctx context.Context, query string, Query: autnum.Handle, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The ASN won't affect the entity - In: false, - // The entity could maybe affect the ASN? Change this if it - // causes issues - Out: true, - }, }) } diff --git a/stdlib-source/adapters/rdap-nameserver.go b/stdlib-source/adapters/rdap-nameserver.go index faa46d6a..414c8bd2 100644 --- a/stdlib-source/adapters/rdap-nameserver.go +++ b/stdlib-source/adapters/rdap-nameserver.go @@ -177,11 +177,6 @@ func (s *RdapNameserverAdapter) Search(ctx context.Context, scope string, query Query: strings.ToLower(nameserver.LDHName), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These represent the same thing so linked them both ways - In: true, - Out: true, - }, }) // Link IP addresses @@ -196,11 +191,6 @@ func (s *RdapNameserverAdapter) Search(ctx context.Context, scope string, query Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } diff --git a/stdlib-source/adapters/test/data.go b/stdlib-source/adapters/test/data.go index ca1567df..140ed090 100644 --- a/stdlib-source/adapters/test/data.go +++ b/stdlib-source/adapters/test/data.go @@ -100,11 +100,6 @@ func admins() *sdp.Item { Query: "test-dylan", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // the show must go on - In: false, - Out: false, - }, }, } @@ -121,11 +116,6 @@ func dylan() *sdp.Item { Method: sdp.QueryMethod_LIST, Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // best friends - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -134,12 +124,6 @@ func dylan() *sdp.Item { Query: "test-motorcycling", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // accidents happen - In: true, - // motorcycles will endure - Out: false, - }, }, { Query: &sdp.Query{ @@ -148,12 +132,6 @@ func dylan() *sdp.Item { Query: "test-london", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // we are what we eat - In: true, - // london don't care - Out: false, - }, }, } @@ -171,12 +149,6 @@ func manny() *sdp.Item { Query: "test-london", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // we are what we eat - In: true, - // london don't care - Out: false, - }, }, { Query: &sdp.Query{ @@ -185,12 +157,6 @@ func manny() *sdp.Item { Query: "test-kibble", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // there are other options - In: false, - // the kibble is soon gone - Out: true, - }, }, } @@ -219,11 +185,6 @@ func london() *sdp.Item { Query: "test-gb", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - // politics, enough said - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -232,10 +193,6 @@ func london() *sdp.Item { Query: "*", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: false, - }, }, { Query: &sdp.Query{ @@ -244,10 +201,6 @@ func london() *sdp.Item { Query: "test-soho", Scope: "test", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } From 71d819999d2e2e2f39abbe2c884ca1d924e4b152 Mon Sep 17 00:00:00 2001 From: jameslaneovermind <122231433+jameslaneovermind@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:42:25 +0000 Subject: [PATCH 09/51] Add IAM Terraform mappings for Pub/Sub subscription and topic adapters (ENG-2459, ENG-2460) (#3858) ## Summary _As per discussions yesterday this is the first part of Option C to add the mappings immediately._ - Adds Terraform mappings so `google_pubsub_subscription_iam_binding`, `_iam_member`, and `_iam_policy` resolve to the parent Pub/Sub Subscription during change analysis (instead of being skipped as "Unsupported") - Same for `google_pubsub_topic_iam_binding`, `_iam_member`, and `_iam_policy` resolving to the parent Topic - Adds regression tests verifying these and other critical Terraform mappings remain registered in adapter metadata Covers **ENG-2459** and **ENG-2460**. ## Context and design decision GCP IAM binding resources (`google_*_iam_binding`, `_iam_member`, `_iam_policy`) are Terraform-only constructs. There is no standalone GCP API to get or list individual IAM bindings -- the actual API is `getIamPolicy` on the parent resource, which returns the full policy. We evaluated two approaches: ### Approach 1: Interim Terraform mapping to parent resource (this PR) Add `TerraformQueryMap` entries to the existing adapter so that IAM changes resolve to the parent resource. When `google_pubsub_subscription_iam_binding` appears in a plan, the CLI extracts the `subscription` attribute and queries the existing PubSubSubscription adapter. - **Effort**: ~20 lines per adapter, ~120 lines of tests - **Time**: ~1 hour total for both adapters - **Benefit**: Immediately resolves the "Unsupported/Skipped" status in the UI; blast radius analysis works from the parent resource - **Limitation**: The LLM sees "a subscription changed" rather than "an IAM binding on a subscription changed"; no IAM-specific observations (e.g. which service accounts are affected) ### Approach 2: Dedicated IAM binding adapter (future, separate tickets) Build a proper manual adapter with its own item type (`PubSubSubscriptionIAMBinding`), calling the `getIamPolicy` API, with blast propagation linking to specific `IAMServiceAccount` items. - **Effort per adapter**: ~450-550 lines (client interface, adapter, tests) - **Time per adapter**: ~1-1.5 days - **Complication**: `roles/pubsub.viewer` does not include `pubsub.subscriptions.getIamPolicy` -- would require updating the Overmind custom IAM role across `deploy/sources.tf`, documentation, and customer-facing scripts - **Benefit**: Richer, IAM-specific observations (e.g. "service account X will lose `roles/storage.objectViewer` on this subscription") **Decision**: Ship Approach 1 now for immediate customer value. Approach 2 remains in the dedicated adapter tickets (ENG-2459, ENG-2460, ENG-2461) for a future sprint when IAM permission infrastructure can be scoped properly. This follows the same pattern AWS uses -- `aws_s3_bucket_policy.bucket`, `aws_s3_bucket_acl.bucket` etc. all map to the parent S3 bucket adapter. --- > [!NOTE] > **Low Risk** > Metadata-only Terraform mapping additions plus tests; no runtime GCP API behavior or data handling changes beyond improved plan-to-item resolution. > > **Overview** > Pub/Sub IAM-only Terraform resources (e.g. `google_pubsub_*_iam_binding/member/policy`) are now mapped to their parent `PubSubTopic`/`PubSubSubscription` during Terraform plan change analysis, so IAM changes participate in blast radius/risk analysis instead of showing as *Unsupported*. > > Adds adapter-level mappings for both topic and subscription, plus new tests that assert these IAM mappings (and a small set of other customer-critical Terraform mappings) remain registered and correctly parse `TerraformQueryMap` entries. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d85197ec05f980cae0c3255109bd1cd651931a9a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor GitOrigin-RevId: 53bc03d09c99b3bdae7660b0809b631702bafc4e --- .../dynamic/adapters/pubsub-subscription.go | 22 ++ .../adapters/pubsub-subscription_test.go | 119 +++++++++++ sources/gcp/dynamic/adapters/pubsub-topic.go | 20 ++ sources/gcp/proc/proc_test.go | 195 ++++++++++++++++++ 4 files changed, 356 insertions(+) diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index 3e277006..9bab54fb 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -67,6 +67,28 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_pubsub_subscription.name", }, + // IAM resources for Pub/Sub Subscriptions. These are Terraform-only + // constructs (no standalone GCP API resource exists for them). When an + // IAM binding/member/policy changes in a Terraform plan, we resolve it + // to the parent subscription so that blast radius analysis can show the + // downstream impact of the access change. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_subscription_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_binding.subscription", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_member.subscription", + }, + { + // Authoritative for the entire IAM policy on the subscription. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_subscription_iam_policy.subscription", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index 61690e67..ed52fdbc 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "testing" "google.golang.org/api/pubsub/v1" @@ -218,3 +219,121 @@ func TestPubSubSubscription(t *testing.T) { } }) } + +// TestPubSubSubscriptionIAMTerraformMappings verifies that the IAM Terraform resource +// types (iam_binding, iam_member, iam_policy) are registered as terraform mappings on +// the PubSub Subscription adapter. This is critical because these Terraform-only +// resources don't have their own GCP API — they represent IAM policy changes on the +// parent subscription. Without these mappings, IAM changes would show as "Unsupported" +// in the change analysis UI instead of being resolved to the parent subscription for +// blast radius analysis. +// +// Background: google_pubsub_subscription_iam_binding is an authoritative Terraform +// resource that manages a single role's members on a subscription. When it changes, +// we need to resolve it to the affected subscription so customers see the downstream +// impact (e.g. services that read from the subscription losing access). +func TestPubSubSubscriptionIAMTerraformMappings(t *testing.T) { + // Retrieve the terraform mappings registered for PubSubSubscription + tfMapping, ok := gcpshared.SDPAssetTypeToTerraformMappings[gcpshared.PubSubSubscription] + if !ok { + t.Fatal("Expected PubSubSubscription to have terraform mappings registered, but none were found") + } + + // Build a lookup of terraform type -> query field from the registered mappings. + // This mirrors the logic in cli/tfutils/plan_mapper.go that splits + // TerraformQueryMap on "." to get the terraform type and attribute name. + type mappingInfo struct { + terraformType string + queryField string + method sdp.QueryMethod + } + registeredMappings := make([]mappingInfo, 0, len(tfMapping.Mappings)) + for _, m := range tfMapping.Mappings { + parts := strings.SplitN(m.GetTerraformQueryMap(), ".", 2) + if len(parts) != 2 { + t.Errorf("Invalid TerraformQueryMap format: %q (expected 'type.attribute')", m.GetTerraformQueryMap()) + continue + } + registeredMappings = append(registeredMappings, mappingInfo{ + terraformType: parts[0], + queryField: parts[1], + method: m.GetTerraformMethod(), + }) + } + + // Define the IAM terraform types we expect to be mapped, along with the + // Terraform attribute that identifies the parent subscription. + // All three IAM resource types use "subscription" as the attribute that + // contains the subscription name. + expectedIAMMappings := []struct { + terraformType string + queryField string + method sdp.QueryMethod + description string // documents why this mapping exists, for reviewer clarity + }{ + { + terraformType: "google_pubsub_subscription_iam_binding", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Authoritative for a given role — maps to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_member", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Non-authoritative single member — maps to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_policy", + queryField: "subscription", + method: sdp.QueryMethod_GET, + description: "Authoritative for full IAM policy — maps to parent subscription for blast radius", + }, + } + + for _, expected := range expectedIAMMappings { + t.Run(expected.terraformType, func(t *testing.T) { + found := false + for _, registered := range registeredMappings { + if registered.terraformType == expected.terraformType { + found = true + + if registered.queryField != expected.queryField { + t.Errorf("Terraform type %s: expected query field %q, got %q", + expected.terraformType, expected.queryField, registered.queryField) + } + + if registered.method != expected.method { + t.Errorf("Terraform type %s: expected method %s, got %s", + expected.terraformType, expected.method, registered.method) + } + break + } + } + + if !found { + t.Errorf("Terraform type %s is not registered as a mapping on PubSubSubscription. "+ + "This means %q changes will show as 'Unsupported' in the change analysis UI. "+ + "Purpose: %s", + expected.terraformType, expected.terraformType, expected.description) + } + }) + } + + // Also verify the base subscription mapping still exists (sanity check) + t.Run("google_pubsub_subscription", func(t *testing.T) { + found := false + for _, registered := range registeredMappings { + if registered.terraformType == "google_pubsub_subscription" { + found = true + if registered.queryField != "name" { + t.Errorf("Expected query field 'name' for google_pubsub_subscription, got %q", registered.queryField) + } + break + } + } + if !found { + t.Error("Base terraform mapping for google_pubsub_subscription is missing — this would break all subscription change analysis") + } + }) +} diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index e611bed4..4a739305 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -93,6 +93,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_pubsub_topic.name", }, + // IAM resources for Pub/Sub Topics. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent topic for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/pubsub_topic_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_binding.topic", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_member.topic", + }, + { + // Authoritative for the entire IAM policy on the topic. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_pubsub_topic_iam_policy.topic", + }, }, }, }.Register() diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index cf1bf3bd..10b0abc2 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "sort" + "strings" "sync" "sync/atomic" "testing" @@ -676,3 +677,197 @@ func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr)))) } + +// TestCriticalTerraformMappingsRegistered verifies that customer-critical Terraform +// resource types are correctly registered in the adapter metadata. This test mirrors +// the mapping table construction in cli/tfutils/plan_mapper.go — it loads all +// registered adapter metadata, parses TerraformQueryMap entries, and checks that +// each critical Terraform type resolves to the expected Overmind item type. +// +// If this test fails, the affected Terraform resources will show as "Unsupported" +// (skipped) in the change analysis UI, meaning no blast radius or risk analysis. +func TestCriticalTerraformMappingsRegistered(t *testing.T) { + // Build the mapping table from all registered adapter metadata, exactly as + // cli/tfutils/plan_mapper.go does at lines 168-190 + type tfMapEntry struct { + overmindType string + method sdp.QueryMethod + queryField string + } + mappings := make(map[string][]tfMapEntry) + for _, metadata := range Metadata.AllAdapterMetadata() { + if metadata.GetType() == "" { + continue + } + for _, mapping := range metadata.GetTerraformMappings() { + subs := strings.SplitN(mapping.GetTerraformQueryMap(), ".", 2) + if len(subs) != 2 { + continue + } + terraformType := subs[0] + mappings[terraformType] = append(mappings[terraformType], tfMapEntry{ + overmindType: metadata.GetType(), + method: mapping.GetTerraformMethod(), + queryField: subs[1], + }) + } + } + + // Each entry defines a Terraform resource type that must be mapped, what + // Overmind type it should resolve to, and which attribute is extracted from + // the Terraform plan to perform the lookup. + criticalMappings := []struct { + terraformType string + expectedType string + expectedField string + expectedMethod sdp.QueryMethod + reason string // documents why this mapping is critical + }{ + // Core resource mappings + { + terraformType: "google_compute_instance", + expectedType: gcpshared.ComputeInstance.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Core compute resource — one of the most common GCP resources in Terraform", + }, + { + terraformType: "google_compute_network", + expectedType: gcpshared.ComputeNetwork.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "VPC networks are foundational infrastructure with wide blast radius", + }, + { + terraformType: "google_compute_subnetwork", + expectedType: gcpshared.ComputeSubnetwork.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Subnets are critical networking resources", + }, + { + terraformType: "google_storage_bucket", + expectedType: gcpshared.StorageBucket.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Storage buckets are one of the most common GCP resources", + }, + { + terraformType: "google_pubsub_topic", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Pub/Sub topics are critical messaging infrastructure", + }, + { + terraformType: "google_pubsub_subscription", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Pub/Sub subscriptions are critical messaging infrastructure", + }, + // Previously broken mappings (fixed in PRs #3755 and #3782) + { + terraformType: "google_compute_region_instance_group_manager", + expectedType: gcpshared.ComputeRegionInstanceGroupManager.String(), + expectedField: "name", + expectedMethod: sdp.QueryMethod_GET, + reason: "Regional MIG — was missing before PR #3755; customer-reported issue", + }, + { + terraformType: "google_kms_crypto_key", + expectedType: gcpshared.CloudKMSCryptoKey.String(), + expectedField: "id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "KMS key — TerraformMappings() returned nil before PR #3782; customer-reported issue", + }, + // IAM binding mappings — these Terraform-only resources don't have + // standalone GCP APIs, so they resolve to the parent resource for blast + // radius analysis. + // + // Pub/Sub Subscription IAM + { + terraformType: "google_pubsub_subscription_iam_binding", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on subscription — resolves to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_member", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on subscription — resolves to parent subscription for blast radius", + }, + { + terraformType: "google_pubsub_subscription_iam_policy", + expectedType: gcpshared.PubSubSubscription.String(), + expectedField: "subscription", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on subscription — resolves to parent subscription for blast radius", + }, + // Pub/Sub Topic IAM + { + terraformType: "google_pubsub_topic_iam_binding", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on topic — resolves to parent topic for blast radius", + }, + { + terraformType: "google_pubsub_topic_iam_member", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on topic — resolves to parent topic for blast radius", + }, + { + terraformType: "google_pubsub_topic_iam_policy", + expectedType: gcpshared.PubSubTopic.String(), + expectedField: "topic", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on topic — resolves to parent topic for blast radius", + }, + } + + for _, tc := range criticalMappings { + t.Run(tc.terraformType, func(t *testing.T) { + entries, ok := mappings[tc.terraformType] + if !ok { + t.Fatalf("Terraform type %q is NOT registered in any adapter metadata. "+ + "This means it will show as 'Unsupported' in change analysis. Reason it's critical: %s", + tc.terraformType, tc.reason) + } + + // Verify at least one mapping resolves to the expected Overmind type + found := false + for _, entry := range entries { + if entry.overmindType == tc.expectedType { + found = true + + if entry.queryField != tc.expectedField { + t.Errorf("Terraform type %q maps to %q but uses query field %q, expected %q", + tc.terraformType, tc.expectedType, entry.queryField, tc.expectedField) + } + + if entry.method != tc.expectedMethod { + t.Errorf("Terraform type %q maps to %q but uses method %s, expected %s", + tc.terraformType, tc.expectedType, entry.method, tc.expectedMethod) + } + break + } + } + + if !found { + actualTypes := make([]string, 0, len(entries)) + for _, e := range entries { + actualTypes = append(actualTypes, e.overmindType) + } + t.Errorf("Terraform type %q is registered but resolves to %v, expected %q. "+ + "Reason: %s", + tc.terraformType, actualTypes, tc.expectedType, tc.reason) + } + }) + } +} From 5b3f9f5ac2479ad0aa798a5591173b4edd250c2d Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:36:19 +0000 Subject: [PATCH 10/51] Add Terraform mappings for Compute Project and Storage Bucket adapters (#3862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > **Low Risk** > Mapping-only changes (no new API calls or auth logic) but may affect how Terraform changes are attributed in blast-radius calculations. > > **Overview** > Adds Terraform-to-SDP resolution for `ComputeProject` by mapping `google_project`, Shared VPC host/service project resources, and Terraform-only project IAM resources (`*_iam_binding/member/policy`) back to the project for blast-radius analysis. > > Extends the `StorageBucket` adapter’s Terraform mapping to also resolve Terraform-only bucket IAM resources (`google_storage_bucket_iam_*`) back to the parent bucket. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9d8c5f2fbc183aba507bd8755fe6bae6f2ec9c10. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 5937a229c7bb1ffac178a6b5a76b2bd648611923 --- .../gcp/dynamic/adapters/compute-project.go | 42 ++++++++++++++++++- .../gcp/dynamic/adapters/storage-bucket.go | 20 +++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index 862c9064..ad467aff 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -59,6 +59,46 @@ var _ = registerableAdapter{ }, }, terraformMapping: gcpshared.TerraformMapping{ - Description: "There is no terraform resource for this type.", + Reference: "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project", + Description: "Maps google_project, Shared VPC, and project IAM resources to the Compute Project adapter.", + Mappings: []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project.project_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_host_project.project", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_service_project.service_project", + }, + { + // Host project is also affected when the attachment is created/destroyed. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_compute_shared_vpc_service_project.host_project", + }, + // IAM resources for Projects. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent project for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_binding.project", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_member.project", + }, + { + // Authoritative for the entire IAM policy on the project. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_policy.project", + }, + }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index eb0c4176..5b8b862a 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -57,6 +57,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_storage_bucket.name", }, + // IAM resources for Storage Buckets. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent bucket for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam + { + // Authoritative for a given role — grants the role to a list of members. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_binding.bucket", + }, + { + // Non-authoritative — grants a single member a single role. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_member.bucket", + }, + { + // Authoritative for the entire IAM policy on the bucket. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_policy.bucket", + }, }, }, }.Register() From 946711dfea7eb816d5b78fd4d045a534f3703305 Mon Sep 17 00:00:00 2001 From: Dylan Date: Fri, 13 Feb 2026 00:18:36 +0000 Subject: [PATCH 11/51] [ENG-2469] Improve LLM-based unmapped item mapping (#3861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Embed available types in system prompt** instead of using a separate `ListAvailableTypes` tool call, so the LLM knows valid SDP types before making any queries - **Improve Query tool error messages** with type validation (Levenshtein-based fuzzy suggestions, Terraform→SDP type translation), method validation, and actionable empty-result messages including usage hints - **Remove scope from LLM input data** to prevent the LLM from confusing Terraform scopes with SDP scopes - **Tighten system prompt guidance** to prevent overly broad fallback mappings (e.g. mapping to VPCs) and encourage retrying with correct query methods before falling back to parent resources - **Simplify architecture** by making tools and typeInfos mandatory parameters (created once per batch), removing test override fields from `ChangeAnalysisCalculationArgs` ## Linear Ticket - **Ticket**: [ENG-2469](https://linear.app/overmind/issue/ENG-2469) — Implement LLM-based mapping ## Changes - `upcycle_tools.go`: Added `typeValidator` with Levenshtein-based fuzzy matching (`fuzzy.LevenshteinDistance`), Terraform type translation, method validation, and `usageHint` for enriched error messages. Removed `ListAvailableTypes` tool. Added `FormatAvailableTypesForPrompt` and `AvailableTypeInfoFromSources` (with Terraform mapping extraction). - `upcycle_prompt.md`: Embedded `{{ .AvailableTypes }}` in system prompt. Tightened fallback guidance (no VPCs). Added retry-with-correct-method instructions. - `upcycle.go`: Tools/typeInfos created once in `processUnmappedItems` and passed down as parameters. Scope stripped from input data to avoid LLM confusion. - `upcycle_test.go`: Flexible test expectations (`mapped_to_one_of`, `results` list). Removed LLM test overrides, replaced with direct `mappedItemToQuery` tests. Added unit tests for type validation, fuzzy suggestions, Terraform type extraction. - `upcycle_manual_cases.yaml`: YAML-driven manual LLM test cases with flexible expectations for multi-scope and parent-resource mappings. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Changes the control flow for unmapped-item handling and how mappings feed into blast radius analysis; mis-mappings or stricter query validation could alter analysis output for affected changes. > > **Overview** > Improves the *LLM-based upcycle mapping* flow by having the LLM return a **single best `mapped_item`** and converting it into a `MappingQuery`, then running normal blast radius analysis on newly-mapped diffs (instead of doing a separate recursive “affected items” path and merging results). > > Makes LLM querying more reliable by **embedding available SDP types (plus Terraform→SDP mappings)** directly into the system prompt, forcing `Query` to use wildcard scope, stripping `scope` from Terraform item JSON sent to the model, and tightening prompt guidance to prefer specific matches/parents over broad fallbacks. > > Upgrades tooling and tests: removes the `ListAvailableTypes` tool, adds `Query` type/method validation with fuzzy suggestions and richer empty-result guidance, adds YAML-driven manual test cases (`upcycle_manual_cases.yaml`) with flexible expectations, and adds context-based LLM conversation logging plus expanded unit coverage for type extraction/validation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b3e5de86e7c5702ded0ac0aa816b8bd78c8a16cc. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Co-authored-by: Elliot Waddington GitOrigin-RevId: 0c3c4fa1476187b9d9f1ac5f1e248518a56878f3 --- aws-source/adapters/rds-option-group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-source/adapters/rds-option-group.go b/aws-source/adapters/rds-option-group.go index 5bdbe97e..af984c82 100644 --- a/aws-source/adapters/rds-option-group.go +++ b/aws-source/adapters/rds-option-group.go @@ -53,7 +53,7 @@ func NewRDSOptionGroupAdapter(client rdsClient, accountID string, region string, AccountID: accountID, Client: client, AdapterMetadata: optionGroupAdapterMetadata, - cache: cache, + cache: cache, PaginatorBuilder: func(client rdsClient, params *rds.DescribeOptionGroupsInput) Paginator[*rds.DescribeOptionGroupsOutput, *rds.Options] { return rds.NewDescribeOptionGroupsPaginator(client, params) }, From 31d442d87c5273bd7e8a2dfd69003f35aebb91fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:07:28 +0200 Subject: [PATCH 12/51] Update github.com/hashicorp/terraform-config-inspect digest to f4be3ba (#3864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [github.com/hashicorp/terraform-config-inspect](https://redirect.github.com/hashicorp/terraform-config-inspect) | require | digest | `477360e` → `f4be3ba` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 10am on friday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 12b9430349cde2ac02589bce083d6820c1fe98b0 --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4e96f31c..38e05b1e 100644 --- a/go.mod +++ b/go.mod @@ -114,7 +114,7 @@ require ( github.com/harness/harness-go-sdk v0.7.6 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 - github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 + github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 github.com/invopop/jsonschema v0.13.0 github.com/jackc/pgx/v5 v5.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 @@ -337,7 +337,7 @@ require ( github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/lithammer/fuzzysearch v1.1.8 github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect diff --git a/go.sum b/go.sum index 93b86f51..6528be1a 100644 --- a/go.sum +++ b/go.sum @@ -612,8 +612,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77 h1:JyCyXTn0iSHO66Gy5D+4Q031oqRBSRrARILrc1NFu2U= -github.com/hashicorp/terraform-config-inspect v0.0.0-20260204111900-477360eb0c77/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 h1:p+oHuSCXvfFBFAejlPswDa7i5fi3r3+03jeW9mJs4qM= +github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= From 25d0519b6aa3fbf2e307ce89eb3ee7594ea63e10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:15:04 +0200 Subject: [PATCH 13/51] Update k8s.io/utils digest to b8788ab (#3866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [k8s.io/utils](https://redirect.github.com/kubernetes/utils) | require | digest | `914a6e7` → `b8788ab` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 10am on friday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 84f13505cb86a8d243d7c3e2db600f9b06bfb849 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38e05b1e..da2d321a 100644 --- a/go.mod +++ b/go.mod @@ -192,7 +192,7 @@ require ( k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/component-base v0.35.0 - k8s.io/utils v0.0.0-20260108192941-914a6e750570 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 modernc.org/sqlite v1.44.3 riverqueue.com/riverui v0.14.0 sigs.k8s.io/controller-runtime v0.23.1 diff --git a/go.sum b/go.sum index 6528be1a..25ff62d6 100644 --- a/go.sum +++ b/go.sum @@ -1445,8 +1445,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= -k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= From 7092076351a14ee17a74041198ea5e5f852fff43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:15:34 +0200 Subject: [PATCH 14/51] Update google.golang.org/genproto/googleapis/rpc digest to 4cfbd41 (#3865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [google.golang.org/genproto/googleapis/rpc](https://redirect.github.com/googleapis/go-genproto) | require | digest | `546029d` → `4cfbd41` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "before 10am on friday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 9cd6970532d12e63e1b2755a6d349540d2365af5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index da2d321a..26e0750a 100644 --- a/go.mod +++ b/go.mod @@ -183,7 +183,7 @@ require ( gonum.org/v1/gonum v0.17.0 google.golang.org/api v0.265.0 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 diff --git a/go.sum b/go.sum index 25ff62d6..e066529d 100644 --- a/go.sum +++ b/go.sum @@ -1378,8 +1378,8 @@ google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH8 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= From 5a8965eae33d6f272b6d06c94b7331a6d75967b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:39:31 +0200 Subject: [PATCH 15/51] Update Terraform (#3868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [auth0](https://registry.terraform.io/providers/auth0/auth0) ([source](https://redirect.github.com/auth0/terraform-provider-auth0)) | required_provider | patch | `1.39.0` → `1.39.1` | | [aws](https://registry.terraform.io/providers/hashicorp/aws) ([source](https://redirect.github.com/hashicorp/terraform-provider-aws)) | required_provider | minor | `6.31.0` → `6.32.0` | | [github](https://registry.terraform.io/providers/integrations/github) ([source](https://redirect.github.com/integrations/terraform-provider-github)) | required_provider | patch | `6.11.0` → `6.11.1` | | [google](https://registry.terraform.io/providers/hashicorp/google) ([source](https://redirect.github.com/hashicorp/terraform-provider-google)) | required_provider | minor | `7.18.0` → `7.19.0` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
auth0/terraform-provider-auth0 (auth0) ### [`v1.39.1`](https://redirect.github.com/auth0/terraform-provider-auth0/blob/HEAD/CHANGELOG.md#v1391) [Compare Source](https://redirect.github.com/auth0/terraform-provider-auth0/compare/v1.39.0...v1.39.1) BUG FIXES: - `resource/auth0_attack_protection` – Improve CAPTCHA provider validation to allow imports with null sensitive fields while still enforcing checks on create/update ([#​1468](https://redirect.github.com/auth0/terraform-provider-auth0/pull/1468)) - `resource/auth0_client_grant` – Make `allow_all_scopes` nullable so it's omitted from API requests when not explicitly set, and fix transitions to specific scopes ([#​1471](https://redirect.github.com/auth0/terraform-provider-auth0/pull/1471)) - `resource/auth0_user_attribute_profile` – Remove redundant `MinItems` constraint from SAML mappings to fix Terraform generation errors ([#​1461](https://redirect.github.com/auth0/terraform-provider-auth0/pull/1461)) NOTES: - `resource/auth0_client` – Update `grant_types` documentation to include Auth0 extension grants ([#​1470](https://redirect.github.com/auth0/terraform-provider-auth0/pull/1470))
hashicorp/terraform-provider-aws (aws) ### [`v6.32.0`](https://redirect.github.com/hashicorp/terraform-provider-aws/blob/HEAD/CHANGELOG.md#6320-February-11-2026) [Compare Source](https://redirect.github.com/hashicorp/terraform-provider-aws/compare/v6.31.0...v6.32.0) FEATURES: - **New List Resource:** `aws_ecr_repository` ([#​46344](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46344)) - **New List Resource:** `aws_lambda_permission` ([#​46341](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46341)) - **New List Resource:** `aws_route` ([#​46370](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46370)) - **New List Resource:** `aws_route53_resolver_rule_association` ([#​46349](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46349)) - **New List Resource:** `aws_route_table` ([#​46337](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46337)) - **New List Resource:** `aws_s3_directory_bucket` ([#​46373](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46373)) - **New List Resource:** `aws_secretsmanager_secret` ([#​46318](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46318)) - **New List Resource:** `aws_secretsmanager_secret_version` ([#​46342](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46342)) - **New List Resource:** `aws_vpc_security_group_egress_rule` ([#​46368](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46368)) - **New List Resource:** `aws_vpc_security_group_ingress_rule` ([#​46367](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46367)) - **New Resource:** `aws_ec2_secondary_network` ([#​46408](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46408)) - **New Resource:** `aws_ec2_secondary_subnet` ([#​46408](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46408)) ENHANCEMENTS: - resource/aws\_instance: Add `secondary_network_interface` argument ([#​46408](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46408)) - resource/aws\_quicksight\_data\_set: Support `use_as` property to create special RLS rules dataset ([#​42687](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/42687)) BUG FIXES: - data-source/aws\_odb\_network\_peering\_connections: Fix plan phase failure of listing. ([#​46384](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46384)) - list-resource/aws\_s3\_bucket\_policy: Now supports listing Bucket Policies for S3 Directory Buckets ([#​46401](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46401)) - resource/aws\_athena\_workgroup: Allows unsetting `configuration.result_configuration` or child attributes. ([#​46427](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46427)) - resource/aws\_cloudfront\_multitenant\_distribution: Fix the "inconsistent result" error when `custom_error_response` is configured and `custom_error_response.response_code` and `custom_error_response.response_page_path` are omitted ([#​46375](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/46375)) - resource/aws\_grafana\_workspace: Fix perpetual diff when `network_access_control` is configured with empty `prefix_list_ids` and `vpce_ids` ([#​45637](https://redirect.github.com/hashicorp/terraform-provider-aws/issues/45637))
integrations/terraform-provider-github (github) ### [`v6.11.1`](https://redirect.github.com/integrations/terraform-provider-github/releases/tag/v6.11.1) [Compare Source](https://redirect.github.com/integrations/terraform-provider-github/compare/v6.11.0...v6.11.1) #### What's Changed ##### 🐛 Bugfixes - fix: Only send allow\_forking on change by [@​stevehipwell](https://redirect.github.com/stevehipwell) in [#​3174](https://redirect.github.com/integrations/terraform-provider-github/pull/3174) - fix: Type mismatch in `team_id` of `emu_group_mapping` by [@​deiga](https://redirect.github.com/deiga) in [#​3163](https://redirect.github.com/integrations/terraform-provider-github/pull/3163) ##### Maintenance - \[MAINT] Fixup `github_repository_file` by [@​deiga](https://redirect.github.com/deiga) in [#​3175](https://redirect.github.com/integrations/terraform-provider-github/pull/3175) **Full Changelog**:
hashicorp/terraform-provider-google (google) ### [`v7.19.0`](https://redirect.github.com/hashicorp/terraform-provider-google/blob/HEAD/CHANGELOG.md#7190-Unreleased) [Compare Source](https://redirect.github.com/hashicorp/terraform-provider-google/compare/v7.18.0...v7.19.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 10am on friday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: d74f4530d1e4f3de277d42e601f6d4606e0bc273 --- .terraform.lock.hcl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index a462d8d9..539dd127 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "6.31.0" + version = "6.32.0" constraints = ">= 4.56.0" hashes = [ - "h1:1s5nzQMTgvB1AR2yUZcLz7hDM4PdEZZkKdEnBKaq8AY=", - "h1:8ylMF1VIXmf5Dc2Ew5BGOx+T1jblHB0Ik6nzxWxuu2Y=", - "h1:KYem6P5vF6BD+OpuZxrD2Vj668i54BS7+x4pgu0U6DY=", - "h1:M7p95xWoE3zWo1hx0aGg7GB262c7OizE+h6ISrzl1So=", - "h1:NPMzGvaqqewWvKA5ButStYnFWbgFIExWNKuuHZug1sE=", - "h1:PWZAzhvwJ7ccFn9c7EPuCrmGZHk+4w1cqeNA0DDGh/I=", - "h1:QSBCfLTHwJ4iMMFXEwGnc0hJ4pVZ94cenZYLXjnLJx8=", - "h1:bpfP3nGNyZEDeITqMfP/RSZ7aNsdskoK1aDj1t3xWQw=", - "h1:cptVA3yG3sJCAFVcTAtdoreGvvNUqskDYWkGSe/toB4=", - "h1:frOihN9Tg4JBTrN5eBhWmMcR4chDM+YJazvwgXQSHFU=", - "h1:mbLgY4xcgqOsFbkWMzOHa478thZM5SX1ci0F7Isf+7k=", - "h1:pQvUqZS7sRQzYj06cmDoRL/AkRgGo16laRl2nDQC0hQ=", - "h1:uOBPLestaUsogblJEzezIuPD3yi2VqAxjMn3usJ7i5g=", - "h1:yrswE/HFLKpPvs2622EyUPWv87ir0OuixxhrESWTt4g=", - "zh:0184b83f61dfb2f90f051d6a10e85d554809eb7dec13c49000bc884cfd1e956d", - "zh:16f76019ad67f0dfafea2c65b17bd1aa289cb5c275521df71337e23b08af6fec", - "zh:296ebaa261729b78159694e3ca709735c5c67913d6107c7e1abd4d1e9b05fc6b", - "zh:6b4c37bd7e8abca1b428903212de731b04695dcc59e2ba2acefc3d936b36c4dc", - "zh:6f49e2f7464dbb9d6911dd32951637f589d21a5c3c9a7c5056837701977ec803", - "zh:6fc4095e59286dd83e9528346390b0b07b3bffa1d46b50027c9e080352207626", - "zh:98816c0c5d1b956b564c2d2feb423fdf4eb3e476a6c5202668a285ff3b2d6910", + "h1:06E17LKFuocdc5P7YHtAmig8Zu+S+gy4CKhYhdxh434=", + "h1:GmBSZ299apGpQl3y7W8AlBQKXtpUQvis75x5tb8t5hs=", + "h1:KlmHUHezk7AkOTTJLhl8JpBOsV+qK6wV9Py3vIsN7Mo=", + "h1:NoPDPkJFBBCZiJIvPhyQG198UmDBXoX2yHnkeJFW0gU=", + "h1:Orm3zdQYphZwxTzDxMQznPAgssFi9OOME2MzJUBg1P8=", + "h1:SQSVbmxbj9QHkla4qaAiJOgjjZD2bUOLrniCT8DEpfQ=", + "h1:VI0iNxbVgblVEil2nyJgtHqijaLQE6VAgv55EWi56Ow=", + "h1:Yq/qR5VAO2WSH4gextYqb7zTcCNgO7Q7G8cJR2ps23E=", + "h1:bm1kmfgtxYybVyTG/zlQVOpaDxMZ9aOQ7kmX0BjAbxs=", + "h1:hFdNMX6bUH1hyxVGW8MxdUhPSUpQRNFR4cscSPfJDzk=", + "h1:kB9++004H9FgpxdtZS11L2ESIw1tJqNW4xsyt1WM0+c=", + "h1:mi90ohakIqjaalUb185KRxb/F+etJN7LB2/44k8+u9g=", + "h1:qA6Rfb/qlHhjnAmyLtv/jfu/tO2XOmp0tNNP0Fdmzfs=", + "h1:rriTF/YcLBH13/IijWHwuWAG84KRPw3x++afslASeL4=", + "zh:100d4cffe4be3d0ae41799ab1bb649f0dd1d40aebe8956d0a30ab923e417e085", + "zh:132ed90c0572a6ff5d760b5b2f2e18e67eabed44286619475777de647cd4fe7b", + "zh:4bc7efffa57ed187a4ae1b88a81fd0fd7663610aad8a1d4e3f2c6d19621a5424", + "zh:5682f9486a77d41245867f20e4858fac7e62b2e32cdf8553fd3eeeb520349294", + "zh:65524aa437659dc111f1c9bb304f18f63b368c32a2375651752ce01400321f6a", + "zh:9086e392b5759e35610b45f0e19348916a6782707ab24a0acafa89e020976393", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a70b34fb8d5a7d3b3823046938f3c9b0527afa93d02086b3d87ffa668c9a350e", - "zh:c24c0a58a8301d13cb4c27738840c8e7e0f29563ccf8d6b5ca1c87fcf21bdf89", - "zh:c95d44b2baea56b03198acaaf50f9196504d0207118e1afca7d70b5840315dc4", - "zh:db5a7692e2bde721a37b83f89eb9886715dbd17eb45858b1b58b8f7903ce5144", - "zh:db706b23a652e06c6c3f5de1da70e55a20a4fc2f73c01c24f7b9cd39a9a35f56", - "zh:fb781119fa98d8b0318ffb26ef013d5e674637e8f6a36b4b9c2742f24c022538", - "zh:fc459573b260a5a295d5fed442bf4f44fe034a0702ca22a65d320bd3b3e70eb5", + "zh:b76afced8e29db21f6d49ef3fbb2ed504f4da11cad1585c53a1c0a141c707ebe", + "zh:c3611868f7504e3e1d35d4b834d59c2de30718c4581f5f06afc448525884eeec", + "zh:c62f1f262975ccb9014815484ff09b3854668384213797cf6e6bea9b237a469e", + "zh:c6cdcb1d859b83b2507f163aeeea0a9019eacdeb90baca65fe014713485aae6b", + "zh:d157ccba5e47ac9c919c4b7c00cbb34ffd0e67dcf90e5b67e24f5f639cd1e496", + "zh:d21b00b8b4cfc6234794ad83ee37756b08eded9fcf262455d4443c38ac020917", + "zh:db55a5057cda8ca84445a362366b2bc15ffef55297ef1c883eb9ac52439c5933", + "zh:f2497a4cece92bc16ffdb9f0445f28bea1b99ef06923871ab1ef16c69b7dce36", ] } From bc99e55df9eb726f0146b3c4067f7e6ce4808ebb Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Fri, 13 Feb 2026 11:13:54 +0100 Subject: [PATCH 16/51] [GCP] Remove BlastPropagation from all GCP adapters (#3847) > [!NOTE] > **Medium Risk** > Removes `BlastPropagation` (in/out) semantics from many GCP adapter link definitions and their StaticTests, which can change how dependency/blast-radius relationships are represented downstream even though link discovery remains. > > **Overview** > **Removes blast propagation metadata from GCP dynamic adapters.** Adapter `blastPropagation` maps now only describe *linked resource types* (`ToSDPItemType` + `Description` + optional `IsParentToChild`), dropping per-link `sdp.BlastPropagation` settings. > > **Updates tests and docs accordingly.** StaticTests no longer assert `ExpectedBlastPropagation`, and documentation/rules are revised to require coverage of all linked resources the adapter produces rather than blast-propagation paths. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 758d93a0a8fbd990569cc12575fe62fbc9486eb8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: e1880f9ff9af95275befc7a23ab9068410959d06 --- sources/gcp/README.md | 7 - .../rules/dynamic-adapter-creation.mdc | 29 +-- .../.cursor/rules/dynamic-adapter-testing.mdc | 44 +--- .../ai-platform-batch-prediction-job.go | 24 -- .../ai-platform-batch-prediction-job_test.go | 20 -- .../adapters/ai-platform-custom-job.go | 12 - .../adapters/ai-platform-custom-job_test.go | 12 - .../dynamic/adapters/ai-platform-endpoint.go | 27 --- .../adapters/ai-platform-endpoint_test.go | 24 -- ...latform-model-deployment-monitoring-job.go | 25 --- ...rm-model-deployment-monitoring-job_test.go | 52 ----- .../gcp/dynamic/adapters/ai-platform-model.go | 14 -- .../adapters/ai-platform-model_test.go | 16 -- .../adapters/ai-platform-pipeline-job.go | 3 - .../adapters/ai-platform-pipeline-job_test.go | 12 - .../artifact-registry-docker-image.go | 1 - .../artifact-registry-docker-image_test.go | 4 - .../adapters/artifact-registry-repository.go | 12 - ...big-query-data-transfer-transfer-config.go | 7 - ...uery-data-transfer-transfer-config_test.go | 12 - .../adapters/big-table-admin-app-profile.go | 3 - .../big-table-admin-app-profile_test.go | 8 - .../adapters/big-table-admin-backup.go | 3 - .../adapters/big-table-admin-backup_test.go | 16 -- .../adapters/big-table-admin-cluster.go | 4 - .../adapters/big-table-admin-cluster_test.go | 8 - .../adapters/big-table-admin-instance.go | 4 - .../adapters/big-table-admin-instance_test.go | 4 - .../dynamic/adapters/big-table-admin-table.go | 3 - .../adapters/big-table-admin-table_test.go | 12 - .../adapters/cloud-billing-billing-info.go | 5 - .../gcp/dynamic/adapters/cloud-build-build.go | 12 - .../adapters/cloud-build-build_test.go | 8 - .../cloud-resource-manager-tag-value.go | 4 - .../cloud-resource-manager-tag-value_test.go | 4 - .../adapters/cloudfunctions-function.go | 32 --- .../adapters/cloudfunctions-function_test.go | 40 ---- .../compute-external-vpn-gateway_test.go | 4 - .../gcp/dynamic/adapters/compute-firewall.go | 4 - .../dynamic/adapters/compute-firewall_test.go | 12 - .../adapters/compute-global-address.go | 4 - .../adapters/compute-global-address_test.go | 8 - .../compute-global-forwarding-rule.go | 8 - .../compute-global-forwarding-rule_test.go | 20 -- .../compute-http-health-check_test.go | 8 - .../adapters/compute-instance-template.go | 30 --- .../compute-instance-template_test.go | 112 ---------- .../compute-network-endpoint-group.go | 16 -- .../compute-network-endpoint-group_test.go | 16 -- .../gcp/dynamic/adapters/compute-network.go | 9 - .../dynamic/adapters/compute-network_test.go | 12 - .../gcp/dynamic/adapters/compute-project.go | 8 - .../dynamic/adapters/compute-project_test.go | 8 - .../compute-public-delegated-prefix.go | 3 - .../compute-public-delegated-prefix_test.go | 16 -- .../adapters/compute-region-commitment.go | 8 - .../compute-region-commitment_test.go | 8 - .../adapters/compute-resource-policy.go | 1 - sources/gcp/dynamic/adapters/compute-route.go | 30 --- .../dynamic/adapters/compute-route_test.go | 20 -- .../gcp/dynamic/adapters/compute-router.go | 20 -- .../dynamic/adapters/compute-router_test.go | 56 ----- .../dynamic/adapters/compute-storage-pool.go | 8 - .../dynamic/adapters/compute-subnetwork.go | 12 - .../adapters/compute-subnetwork_test.go | 8 - .../adapters/compute-target-http-proxy.go | 3 - .../compute-target-http-proxy_test.go | 4 - .../adapters/compute-target-https-proxy.go | 12 - .../compute-target-https-proxy_test.go | 12 - .../dynamic/adapters/compute-target-pool.go | 3 - .../adapters/compute-target-pool_test.go | 20 -- .../gcp/dynamic/adapters/compute-url-map.go | 4 - .../dynamic/adapters/compute-url-map_test.go | 48 ---- .../dynamic/adapters/compute-vpn-gateway.go | 4 - .../adapters/compute-vpn-gateway_test.go | 12 - .../dynamic/adapters/compute-vpn-tunnel.go | 20 -- .../adapters/compute-vpn-tunnel_test.go | 24 -- .../gcp/dynamic/adapters/container-cluster.go | 7 - .../adapters/container-cluster_test.go | 56 ----- .../dynamic/adapters/container-node-pool.go | 6 - .../adapters/container-node-pool_test.go | 16 -- .../dynamic/adapters/dataform-repository.go | 4 - .../adapters/dataform-repository_test.go | 8 - .../dynamic/adapters/dataplex-data-scan.go | 12 - .../adapters/dataplex-data-scan_test.go | 4 - .../gcp/dynamic/adapters/dataproc-cluster.go | 18 -- .../dynamic/adapters/dataproc-cluster_test.go | 40 ---- .../gcp/dynamic/adapters/dns-managed-zone.go | 10 - .../dynamic/adapters/dns-managed-zone_test.go | 22 +- .../gcp/dynamic/adapters/eventarc-trigger.go | 36 --- sources/gcp/dynamic/adapters/file-instance.go | 1 - .../dynamic/adapters/file-instance_test.go | 12 - .../dynamic/adapters/logging-bucket_test.go | 12 - sources/gcp/dynamic/adapters/logging-link.go | 5 - .../gcp/dynamic/adapters/logging-link_test.go | 8 - .../adapters/monitoring-alert-policy.go | 2 - .../adapters/monitoring-alert-policy_test.go | 8 - .../monitoring-notification-channel.go | 2 - .../gcp/dynamic/adapters/orgpolicy-policy.go | 1 - .../dynamic/adapters/pubsub-subscription.go | 6 - .../adapters/pubsub-subscription_test.go | 32 --- sources/gcp/dynamic/adapters/pubsub-topic.go | 14 -- .../gcp/dynamic/adapters/pubsub-topic_test.go | 8 - .../gcp/dynamic/adapters/redis-instance.go | 1 - .../dynamic/adapters/redis-instance_test.go | 20 -- sources/gcp/dynamic/adapters/run-revision.go | 10 - .../gcp/dynamic/adapters/run-revision_test.go | 8 - sources/gcp/dynamic/adapters/run-service.go | 25 --- .../gcp/dynamic/adapters/run-service_test.go | 48 ---- .../gcp/dynamic/adapters/run-worker-pool.go | 17 -- .../dynamic/adapters/secret-manager-secret.go | 4 - .../adapters/secret-manager-secret_test.go | 12 - ...nter-management-security-center-service.go | 4 - ...management-security-center-service_test.go | 4 - .../adapters/service-directory-endpoint.go | 1 - .../service-directory-endpoint_test.go | 12 - .../adapters/service-directory-service.go | 4 - .../dynamic/adapters/service-usage-service.go | 26 --- .../adapters/service-usage-service_test.go | 4 - .../gcp/dynamic/adapters/spanner-backup.go | 6 - .../gcp/dynamic/adapters/spanner-database.go | 3 - .../dynamic/adapters/spanner-database_test.go | 20 -- .../gcp/dynamic/adapters/spanner-instance.go | 8 - .../dynamic/adapters/spanner-instance_test.go | 9 +- .../dynamic/adapters/sql-admin-backup-run.go | 4 - .../gcp/dynamic/adapters/sql-admin-backup.go | 8 - .../dynamic/adapters/sql-admin-backup_test.go | 24 -- .../dynamic/adapters/sql-admin-instance.go | 22 +- .../adapters/sql-admin-instance_test.go | 48 ---- .../gcp/dynamic/adapters/storage-bucket.go | 1 - .../dynamic/adapters/storage-bucket_test.go | 4 - .../adapters/storage-transfer-transfer-job.go | 51 ----- .../storage-transfer-transfer-job_test.go | 36 --- .../compute-node-group_test.go | 6 +- sources/gcp/manual/README.md | 4 - sources/gcp/manual/big-query-dataset.go | 34 --- sources/gcp/manual/big-query-dataset_test.go | 28 --- sources/gcp/manual/big-query-model.go | 20 -- sources/gcp/manual/big-query-model_test.go | 8 - sources/gcp/manual/big-query-routine.go | 20 +- sources/gcp/manual/big-query-routine_test.go | 16 -- sources/gcp/manual/big-query-table.go | 31 +-- sources/gcp/manual/big-query-table_test.go | 24 -- .../cloud-kms-crypto-key-version_test.go | 8 - .../gcp/manual/cloud-kms-crypto-key_test.go | 24 -- sources/gcp/manual/cloud-kms-key-ring_test.go | 16 -- sources/gcp/manual/compute-address.go | 17 -- sources/gcp/manual/compute-address_test.go | 60 ----- sources/gcp/manual/compute-autoscaler.go | 4 - sources/gcp/manual/compute-autoscaler_test.go | 4 - sources/gcp/manual/compute-backend-service.go | 56 ----- .../manual/compute-backend-service_test.go | 168 -------------- sources/gcp/manual/compute-disk.go | 67 +----- sources/gcp/manual/compute-disk_test.go | 50 ----- sources/gcp/manual/compute-forwarding-rule.go | 37 +--- .../manual/compute-forwarding-rule_test.go | 100 --------- sources/gcp/manual/compute-healthcheck.go | 16 -- .../gcp/manual/compute-healthcheck_test.go | 24 -- sources/gcp/manual/compute-image.go | 35 +-- sources/gcp/manual/compute-image_test.go | 52 ----- .../compute-instance-group-manager-shared.go | 13 -- .../compute-instance-group-manager_test.go | 84 ------- sources/gcp/manual/compute-instance-group.go | 16 -- .../gcp/manual/compute-instance-group_test.go | 12 - sources/gcp/manual/compute-instance.go | 76 ------- sources/gcp/manual/compute-instance_test.go | 208 ------------------ .../gcp/manual/compute-instant-snapshot.go | 4 - .../manual/compute-instant-snapshot_test.go | 4 - sources/gcp/manual/compute-machine-image.go | 60 ----- .../gcp/manual/compute-machine-image_test.go | 72 ------ sources/gcp/manual/compute-node-group.go | 4 - sources/gcp/manual/compute-node-group_test.go | 4 - sources/gcp/manual/compute-node-template.go | 4 - .../gcp/manual/compute-node-template_test.go | 4 - ...pute-region-instance-group-manager_test.go | 48 ---- sources/gcp/manual/compute-reservation.go | 12 - .../gcp/manual/compute-reservation_test.go | 12 - sources/gcp/manual/compute-security-policy.go | 4 - .../manual/compute-security-policy_test.go | 4 - sources/gcp/manual/compute-snapshot.go | 20 -- sources/gcp/manual/compute-snapshot_test.go | 25 --- sources/gcp/manual/iam-service-account-key.go | 7 - .../manual/iam-service-account-key_test.go | 1 - sources/gcp/manual/iam-service-account.go | 8 - .../gcp/manual/iam-service-account_test.go | 4 - sources/gcp/manual/logging-sink.go | 20 -- sources/gcp/manual/logging-sink_test.go | 24 -- sources/gcp/shared/blast-propagations.go | 53 ++--- .../gcp/shared/cross_project_linking_test.go | 29 +-- sources/gcp/shared/kms-asset-loader.go | 44 ---- sources/gcp/shared/linker.go | 13 +- sources/gcp/shared/manual-adapter-links.go | 132 +++++------ .../gcp/shared/manual-adapter-links_test.go | 77 +------ sources/shared/testing.go | 2 +- 194 files changed, 113 insertions(+), 3876 deletions(-) diff --git a/sources/gcp/README.md b/sources/gcp/README.md index e60a635e..30e4e642 100644 --- a/sources/gcp/README.md +++ b/sources/gcp/README.md @@ -116,15 +116,12 @@ When defining a relation between two adapters, we need to answer the following q - What is the method to use to get the related item?: `sdp.QueryMethod_GET`, `sdp.QueryMethod_SEARCH`, `sdp.QueryMethod_LIST` - What is the query string to pass to the selected method? - What is the scope of the related item?: `project`, `region`, `zone` -- How is the relation between the two items?: `BlastPropagation` - In the following example, we define a relation between the `ComputeInstance` and `ComputeSubnetwork` adapters. - We identify the `ComputeSubnetwork` adapter as the related item. - We use the `sdp.QueryMethod_GET` method to get the related item. Because the attribute `subnetwork_name` can be used to get the `ComputeSubnetwork` resource. If it was an attribute that can be used for searching, we would use the `sdp.QueryMethod_SEARCH` method. By the time we are developing the adapter, the linked adapter may not be present. In that case, we have to research the linked adapter and make the correct judgement. - We use the `subnetworkName` as the query string to pass to the `GET` method. Because its [SDK documentation](https://cloud.google.com/compute/docs/reference/rest/v1/subnetworks/get) states that we need to pass its `name` to get the resource. - We define the scope as `region` via the `gcpshared.RegionalScope(c.ProjectID(), region)` helper function. Because the `ComputeSubnetwork` resource is a regional resource. It requires the `project_id` and `region` along with its `name` to get the resource. -- We define the relation as `BlastPropagation` with `In: true` and `Out: false`. Because the adapter we define is the `ComputeInstance` adapter, we want to propagate the blast radius from the `ComputeInstance` to the `ComputeSubnetwork`. This means that if the `ComputeSubnetwork` is deleted, the `ComputeInstance` will be affected by that (`in:true`). But if the `ComputeInstance` is deleted, the `ComputeSubnetwork` will not be affected (`out:false`). The relation might not be that clear all the time. In this case we should err on to `true` side. ```go &sdp.LinkedItemQuery{ @@ -135,10 +132,6 @@ In the following example, we define a relation between the `ComputeInstance` and // This is a regional resource Scope: gcpshared.RegionalScope(c.ProjectID(), region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } ``` diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc index e2aef159..8799ec19 100644 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc +++ b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc @@ -164,12 +164,12 @@ For each attribute that references another resource: ```go blastPropagation: map[string]*gcpshared.Impact{ "network": { - In: true, // This resource is affected if network changes - Out: false, // Network is not affected if this resource changes + ToSDPItemType: gcpshared.ComputeNetwork, + Description: "This resource is affected if network changes. Network is not affected if this resource changes.", }, "subnet": { - In: true, // This resource is affected if subnet changes - Out: true, // Subnet is affected if this resource changes + ToSDPItemType: gcpshared.ComputeSubnetwork, + Description: "This resource is affected if subnet changes. Subnet is affected if this resource changes.", }, }, ``` @@ -199,9 +199,8 @@ SpannerDatabase: { // This is a backlink to instance. // Framework will extract the instance name and create the linked item query with GET "name": { - Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", - ToSDPItemType: SpannerInstance, - BlastPropagation: impactInOnly, + Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", + ToSDPItemType: SpannerInstance, }, } ``` @@ -228,10 +227,6 @@ When adding backlink blast propagations, verify they work correctly in tests: ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } ``` @@ -262,12 +257,8 @@ var spannerInstanceAdapter = registerableAdapter{ // This a link from parent to child via SEARCH // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. "name": { - ToSDPItemType: gcpshared.SpannerDatabase, - Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, + ToSDPItemType: gcpshared.SpannerDatabase, + Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", IsParentToChild: true, }, }, @@ -297,10 +288,6 @@ When adding parent-to-child blast propagations, verify they work correctly in te ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, } ``` diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc index 12118ff2..2481e07b 100644 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc +++ b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc @@ -292,11 +292,11 @@ t.Run("Get", func(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", resourceName, val) } - // Include static tests - MUST cover ALL blast propagation links + // Include static tests - MUST cover ALL linked resources the adapter produces t.Run("StaticTests", func(t *testing.T) { - // CRITICAL: Review the adapter's blast propagation configuration and to create - // test cases for EVERY linked resource defined in the adapter's blastPropagation map - // Check the adapter file (e.g., compute-global-address.go) for all blast propagation entries + // CRITICAL: Review the adapter's linked-resource map and create + // test cases for EVERY linked item type the adapter produces. + // Check the adapter file (e.g., compute-global-address.go) for all linked resource types. // IMPORTANT: Always use Item types with .String() for ExpectedType (e.g., gcpshared.ComputeNetwork.String()) // NEVER use pure strings (e.g., "gcp-compute-network") for ExpectedType queryTests := shared.QueryTests{ @@ -305,10 +305,6 @@ t.Run("Get", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "linked-resource-name", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -460,14 +456,14 @@ func uint64Ptr(u uint64) *uint64 { ## Blast Propagation Testing Requirements -**CRITICAL**: Every adapter test SHOULD include comprehensive StaticTests that cover ALL blast propagation links defined in the adapter configuration. +**CRITICAL**: Every adapter test SHOULD include comprehensive StaticTests that cover ALL linked resources defined in the adapter. ### Steps to Ensure Complete Coverage 1. **Review Adapter File**: Open the corresponding adapter file (e.g., `compute-global-address.go`) -2. **Find blastPropagation Map**: Locate the `blastPropagation` field in the adapter configuration -3. **Create Test Cases**: Write a QueryTest for EVERY entry in the blastPropagation map -4. **Handle TODOs**: Note any blast propagation entries marked with TODO comments - these may not work yet but should be documented in test comments +2. **Find linked-resource map**: Locate the map in the adapter that defines which linked item types the adapter produces (field name may be `blastPropagation`; values are `*gcpshared.Impact`). +3. **Create Test Cases**: Write a QueryTest for every linked item type the adapter produces (one test per distinct linked type, not per attribute path). +4. **Handle TODOs**: Note any entries marked with TODO comments—these may not work yet but should be documented in test comments. ### Example Complete StaticTests @@ -480,10 +476,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -491,10 +483,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Backend service link { @@ -502,10 +490,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-backend-service", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Subnetwork link (note special scope format) { @@ -513,10 +497,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnet", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -539,7 +519,7 @@ t.Run("StaticTests", func(t *testing.T) { - [ ] **ErrorHandling test**: Tests error responses (e.g., 404 Not Found) - [ ] **LIMIT TO 3/4 TEST CASES ONLY**: Get, List/Search, Search with Terraform, ErrorHandling - no additional tests needed - [ ] Multiple query parameter resources test combined query format (e.g., "location/resource") -- [ ] **CRITICAL**: Static tests include blast propagation for ALL linked resources in the adapter's blastPropagation map +- [ ] **CRITICAL**: Static tests include a test case for ALL linked resources defined in the adapter (every linked item type the adapter produces) - [ ] Static test queries use correct scope formats (especially for subnetworks: "projectID.region") - [ ] Static test queries use correct query formats (especially for KMS keys: use `shared.CompositeLookupKey()`) - [ ] Pointer helper functions are defined locally @@ -553,7 +533,7 @@ t.Run("StaticTests", func(t *testing.T) { - Always validate SDP item type, scope, and unique attribute - Include comprehensive attribute validation in Get tests using **camelCase** for attribute names - Use `t.Skipf()` for optional functionality (like Search) -- Always include static tests with blast propagation +- Always include static tests that cover every linked resource the adapter produces - Mock HTTP responses must match actual API endpoints exactly - **Length checks**: Regular Search/List tests expect 2+ items, but Terraform format Search expects exactly 1 item @@ -595,7 +575,7 @@ go test -race ./sources/gcp/... -v - Missing mock responses for HTTP calls - Incorrect protobuf type usage - Wrong API endpoint URLs - - Missing or incorrect blast propagation configurations + - Missing or incorrect linked-resource test cases in StaticTests - Scope format issues (especially for regional resources) - Fix the underlying issues in the adapter or test code - Re-run tests until all tests pass @@ -627,7 +607,7 @@ TestYourAdapter(t *testing.T) { t.Run("Get", func(t *testing.T) { // Test Get functionality t.Run("StaticTests", func(t *testing.T) { - // Test ALL blast propagation links + // Test ALL linked resources the adapter produces }) }) diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go index 9be4fdcf..f88335d7 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go @@ -36,24 +36,15 @@ var _ = registerableAdapter{ "model": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "If the Model is deleted or updated: The batch prediction job may fail. If the batch prediction job is updated: The model remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "If the Endpoint is deleted or updated: The batch prediction job may fail. If the batch prediction job is updated: The endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "inputConfig.gcsSource.uris": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS source bucket is deleted or inaccessible: The batch prediction job will fail to read input data. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: // BigQuery path. For example: bq://projectId.bqDatasetId.bqTableId. @@ -61,17 +52,11 @@ var _ = registerableAdapter{ "inputConfig.bigquerySource.inputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table is deleted or inaccessible: The batch prediction job will fail to read input data. If the batch prediction job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "outputConfig.gcsDestination.outputUriPrefix": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the output GCS bucket is deleted or inaccessible: The batch prediction job will fail to write results. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: // BigQuery path. For example: bq://projectId.bqDatasetId.bqTableId. @@ -79,25 +64,16 @@ var _ = registerableAdapter{ "outputConfig.bigqueryDestination.outputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery output table is deleted or inaccessible: The batch prediction job will fail to write results. If the batch prediction job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the Service Account is deleted or permissions are revoked: The batch prediction job may fail to access required resources. If the batch prediction job is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "network": gcpshared.ComputeNetworkImpactInOnly, // TODO: https://linear.app/overmind/issue/ENG-1446/investigate-creating-a-manual-linker-for-cloud-storage "unmanagedContainerModel.artifactUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the model artifacts is deleted or inaccessible: The batch prediction job will fail to access the model. If the batch prediction job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go index ff522bd7..cee47075 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go @@ -301,30 +301,18 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-model", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.IAMServiceAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("batch-prediction@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Input GCS bucket link { @@ -332,10 +320,6 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-input-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Output GCS bucket link { @@ -343,10 +327,6 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-output-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go index 02e24ab9..ca6a1e0d 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go @@ -34,73 +34,61 @@ var _ = registerableAdapter{ "encryptionSpec.kmsKeyName": { Description: "If the Cloud KMS CryptoKey is updated: The CustomJob may not be able to access encrypted output artifacts. If the CustomJob is updated: The CryptoKey remains unaffected.", ToSDPItemType: gcpshared.CloudKMSCryptoKey, - BlastPropagation: gcpshared.ImpactInOnly, }, // The full name of the network to which the job should be peered. "jobSpec.network": { Description: "If the Compute Network is deleted or updated: The CustomJob may lose connectivity or fail to run as expected. If the CustomJob is updated: The network remains unaffected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, // The service account that the job runs as. "jobSpec.serviceAccount": { Description: "If the IAM Service Account is deleted or updated: The CustomJob may fail to run or lose permissions. If the CustomJob is updated: The service account remains unaffected.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: gcpshared.ImpactInOnly, }, // The Cloud Storage location to store the output of this CustomJob. "jobSpec.baseOutputDirectory.gcsOutputDirectory": { Description: "If the Storage Bucket is deleted or updated: The CustomJob may fail to write outputs. If the CustomJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of a Vertex AI Tensorboard resource to which this CustomJob will upload Tensorboard logs. "jobSpec.tensorboard": { Description: "If the Vertex AI Tensorboard is deleted or updated: The CustomJob may fail to upload logs or lose access to previous logs. If the CustomJob is updated: The tensorboard remains unaffected.", ToSDPItemType: gcpshared.AIPlatformTensorBoard, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of an experiment to associate with the CustomJob. "jobSpec.experiment": { Description: "If the Vertex AI Experiment is deleted or updated: The CustomJob may lose experiment tracking or association. If the CustomJob is updated: The experiment remains unaffected.", ToSDPItemType: gcpshared.AIPlatformExperiment, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of an experiment run to associate with the CustomJob. "jobSpec.experimentRun": { Description: "If the Vertex AI ExperimentRun is deleted or updated: The CustomJob may lose run tracking or association. If the CustomJob is updated: The experiment run remains unaffected.", ToSDPItemType: gcpshared.AIPlatformExperimentRun, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The name of a model to upload the trained Model to upon job completion. "jobSpec.models": { Description: "If the Vertex AI Model is deleted or updated: The CustomJob may fail to upload or associate the trained model. If the CustomJob is updated: The model remains unaffected.", ToSDPItemType: gcpshared.AIPlatformModel, - BlastPropagation: gcpshared.ImpactInOnly, }, // Optional. The ID of a PersistentResource to run the job on existing machines. "jobSpec.persistentResourceId": { Description: "If the Vertex AI PersistentResource is deleted or updated: The CustomJob may fail to run or lose access to the persistent resources. If the CustomJob is updated: The PersistentResource remains unaffected.", ToSDPItemType: gcpshared.AIPlatformPersistentResource, - BlastPropagation: gcpshared.ImpactInOnly, }, // Container image URI used in worker pool specs (for containerSpec). "jobSpec.workerPoolSpecs.containerSpec.imageUri": { Description: "If the Artifact Registry Docker Image is updated or deleted: The CustomJob may fail to run or use an incorrect container image. If the CustomJob is updated: The Docker image remains unaffected.", ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, - BlastPropagation: gcpshared.ImpactInOnly, }, // Executor container image URI used in worker pool specs (for pythonPackageSpec). "jobSpec.workerPoolSpecs.pythonPackageSpec.executorImageUri": { Description: "If the Artifact Registry Docker Image is updated or deleted: The CustomJob may fail to run or use an incorrect executor image. If the CustomJob is updated: The Docker image remains unaffected.", ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, - BlastPropagation: gcpshared.ImpactInOnly, }, // GCS URIs of Python package files used in worker pool specs. "jobSpec.workerPoolSpecs.pythonPackageSpec.packageUris": { Description: "If the Storage Bucket containing the Python packages is deleted or updated: The CustomJob may fail to access required package files. If the CustomJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go index b77ec13d..fb440b92 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go @@ -74,10 +74,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // jobSpec.network @@ -85,10 +81,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // jobSpec.serviceAccount @@ -96,10 +88,6 @@ func TestAIPlatformCustomJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "aiplatform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go index 87f3a4e4..408084f0 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go @@ -33,57 +33,30 @@ var _ = registerableAdapter{ "deployedModels.model": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "deployedModels.serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the service account is deleted or its permissions are updated: The DeployedModel may fail to run or access required resources. If the DeployedModel is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "deployedModels.sharedResources": { ToSDPItemType: gcpshared.AIPlatformDeploymentResourcePool, Description: "If the DeploymentResourcePool is deleted or updated: The DeployedModel may fail to run or lose access to shared resources. If the DeployedModel is updated: The DeploymentResourcePool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "deployedModels.privateEndpoints.serviceAttachment": { ToSDPItemType: gcpshared.ComputeServiceAttachment, Description: "If the Service Attachment is deleted or updated: The DeployedModel's private endpoint connectivity may be disrupted. If the DeployedModel is updated: The Service Attachment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "modelDeploymentMonitoringJob": { ToSDPItemType: gcpshared.AIPlatformModelDeploymentMonitoringJob, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "dedicatedEndpointDns": { ToSDPItemType: stdlib.NetworkDNS, Description: "The DNS name for the dedicated endpoint. If the Endpoint is deleted, this DNS name will no longer resolve.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "predictRequestResponseLoggingConfig.bigqueryDestination.outputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery Table is deleted or updated, the Endpoint's logging configuration may be affected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index 24797117..177372ad 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -119,10 +119,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network link { @@ -130,10 +126,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed model link { @@ -141,10 +133,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-model", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Model deployment monitoring job link { @@ -152,10 +140,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-job"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Dedicated endpoint DNS link { @@ -163,10 +147,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-endpoint.aiplatform.googleapis.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // BigQuery table link { @@ -174,10 +154,6 @@ func TestAIPlatformEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test_dataset", "test_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go index ca74dee1..26ffeca9 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go @@ -36,59 +36,34 @@ var _ = registerableAdapter{ "endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "They are tightly coupled - monitoring job monitors the endpoint's deployed models.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.deployedModelId": { ToSDPItemType: gcpshared.AIPlatformModel, Description: "If the Model is deleted or updated: The monitoring job may fail to monitor. If the monitoring job is updated: The model remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelMonitoringAlertConfig.notificationChannels": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "If the Notification Channel is deleted or updated: The monitoring job may fail to send alerts. If the monitoring job is updated: The notification channel remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "bigqueryTables.bigqueryTablePath": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table storing monitoring logs is deleted or inaccessible: The monitoring job may fail to write logs. If the monitoring job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.objectiveConfig.trainingDataset.gcsSource.uris": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing training data is deleted or inaccessible: The monitoring job may fail to compare predictions against training data. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "modelDeploymentMonitoringObjectiveConfigs.objectiveConfig.trainingDataset.bigquerySource.inputUri": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table containing training data is deleted or inaccessible: The monitoring job may fail to compare predictions against training data. If the monitoring job is updated: The table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "predictInstanceSchemaUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the prediction instance schema is deleted or inaccessible: The monitoring job may fail to validate prediction requests. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "analysisInstanceSchemaUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the GCS bucket containing the analysis instance schema is deleted or inaccessible: The monitoring job may fail to perform analysis. If the monitoring job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go index efe28db3..89ba61b6 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go @@ -137,10 +137,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // AI Platform Endpoint link (bidirectional) { @@ -148,10 +144,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-endpoint", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Deployed Model ID link (AI Platform Model) { @@ -159,10 +151,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "deployed-model-123", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Notification Channel 1 link { @@ -170,10 +158,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "alert-channel-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Notification Channel 2 link { @@ -181,10 +165,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "alert-channel-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery table 1 link (training predict log) { @@ -192,10 +172,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("monitoring_dataset", "training_predict_log"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery table 2 link (serving predict log) { @@ -203,10 +179,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("monitoring_dataset", "serving_predict_log"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Training dataset GCS source bucket links { @@ -214,20 +186,12 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "training-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "training-bucket-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Training dataset BigQuery source link { @@ -235,10 +199,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("training_dataset", "training_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed Model ID link (second model) { @@ -246,10 +206,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "deployed-model-456", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Schema bucket link for predict instance schema { @@ -257,10 +213,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "schema-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Schema bucket link for analysis instance schema { @@ -268,10 +220,6 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "schema-bucket-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model.go b/sources/gcp/dynamic/adapters/ai-platform-model.go index 29f48853..0a358a1a 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model.go @@ -31,33 +31,19 @@ var _ = registerableAdapter{ "containerSpec.imageUri": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is updated or deleted: The Model may fail to serve predictions. If the Model is updated: The Docker image remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "pipelineJob": { ToSDPItemType: gcpshared.AIPlatformPipelineJob, Description: "If the Pipeline Job is deleted: The Model may not be retrievable. If the Model is updated: The Pipeline Job remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "deployedModels.endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // GCS bucket containing the Model artifact and supporting files (artifactUri). "artifactUri": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket containing model artifacts is deleted or its permissions are changed: The Model may fail to load artifacts and serve predictions. If the Model is updated: The Storage Bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index 838b4f03..c07e1167 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -115,10 +115,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pipeline job link { @@ -126,10 +122,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pipeline", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Deployed model endpoint link { @@ -137,10 +129,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-endpoint", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Storage bucket link (artifactUri) { @@ -148,10 +136,6 @@ func TestAIPlatformModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-model-artifacts", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go index 9709ffb8..42476847 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go @@ -31,19 +31,16 @@ var _ = registerableAdapter{ "runtimeConfig.gcsOutputDirectory": { Description: "If the Storage Bucket is deleted or updated: The PipelineJob may fail to write outputs. If the PipelineJob is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: gcpshared.ImpactInOnly, }, // The network attachment resource that the pipeline job will use for Private Service Connect. "pscInterfaceConfig.networkAttachment": { Description: "If the Compute Network Attachment is deleted or updated: The PipelineJob may lose access to network services via Private Service Connect. If the PipelineJob is updated: The network attachment remains unaffected.", ToSDPItemType: gcpshared.ComputeNetworkAttachment, - BlastPropagation: gcpshared.ImpactInOnly, }, // The schedule resource name, returned if the pipeline is created by the Schedule API. "scheduleName": { Description: "If the Vertex AI Schedule is deleted or updated: The PipelineJob may stop being triggered or may be triggered incorrectly. If the PipelineJob is updated: The schedule remains unaffected.", ToSDPItemType: gcpshared.AIPlatformSchedule, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go index ff5037d3..85e04021 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go @@ -72,10 +72,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "aiplatform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // network @@ -83,10 +79,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // encryptionSpec.kmsKeyName @@ -94,10 +86,6 @@ func TestAIPlatformPipelineJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go index 695dcdb7..a9900076 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go @@ -30,7 +30,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository is deleted or updated: The Docker Image may become invalid or inaccessible. If the Docker Image is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go index 0ed4ba85..833ad530 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go @@ -110,10 +110,6 @@ func TestArtifactRegistryDockerImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, repository), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/artifact-registry-repository.go b/sources/gcp/dynamic/adapters/artifact-registry-repository.go index c57545d6..51fcffe1 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-repository.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-repository.go @@ -28,29 +28,17 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Repository is deleted or updated: All associated Docker Images may become invalid or inaccessible. If a Docker Image is updated: The repository remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to upstream repositories in virtual repository configuration "virtualRepositoryConfig.upstreamPolicies.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If an upstream Artifact Registry Repository is deleted or updated: The virtual repository may fail to serve artifacts from that upstream. If the virtual repository is updated: The upstream repositories remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Link to Secret Manager Secret Version used for remote repository authentication "remoteRepositoryConfig.upstreamCredentials.passwordSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecretVersion, Description: "If the Secret Manager Secret Version is deleted or its access is revoked: The remote repository may fail to authenticate with upstream sources. If the remote repository is updated: The secret version remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go index f3e90cad..21fd7f14 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go @@ -29,17 +29,14 @@ var _ = registerableAdapter{ "destinationDatasetId": { ToSDPItemType: gcpshared.BigQueryDataset, Description: "If the BigQuery Dataset is deleted or updated: The transfer config may fail to write data. If the transfer config is updated: The dataset remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "dataSourceId": { ToSDPItemType: gcpshared.BigQueryDataTransferDataSource, Description: "If the Data Source is deleted or updated: The transfer config may fail to function. If the transfer config is updated: The data source remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "notificationPubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: Notifications may fail to be sent. If the transfer config is updated: The Pub/Sub topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "encryptionConfiguration.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "serviceAccountName": gcpshared.IAMServiceAccountImpactInOnly, @@ -50,10 +47,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigQueryDataTransferTransferRun, Description: "If the Transfer Config is deleted or updated: All associated transfer runs may become invalid or inaccessible. If a transfer run is updated: The transfer config remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go index d4909abf..e0fd1918 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go @@ -161,10 +161,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: destinationDatasetId, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // dataSourceId link - NOTE: BigQueryDataTransferDataSource adapter doesn't exist yet // TODO: Add test case when BigQueryDataTransferDataSource adapter is created @@ -174,10 +170,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // encryptionConfiguration.kmsKeyName link { @@ -185,10 +177,6 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go index ee413fd5..6b412b52 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go @@ -25,17 +25,14 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The AppProfile may become invalid or inaccessible. If the AppProfile is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "multiClusterRoutingUseAny.clusterIds": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The AppProfile may lose routing capabilities or fail to access data. If the AppProfile is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "singleClusterRouting.clusterId": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The AppProfile may lose routing capabilities or fail to access data. If the AppProfile is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go index abdd53a5..d966acc7 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go @@ -85,10 +85,6 @@ func TestBigTableAdminAppProfile(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for singleClusterRouting.clusterId → BigTableAdminCluster // Requires manual linker to combine instance name with cluster ID @@ -122,10 +118,6 @@ func TestBigTableAdminAppProfile(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add tests for multiClusterRoutingUseAny.clusterIds → BigTableAdminCluster // Requires manual linker to combine instance name with cluster IDs diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup.go b/sources/gcp/dynamic/adapters/big-table-admin-backup.go index 0386beca..5ba50cf2 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup.go @@ -26,17 +26,14 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceTable": { ToSDPItemType: gcpshared.BigTableAdminTable, Description: "If the BigTableAdmin Table is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceBackup": { ToSDPItemType: gcpshared.BigTableAdminBackup, Description: "If the source Backup is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The source backup remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go index a3a30bdb..2eba1222 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go @@ -75,10 +75,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, clusterName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // sourceTable @@ -86,10 +82,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "source-table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // sourceBackup @@ -97,10 +89,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, clusterName, "source-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // encryptionInfo.kmsKeyVersion @@ -108,10 +96,6 @@ func TestBigTableAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go index 478f9f0e..4cf5c585 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go @@ -31,10 +31,6 @@ var bigTableAdminClusterAdapter = registerableAdapter{ //nolint:unused "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The Cluster may become invalid or inaccessible. If the Cluster is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, // No Terraform mapping diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go index 2de47266..a253104a 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go @@ -172,10 +172,6 @@ func TestBigTableAdminCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-keyring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // name field creates a backlink to the BigTable instance @@ -183,10 +179,6 @@ func TestBigTableAdminCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index 47bc68c7..c0f410aa 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -27,10 +27,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Instance is deleted or updated: All associated Clusters may become invalid or inaccessible. If a Cluster is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go index 936adbbc..8ac43791 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go @@ -78,10 +78,6 @@ func TestBigTableAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 3a2b8930..8c7ffab8 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -27,18 +27,15 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The Table may become invalid or inaccessible. If the Table is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // If this table was restored from another data source (e.g. a backup), this field, restoreInfo, will be populated with information about the restore. "restoreInfo.backupInfo.sourceTable": { ToSDPItemType: gcpshared.BigTableAdminTable, Description: "If the source BigTableAdmin Table is deleted or updated: The restored table may become invalid or inaccessible. If the restored table is updated: The source table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "restoreInfo.backupInfo.sourceBackup": { ToSDPItemType: gcpshared.BigTableAdminBackup, Description: "If the source BigTableAdmin Backup is deleted or updated: The restored table may become invalid or inaccessible. If the restored table is updated: The source backup remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go index 363a8197..4e5e2984 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go @@ -75,10 +75,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // restoreInfo.backupInfo.sourceTable { @@ -86,10 +82,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "source-table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // restoreInfo.backupInfo.sourceBackup { @@ -97,10 +89,6 @@ func TestBigTableAdminTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(instanceName, "test-cluster", "test-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go index 43939e5d..4243b6f7 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go @@ -37,15 +37,10 @@ var _ = registerableAdapter{ "projectId": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Cloud Resource Manager Project is deleted or updated: The billing information may become invalid or inaccessible. If the billing info is updated: The project remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "billingAccountName": { ToSDPItemType: gcpshared.CloudBillingBillingAccount, Description: "If the Cloud Billing Billing Account is deleted or updated: The billing information may become invalid or inaccessible. If the billing info is updated: The billing account is impacted as well.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/cloud-build-build.go b/sources/gcp/dynamic/adapters/cloud-build-build.go index 7d3c5a0c..8f171550 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build.go @@ -28,58 +28,48 @@ var _ = registerableAdapter{ "source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The Cloud Build may fail to access source files. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "steps.name": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The Cloud Build may fail to pull the image. If the Cloud Build is updated: The Docker image remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "results.images": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Cloud Build is updated or deleted: The Artifact Registry Docker Images may no longer be valid or accessible. If the Docker Images are updated: The Cloud Build remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "images": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If any of the images fail to be pushed, the build status is marked FAILURE.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "logsBucket": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Cloud Build may fail to write logs. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "buildTriggerId": { // The ID of the BuildTrigger that triggered this build, if it was triggered automatically. ToSDPItemType: gcpshared.CloudBuildTrigger, Description: "If the Cloud Build Trigger is deleted or updated: The Cloud Build may not be retriggered as expected. If the Cloud Build is updated: The trigger remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Artifacts storage location (Cloud Storage bucket for build artifacts) "artifacts.objects.location": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket for artifacts is deleted or updated: The Cloud Build may fail to store build artifacts. If the Cloud Build is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Maven artifacts repository in Artifact Registry "artifacts.mavenArtifacts.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for Maven artifacts is deleted or updated: The Cloud Build may fail to store Maven artifacts. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // NPM packages repository in Artifact Registry "artifacts.npmPackages.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for NPM packages is deleted or updated: The Cloud Build may fail to store NPM packages. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Python packages repository in Artifact Registry "artifacts.pythonPackages.repository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Artifact Registry Repository for Python packages is deleted or updated: The Cloud Build may fail to store Python packages. If the Cloud Build is updated: The repository remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets used in the build (availableSecrets.secretManager[].version) // The version field contains the full path: projects/{project}/secrets/{secret}/versions/{version} @@ -87,13 +77,11 @@ var _ = registerableAdapter{ "availableSecrets.secretManager.version": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or its access is revoked: The Cloud Build may fail to access required secrets during execution. If the Cloud Build is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Worker pool used for the build (same as Cloud Functions - Run Worker Pool) "options.pool.name": { ToSDPItemType: gcpshared.RunWorkerPool, Description: "If the Cloud Run Worker Pool is deleted or misconfigured: The Cloud Build may fail to execute. If the Cloud Build is updated: The worker pool remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // KMS key for encrypting build logs (if using CMEK) "options.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/cloud-build-build_test.go b/sources/gcp/dynamic/adapters/cloud-build-build_test.go index dcea1448..3003bb38 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build_test.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build_test.go @@ -74,10 +74,6 @@ func TestCloudBuildBuild(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // serviceAccount @@ -85,10 +81,6 @@ func TestCloudBuildBuild(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "cloudbuild-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go index d8179495..6309c7fa 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go @@ -40,10 +40,6 @@ var _ = registerableAdapter{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerTagKey, Description: "They are tightly coupled", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go index afa06d04..b5f88440 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go @@ -81,10 +81,6 @@ func TestCloudResourceManagerTagValue(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: tagKeyID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function.go b/sources/gcp/dynamic/adapters/cloudfunctions-function.go index 93d1da6c..34adfece 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function.go @@ -33,68 +33,36 @@ var cloudFunctionAdapter = registerableAdapter{ //nolint:unused "buildConfig.source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage bucket is deleted or misconfigured: Function deployment may fail. If the function changes: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.sourceProvenance.resolvedStorageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage bucket is deleted or misconfigured: Function deployment may fail. If the function changes: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.workerPool": { ToSDPItemType: gcpshared.RunWorkerPool, Description: "If the Cloud Run Worker Pool is deleted or misconfigured: Function deployment may fail. If the function changes: The worker pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.dockerRepository": { ToSDPItemType: gcpshared.ArtifactRegistryRepository, Description: "If the Container Repository is deleted or misconfigured: Function deployment may fail. If the function changes: The repository remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "buildConfig.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "serviceConfig.vpcConnector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or misconfigured: Function outbound networking may fail. If the function changes: The connector remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "serviceConfig.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is deleted or misconfigured: Function execution may fail. If the function changes: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "serviceConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "eventTrigger.trigger": { ToSDPItemType: gcpshared.EventarcTrigger, Description: "If the Eventarc Trigger is deleted or misconfigured: Function event handling may fail. If the function changes: The trigger remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "eventTrigger.pubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or misconfigured: Function event handling may fail. If the function changes: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "eventTrigger.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go index 7e0cbe47..b4efc987 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go @@ -195,10 +195,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test storage bucket link (buildConfig.source.storageSource.bucket) { @@ -206,10 +202,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test service account link (serviceConfig.serviceAccountEmail) { @@ -217,10 +209,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-function@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Pub/Sub topic link (eventTrigger.pubsubTopic) { @@ -228,10 +216,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test event trigger service account link (eventTrigger.serviceAccountEmail) { @@ -239,10 +223,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("event-trigger@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Eventarc trigger link (eventTrigger.trigger) { @@ -250,10 +230,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-trigger"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Cloud Run service link (serviceConfig.service) { @@ -261,10 +237,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-function-service"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Artifact Registry repository link (buildConfig.dockerRepository) { @@ -272,10 +244,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-docker-repo"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test Cloud Run Worker Pool link (buildConfig.workerPool) { @@ -283,10 +251,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, "test-worker-pool"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Test resolved storage bucket link (buildConfig.sourceProvenance.resolvedStorageSource.bucket) { @@ -294,10 +258,6 @@ func TestCloudFunctionsFunction(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-resolved-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: serviceConfig.vpcConnector test case omitted because gcp-vpc-access-connector adapter doesn't exist } diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go index c7463a21..bade13c7 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go @@ -85,10 +85,6 @@ func TestComputeExternalVpnGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index ab10b9bb..c752cf41 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -24,10 +24,6 @@ var _ = registerableAdapter{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "sourceServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, "targetServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/compute-firewall_test.go b/sources/gcp/dynamic/adapters/compute-firewall_test.go index a5125654..689518e5 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall_test.go +++ b/sources/gcp/dynamic/adapters/compute-firewall_test.go @@ -83,10 +83,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // sourceServiceAccounts @@ -94,10 +90,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // targetServiceAccounts @@ -105,10 +97,6 @@ func TestComputeFirewall(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "target-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-global-address.go b/sources/gcp/dynamic/adapters/compute-global-address.go index 093b9129..b404d806 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address.go +++ b/sources/gcp/dynamic/adapters/compute-global-address.go @@ -38,10 +38,6 @@ var computeGlobalAddressAdapter = registerableAdapter{ //nolint:unused "ipCollection": { ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, Description: "If the Public Delegated Prefix is deleted or updated: The Global Address may fail to reserve IP addresses from the prefix. If the Global Address is updated: The Public Delegated Prefix remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-global-address_test.go b/sources/gcp/dynamic/adapters/compute-global-address_test.go index e66c34f7..ba95765b 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-address_test.go @@ -158,20 +158,12 @@ func TestComputeGlobalAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.12", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go index 663ceafe..b60b1ebb 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go @@ -41,20 +41,12 @@ var computeGlobalForwardingRuleAdapter = registerableAdapter{ //nolint:unused "backendService": { ToSDPItemType: gcpshared.ComputeBackendService, Description: "If the Backend Service is updated or deleted: The forwarding rule routing behavior changes or breaks. If the forwarding rule is updated or deleted: Traffic will stop or be re-routed affecting the backend service load.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target resource (polymorphic - can be TargetHttpProxy, TargetHttpsProxy, TargetTcpProxy, TargetSslProxy, TargetPool, TargetVpnGateway, or TargetInstance). // The ForwardingRuleTargetLinker function determines the actual target type from the URI. "target": { ToSDPItemType: gcpshared.ComputeTargetHttpProxy, // Default type, but ForwardingRuleTargetLinker will determine actual type from URI Description: "If the target resource (proxy, pool, gateway, or instance) is updated or deleted: The forwarding rule routing behavior changes or breaks. If the forwarding rule is updated or deleted: Traffic will stop or be re-routed affecting the target resource.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go index ed26f145..17e80308 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go @@ -223,50 +223,30 @@ func TestComputeGlobalForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-backend-service", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnet", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetHttpProxy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-proxy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go index 2c514227..82233640 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -82,10 +82,6 @@ func TestComputeHttpHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -129,10 +125,6 @@ func TestComputeHttpHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index d982abbf..e010f6da 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -25,20 +25,14 @@ var _ = registerableAdapter{ "properties.networkInterfaces.network": { Description: "If the network is deleted: Resources may experience connectivity changes or disruptions. If the template is deleted: Network itself is not affected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.subnetwork": { Description: "If the (sub)network is deleted: Resources may experience connectivity changes or disruptions. If the template is updated: Subnetwork itself is not affected.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.networkIP": { Description: "IP address are always tightly coupled with the Compute Instance Template.", ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.networkInterfaces.ipv6Address": gcpshared.IPImpactBothWays, "properties.networkInterfaces.accessConfigs.natIP": gcpshared.IPImpactBothWays, @@ -50,35 +44,24 @@ var _ = registerableAdapter{ "properties.disks.source": { Description: "If the Compute Disk is updated: Instance creation may fail or behave unexpectedly. If the template is deleted: Existing disks can be deleted.", ToSDPItemType: gcpshared.ComputeDisk, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.disks.initializeParams.diskName": { Description: "If the Compute Disk is updated: Instance creation may fail or behave unexpectedly. If the template is deleted: Existing disks can be deleted.", ToSDPItemType: gcpshared.ComputeDisk, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.disks.initializeParams.sourceImage": { Description: "If the Compute Image is updated: Instances created from this template may not boot correctly. If the template is updated: Image is not affected.", ToSDPItemType: gcpshared.ComputeImage, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.diskType": { Description: "If the Compute Disk Type is updated: New instances may fail to provision disks properly. If the template is updated: Disk type is not affected.", ToSDPItemType: gcpshared.ComputeDiskType, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, "properties.disks.initializeParams.sourceSnapshot": { Description: "If the Compute Snapshot is updated: The template may reference an invalid or incompatible snapshot. If the template is updated: no impact on snapshots.", ToSDPItemType: gcpshared.ComputeSnapshot, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, @@ -86,57 +69,44 @@ var _ = registerableAdapter{ "properties.disks.initializeParams.storagePool": { Description: "If the Compute Storage Pool is deleted: Disk provisioning for new instances may fail. If the template is updated: Pool is not affected.", ToSDPItemType: gcpshared.ComputeStoragePool, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.diskEncryptionKey.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "properties.disks.diskEncryptionKey.kmsKeyServiceAccount": gcpshared.IAMServiceAccountImpactInOnly, "properties.guestAccelerators.acceleratorType": { Description: "If the Compute Accelerator Type is updated: New instances may misconfigure or fail hardware initialization. If the template is updated: Accelerator is not affected.", ToSDPItemType: gcpshared.ComputeAcceleratorType, - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceInstance": { Description: "If the Compute Instance is updated: The template may reference an invalid or incompatible instance. If the template is deleted: The instance remains unaffected.", ToSDPItemType: gcpshared.ComputeInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, "sourceInstanceParams.diskConfigs.customImage": { Description: "If the Compute Image is updated: Instances created from this template may not boot correctly. If the template is updated: Image is not affected.", ToSDPItemType: gcpshared.ComputeImage, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.networkInterfaces.networkAttachment": { Description: "If the Compute Network Attachment is updated: Instances using the template may lose access to the network services. If the template is deleted: Attachment is not affected.", ToSDPItemType: gcpshared.ComputeNetworkAttachment, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.initializeParams.licenses": { Description: "If the Compute License is updated: New instances may violate license agreements or lose functionality. If the template is updated: License remains unaffected.", ToSDPItemType: gcpshared.ComputeLicense, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.disks.licenses": { Description: "If the Compute License is updated: New instances may violate license agreements or lose functionality. If the template is updated: License remains unaffected.", ToSDPItemType: gcpshared.ComputeLicense, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.reservationAffinity.values": { Description: "If the Compute Reservation is updated: new instances created using it may fail to launch. If the template is updated: no impacts on reservation.", ToSDPItemType: gcpshared.ComputeReservation, - BlastPropagation: gcpshared.ImpactInOnly, }, "properties.scheduling.nodeAffinities.values": { Description: "If the Compute Node Group is updated: Placement policies may break for new VMs. If the template is updated: Node affinity rules may change. Changing the affinity might cause new VMs to stop using that Node Group", ToSDPItemType: gcpshared.ComputeNodeGroup, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "properties.serviceAccounts.email": { Description: "If the IAM Service Account is deleted or updated: Instances created from this template may fail to authenticate or access required resources. If the template is updated: The service account remains unaffected.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-instance-template_test.go b/sources/gcp/dynamic/adapters/compute-instance-template_test.go index 5494bc3b..98b2fa68 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template_test.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template_test.go @@ -168,10 +168,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.diskName @@ -179,10 +175,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "disk-name", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.disks.source @@ -190,30 +182,18 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/debian-cloud/global/images/family/debian-11", ExpectedScope: "debian-cloud", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.networkInterfaces.networkIP @@ -221,10 +201,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.92", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6Address @@ -232,10 +208,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.natIP @@ -243,10 +215,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.93", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.externalIpv6 @@ -254,10 +222,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.accessConfigs.securityPolicy @@ -265,10 +229,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.natIP @@ -276,10 +236,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.240.17.94", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.externalIpv6 @@ -287,10 +243,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2600:1901:0:1234::3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.networkInterfaces.ipv6AccessConfigs.securityPolicy @@ -298,10 +250,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy-ipv6", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshot @@ -309,10 +257,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.resourcePolicies @@ -320,10 +264,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-resource-policy", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.storagePool @@ -331,10 +271,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-storage-pool", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.licenses @@ -342,10 +278,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "debian-11-bullseye-init-param", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.licenses @@ -353,10 +285,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "debian-11-bullseye-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyName @@ -364,10 +292,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|source-image-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceImageEncryptionKey.kmsKeyServiceAccount @@ -375,10 +299,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-image-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.guestAccelerators.acceleratorType @@ -386,10 +306,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-t4", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.scheduling.nodeAffinities.values @@ -397,10 +313,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-node-group", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // properties.reservationAffinity.values @@ -408,10 +320,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my-reservation", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.diskType @@ -419,10 +327,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyName @@ -430,10 +334,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|source-snapshot-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.initializeParams.sourceSnapshotEncryptionKey.kmsKeyServiceAccount @@ -441,10 +341,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-snapshot-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.diskEncryptionKey.kmsKeyName @@ -452,10 +348,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|my-keyring|disk-encryption-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // properties.disks.diskEncryptionKey.kmsKeyServiceAccount @@ -463,10 +355,6 @@ func TestComputeInstanceTemplate(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "disk-encryption-key-service-account@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go index 29c86303..ad8023b0 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go @@ -35,37 +35,21 @@ var _ = registerableAdapter{ "subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is updated: Endpoint reachability or configuration for the NEG may change. If the NEG is updated: The subnetwork remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing a Cloud Run Service "cloudRun.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The Cloud Run service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing an App Engine service "appEngine.service": { ToSDPItemType: gcpshared.AppEngineService, Description: "If the App Engine Service is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The App Engine service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Serverless NEG referencing a Cloud Function "cloudFunction.function": { ToSDPItemType: gcpshared.CloudFunctionsFunction, Description: "If the Cloud Function is updated or deleted: Requests routed via the NEG may fail or change behavior. If the NEG changes: The Cloud Function remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go index fb4fdd3c..1cd8c5b5 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go @@ -98,10 +98,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -109,10 +105,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Run service link { @@ -120,10 +112,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-cloud-run-service"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: App Engine service link test omitted because gcp-app-engine-service adapter doesn't exist yet // Cloud Function link @@ -132,10 +120,6 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "test-cloud-function"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-network.go b/sources/gcp/dynamic/adapters/compute-network.go index 5a328373..17ab1235 100644 --- a/sources/gcp/dynamic/adapters/compute-network.go +++ b/sources/gcp/dynamic/adapters/compute-network.go @@ -24,23 +24,14 @@ var _ = registerableAdapter{ "subnetworks": { Description: "If the Compute Subnetwork is deleted: The network remains unaffected, but its subnetwork configuration may change. If the network is deleted: All associated subnetworks are also deleted.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "peerings.network": { Description: "If the Compute Network Peering is deleted: The network remains unaffected, but its peering configuration may change. If the network is deleted: All associated peerings are also deleted.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "firewallPolicy": { Description: "If the Compute Firewall Policy is updated: The network's security posture may change. If the network is updated: The firewall policy remains unaffected, but its application to the network may change.", ToSDPItemType: gcpshared.ComputeFirewallPolicy, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-network_test.go b/sources/gcp/dynamic/adapters/compute-network_test.go index 896c93fe..70ab7cd8 100644 --- a/sources/gcp/dynamic/adapters/compute-network_test.go +++ b/sources/gcp/dynamic/adapters/compute-network_test.go @@ -80,10 +80,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // subnetworks @@ -91,10 +87,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // peerings.network @@ -102,10 +94,6 @@ func TestComputeNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peer-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // TODO: Add test for firewallPolicy → ComputeFirewallPolicy // Requires ComputeFirewallPolicy adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index ad467aff..abf127a8 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -44,18 +44,10 @@ var _ = registerableAdapter{ "defaultServiceAccount": { Description: "If the IAM Service Account is deleted: Project resources may fail to work as before. If the project is deleted: service account is deleted.", ToSDPItemType: gcpshared.IAMServiceAccount, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "usageExportLocation.bucketName": { Description: "If the Compute Bucket is deleted: Project usage export may fail. If the project is deleted: bucket is deleted.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-project_test.go b/sources/gcp/dynamic/adapters/compute-project_test.go index aecf096e..b7f54ade 100644 --- a/sources/gcp/dynamic/adapters/compute-project_test.go +++ b/sources/gcp/dynamic/adapters/compute-project_test.go @@ -61,10 +61,6 @@ func TestComputeProject(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // usageExportLocation.bucketName @@ -72,10 +68,6 @@ func TestComputeProject(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "usage-export-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go index f371cb95..a91500f0 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go @@ -41,18 +41,15 @@ var _ = registerableAdapter{ "parentPrefix": { ToSDPItemType: gcpshared.ComputePublicAdvertisedPrefix, Description: "If the Public Advertised Prefix is updated or deleted: the delegated prefix may become invalid or withdrawn. If the delegated prefix changes: the parent advertised prefix remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Each sub-prefix may be delegated to a specific project. "publicDelegatedSubPrefixs.delegateeProject": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the delegatee Project is deleted or disabled: usage of the delegated sub-prefix may stop working. If the delegated prefix changes: the project resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "publicDelegatedSubPrefixs.name": { ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, Description: "If the delegated sub-prefix is updated or deleted: usage of the sub-prefix may stop working. If the parent delegated prefix changes: the sub-prefix remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go index c4458ffc..195b7e7e 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go @@ -99,10 +99,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "delegatee-project-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Delegatee project 2 link { @@ -110,10 +106,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "delegatee-project-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Sub-prefix 1 link { @@ -121,10 +113,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sub-prefix-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Sub-prefix 2 link { @@ -132,10 +120,6 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sub-prefix-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment.go b/sources/gcp/dynamic/adapters/compute-region-commitment.go index 12ddbec4..369aa89a 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment.go @@ -26,18 +26,10 @@ var _ = registerableAdapter{ "reservations.name": { ToSDPItemType: gcpshared.ComputeReservation, Description: "If the Region Commitment is deleted or updated: Reservations that reference this commitment may lose associated discounts or resource guarantees. If the Reservation is updated or deleted: The commitment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Changes to reservations don't affect commitments - Out: true, // Changes to commitments affect reservations that reference them - }, }, "licenseResource.license": { ToSDPItemType: gcpshared.ComputeLicense, Description: "If the Region Commitment is deleted or updated: Licenses that reference this commitment won't be affected. If the License is updated or deleted: The commitment may lose associated discounts or resource guarantees.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go index 851ad18d..5a189545 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go @@ -90,20 +90,12 @@ func TestComputeRegionCommitment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-reservation", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeLicense.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-resource-policy.go b/sources/gcp/dynamic/adapters/compute-resource-policy.go index f22a4659..e7ee1eba 100644 --- a/sources/gcp/dynamic/adapters/compute-resource-policy.go +++ b/sources/gcp/dynamic/adapters/compute-resource-policy.go @@ -28,7 +28,6 @@ var _ = registerableAdapter{ "snapshotSchedulePolicy.snapshotProperties.storageLocations": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The Resource Policy may fail to create snapshots. If the Resource Policy is updated: The Storage Bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index 799573a0..aa3a63e6 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -26,54 +26,32 @@ var _ = registerableAdapter{ "network": { Description: "If the Compute Network is updated: The route may no longer be valid or correctly associated. If the route is updated: The network remains unaffected, but its routing behavior may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Network that the route forwards traffic to, so the relationship will/may be different "nextHopNetwork": { Description: "If the Compute Network is updated: The route may no longer forward traffic properly. If the route is updated: The network remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopIp": { Description: "The network IP address of an instance that should handle matching packets. Tightly coupled with the Compute Route.", ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopInstance": { Description: "If the Compute Instance is updated: Routes using it as a next hop may break or change behavior. If the route is deleted: The instance remains unaffected but traffic that was previously using that route will be impacted.", ToSDPItemType: gcpshared.ComputeInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, "nextHopVpnTunnel": { Description: "If the VPN Tunnel is updated: The route may no longer forward traffic properly. If the route is updated: The VPN tunnel remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeVpnTunnel, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopGateway": { Description: "If the Compute Gateway is updated: The route may no longer forward traffic properly. If the route is updated: The gateway remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeGateway, - BlastPropagation: gcpshared.ImpactInOnly, }, "nextHopHub": { // https://cloud.google.com/network-connectivity/docs/reference/networkconnectivity/rest/v1/projects.locations.global.hubs/get Description: "The full resource name of the Network Connectivity Center hub that will handle matching packets. If the hub is updated: The route may no longer forward traffic properly. If the route is updated: The hub remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.NetworkConnectivityHub, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopIlb": { // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get @@ -81,19 +59,11 @@ var _ = registerableAdapter{ // When it's a URL, it references the ForwardingRule. When it's an IP, it's the IP address of the forwarding rule. Description: "The URL to a forwarding rule of type loadBalancingScheme=INTERNAL that should handle matching packets, or the IP address of the forwarding rule. If the Forwarding Rule is updated or deleted: The route may no longer forward traffic properly. If the route is updated: The forwarding rule remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeForwardingRule, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "nextHopInterconnectAttachment": { // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get Description: "The URL to an InterconnectAttachment which is the next hop for the route. If the Interconnect Attachment is updated or deleted: The route may no longer forward traffic properly. If the route is updated: The interconnect attachment remains unaffected but traffic routed through it may be affected.", ToSDPItemType: gcpshared.ComputeInterconnectAttachment, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-route_test.go b/sources/gcp/dynamic/adapters/compute-route_test.go index 63d859d3..881d1cc1 100644 --- a/sources/gcp/dynamic/adapters/compute-route_test.go +++ b/sources/gcp/dynamic/adapters/compute-route_test.go @@ -76,10 +76,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopNetwork @@ -87,10 +83,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peer-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopIp @@ -98,10 +90,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopVpnTunnel @@ -109,10 +97,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-tunnel", ExpectedScope: "test-project.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // nextHopInstance @@ -120,10 +104,6 @@ func TestComputeRoute(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for nextHopGateway → ComputeGateway // Requires ComputeGateway adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/compute-router.go b/sources/gcp/dynamic/adapters/compute-router.go index 050fee7f..466d5d3d 100644 --- a/sources/gcp/dynamic/adapters/compute-router.go +++ b/sources/gcp/dynamic/adapters/compute-router.go @@ -33,10 +33,6 @@ var _ = registerableAdapter{ "interfaces.linkedInterconnectAttachment": { ToSDPItemType: gcpshared.ComputeInterconnectAttachment, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "interfaces.privateIpAddress": gcpshared.IPImpactBothWays, "interfaces.subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, @@ -47,28 +43,16 @@ var _ = registerableAdapter{ "nats.natIps": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NAT IP address is deleted or updated: The Router NAT may fail to function correctly. If the Router NAT is updated: The IP address remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "nats.drainNatIps": { ToSDPItemType: stdlib.NetworkIP, Description: "If the draining NAT IP address is deleted or updated: The Router NAT may fail to drain correctly. If the Router NAT is updated: The IP address remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "nats.subnetworks.name": gcpshared.ComputeSubnetworkImpactInOnly, "nats.nat64Subnetworks.name": gcpshared.ComputeSubnetworkImpactInOnly, "interfaces.linkedVpnTunnel": { ToSDPItemType: gcpshared.ComputeVpnTunnel, Description: "They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Child resource: RoutePolicy - Router can list all its route policies via listRoutePolicies // This is a link from parent to child via SEARCH @@ -76,10 +60,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ComputeRoutePolicy, Description: "If the Router is deleted or updated: All associated Route Policies may become invalid or inaccessible. If a Route Policy is updated: The router remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, // Router discovers all its Route Policies via SEARCH }, // Note: BgpRoute is also a child resource with listBgpRoutes endpoint, but we can only use "name" diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index 27777a40..b3415689 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -142,10 +142,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Interface private IP address { @@ -153,10 +149,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Interface subnetwork { @@ -164,10 +156,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Interconnect attachment link { @@ -175,10 +163,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-attachment", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // VPN tunnel link { @@ -186,10 +170,6 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-tunnel", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // BGP peer IP addresses { @@ -197,40 +177,24 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.4", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // NAT IP addresses { @@ -238,30 +202,18 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // NAT subnetworks { @@ -269,20 +221,12 @@ func TestComputeRouter(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nat-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nat64-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-storage-pool.go b/sources/gcp/dynamic/adapters/compute-storage-pool.go index 27b681b4..35f589ed 100644 --- a/sources/gcp/dynamic/adapters/compute-storage-pool.go +++ b/sources/gcp/dynamic/adapters/compute-storage-pool.go @@ -26,19 +26,11 @@ var _ = registerableAdapter{ "storagePoolType": { ToSDPItemType: gcpshared.ComputeStoragePoolType, Description: "If the Storage Pool Type is deleted or updated: The Storage Pool may fail to operate correctly or become invalid. If the Storage Pool is updated: The Storage Pool Type remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Link to the zone where the storage pool resides "zone": { ToSDPItemType: gcpshared.ComputeZone, Description: "If the Zone is deleted or becomes unavailable: The Storage Pool may become inaccessible. If the Storage Pool is updated: The Zone remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork.go b/sources/gcp/dynamic/adapters/compute-subnetwork.go index ac4ebec2..fe23869e 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork.go @@ -24,27 +24,15 @@ var _ = registerableAdapter{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "gatewayAddress": gcpshared.IPImpactBothWays, "secondaryIpRanges.reservedInternalRange": { Description: "If the Reserved Internal Range is deleted or updated: The subnetwork's secondary IP range configuration may become invalid. If the subnetwork is updated: The internal range remains unaffected.", ToSDPItemType: gcpshared.NetworkConnectivityInternalRange, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "ipCollection": { Description: "If the Public Delegated Prefix is deleted or updated: The subnetwork may lose its IP allocation source (BYOIP). If the subnetwork is updated: The prefix remains unaffected.", ToSDPItemType: gcpshared.ComputePublicDelegatedPrefix, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go index c93ba5c3..a3eff130 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go @@ -77,10 +77,6 @@ func TestComputeSubnetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // gatewayAddress @@ -88,10 +84,6 @@ func TestComputeSubnetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go index 51311677..9553913b 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go @@ -31,9 +31,6 @@ var _ = registerableAdapter{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTP proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go index 9fe965e3..0fd8c06d 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go @@ -80,10 +80,6 @@ func TestComputeTargetHttpProxy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-url-map", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go index aec3f12d..5d55d515 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go @@ -31,30 +31,18 @@ var _ = registerableAdapter{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTPS proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "sslCertificates": { ToSDPItemType: gcpshared.ComputeSSLCertificate, Description: "If the SSL Certificate is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The certificate resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "sslPolicy": { ToSDPItemType: gcpshared.ComputeSSLPolicy, Description: "If the SSL Policy is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The SSL policy resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "certificateMap": { ToSDPItemType: gcpshared.CertificateManagerCertificateMap, Description: "If the Certificate Map is updated or deleted: TLS handshakes may fail for the HTTPS proxy. If the proxy changes: The certificate map resource remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go index 7e38e34e..ae3caafb 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go @@ -84,30 +84,18 @@ func TestComputeTargetHttpsProxy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-url-map", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSSLCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-cert", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSSLPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-target-pool.go b/sources/gcp/dynamic/adapters/compute-target-pool.go index 73c0c581..d02d711d 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool.go @@ -39,17 +39,14 @@ var _ = registerableAdapter{ "instances": { ToSDPItemType: gcpshared.ComputeInstance, Description: "If the Compute Instance is deleted or updated: the pool membership becomes invalid or traffic may fail to reach it. If the pool is updated: the instance remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "healthChecks": { ToSDPItemType: gcpshared.ComputeHealthCheck, Description: "If the Health Check is updated or deleted: health status and traffic distribution may be affected. If the pool is updated: the health check remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "backupPool": { ToSDPItemType: gcpshared.ComputeTargetPool, Description: "If the backup Target Pool is updated or deleted: failover behavior may change. If this pool is updated: the backup pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-target-pool_test.go b/sources/gcp/dynamic/adapters/compute-target-pool_test.go index 49b7c8c1..a27c4b42 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool_test.go @@ -97,10 +97,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "instance-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Instance 2 link { @@ -108,10 +104,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "instance-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check 1 link { @@ -119,10 +111,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "health-check-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check 2 link { @@ -130,10 +118,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "health-check-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Backup pool link { @@ -141,10 +125,6 @@ func TestComputeTargetPool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backup-pool", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-url-map.go b/sources/gcp/dynamic/adapters/compute-url-map.go index 38215b50..7c73f2df 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map.go +++ b/sources/gcp/dynamic/adapters/compute-url-map.go @@ -8,10 +8,6 @@ import ( var computeBackendImpact = &gcpshared.Impact{ ToSDPItemType: gcpshared.ComputeBackendService, Description: "If the Backend Service or Backend Bucket is updated or deleted: The URL Map's routing behavior may change or break. If the URL Map changes: The backend service or bucket remains structurally unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } // URL Map (global, project-level) resource. diff --git a/sources/gcp/dynamic/adapters/compute-url-map_test.go b/sources/gcp/dynamic/adapters/compute-url-map_test.go index 8c61752f..7abb4516 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map_test.go +++ b/sources/gcp/dynamic/adapters/compute-url-map_test.go @@ -159,10 +159,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default service link { @@ -170,10 +166,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-path-matcher-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule service link { @@ -181,10 +173,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-path-rule-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Default route action weighted backend service link { @@ -192,10 +180,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Default route action request mirror backend service link { @@ -203,10 +187,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default route action weighted backend service link { @@ -214,10 +194,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pm-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path matcher default route action request mirror backend service link { @@ -225,10 +201,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pm-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule route action weighted backend service link { @@ -236,10 +208,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pr-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Path rule route action request mirror backend service link { @@ -247,10 +215,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pr-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule service link { @@ -258,10 +222,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-route-rule-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule route action weighted backend service link { @@ -269,10 +229,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-rr-weighted-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Route rule route action request mirror backend service link { @@ -280,10 +236,6 @@ func TestComputeUrlMap(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-rr-mirror-backend", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go index 50d5f83f..b3cb30b4 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go @@ -38,10 +38,6 @@ var _ = registerableAdapter{ "vpnInterfaces.interconnectAttachment": { ToSDPItemType: gcpshared.ComputeInterconnectAttachment, Description: "If the Interconnect Attachment is deleted or updated: The VPN gateway interface may fail to operate correctly. If the VPN gateway is deleted or updated: The interconnect attachment may become disconnected or unusable. They are tightly coupled.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go index 505bc6e9..f7342c49 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go @@ -91,30 +91,18 @@ func TestComputeVpnGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeInterconnectAttachment.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-attachment", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go index 4a5777ff..c01d507a 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go @@ -36,42 +36,22 @@ var _ = registerableAdapter{ "targetVpnGateway": { ToSDPItemType: gcpshared.ComputeTargetVpnGateway, Description: "If the Target VPN Gateway (Classic VPN) is deleted or updated: The VPN Tunnel may become invalid or fail to establish connections. If the VPN Tunnel is updated or deleted: The Target VPN Gateway may be affected as tunnels are tightly coupled to their gateway.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "vpnGateway": { ToSDPItemType: gcpshared.ComputeVpnGateway, Description: "If the HA VPN Gateway is deleted or updated: The VPN Tunnel may become invalid or fail to establish connections. If the VPN Tunnel is updated or deleted: The HA VPN Gateway may be affected as tunnels are tightly coupled to their gateway.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "peerExternalGateway": { ToSDPItemType: gcpshared.ComputeExternalVpnGateway, Description: "If the External VPN Gateway is deleted or updated: The VPN Tunnel may fail to establish connections with the peer. If the VPN Tunnel is updated or deleted: The External VPN Gateway remains unaffected, but the tunnel endpoint becomes inactive.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "peerGcpGateway": { ToSDPItemType: gcpshared.ComputeVpnGateway, Description: "If the peer HA VPN Gateway is deleted or updated: The VPN Tunnel may fail to establish VPC-to-VPC connections. If the VPN Tunnel is updated or deleted: The peer HA VPN Gateway remains unaffected, but the tunnel endpoint becomes inactive.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "router": { ToSDPItemType: gcpshared.ComputeRouter, Description: "If the Cloud Router is deleted or updated: The VPN Tunnel may lose dynamic routing capabilities (BGP). If the VPN Tunnel is updated or deleted: The Cloud Router may lose routes advertised through this tunnel.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go index 1c8ea8c3..4902c9b5 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go @@ -94,10 +94,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target VPN Gateway link (Classic VPN) { @@ -105,10 +101,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // VPN Gateway link (HA VPN) { @@ -116,10 +108,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Peer External Gateway link { @@ -127,10 +115,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-external-gateway", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Peer GCP Gateway link { @@ -138,10 +122,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-peer-gcp-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Router link { @@ -149,10 +129,6 @@ func TestComputeVpnTunnel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-router", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/container-cluster.go b/sources/gcp/dynamic/adapters/container-cluster.go index 6a773438..b99851df 100644 --- a/sources/gcp/dynamic/adapters/container-cluster.go +++ b/sources/gcp/dynamic/adapters/container-cluster.go @@ -40,12 +40,10 @@ var _ = registerableAdapter{ "nodePools.config.nodeGroup": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the referenced Node Group is deleted or updated: Node pools backed by it may fail to create or manage nodes. Updates to the node pool will not affect the node group.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "notificationConfig.pubsub.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the referenced Pub/Sub topic is deleted or updated: Notifications may fail to be sent. Updates to the cluster will not affect the topic.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // The Cloud KMS cryptoKeyVersions to use for signing service account JWTs issued by this cluster. // Format: projects/{project}/locations/{location}/keyRings/{keyring}/cryptoKeys/{cryptoKey}/cryptoKeyVersions/{cryptoKeyVersion} @@ -63,7 +61,6 @@ var _ = registerableAdapter{ "resourceUsageExportConfig.bigqueryDestination.datasetId": { ToSDPItemType: gcpshared.BigQueryDataset, Description: "If the referenced BigQuery dataset is deleted or updated: Resource usage export may fail. Updates to the cluster will not affect the dataset.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // The IP address of this cluster's master endpoint. "endpoint": gcpshared.IPImpactBothWays, @@ -72,10 +69,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ContainerNodePool, Description: "If the Container Cluster is deleted or updated: All associated Node Pools may become invalid or inaccessible. If a Node Pool is updated: The cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 9bd38706..8de4abb5 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -144,10 +144,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -155,10 +151,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -166,10 +158,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-service-account@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Boot disk KMS key link { @@ -177,10 +165,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Node group link { @@ -188,10 +172,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-node-group", ExpectedScope: fmt.Sprintf("%s.%s", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub topic link { @@ -199,10 +179,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account signing key version link { @@ -210,10 +186,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account verification key version link { @@ -221,10 +193,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key", "2"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Control plane disk encryption key link { @@ -232,10 +200,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "control-plane-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // ETCD backup encryption key link { @@ -243,10 +207,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "etcd-backup-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Database encryption key link { @@ -254,10 +214,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "database-encryption-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // BigQuery dataset link { @@ -265,10 +221,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "gke_usage_export", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master endpoint IP address link { @@ -276,10 +228,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "35.123.45.67", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Forward link to node pools (parent to child) { @@ -287,10 +235,6 @@ func TestContainerCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: combinedQuery, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/container-node-pool.go b/sources/gcp/dynamic/adapters/container-node-pool.go index 92588928..088505c6 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool.go +++ b/sources/gcp/dynamic/adapters/container-node-pool.go @@ -41,24 +41,18 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the Container Cluster is deleted or updated: The Node Pool may become invalid or inaccessible. If the Node Pool is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.bootDiskKmsKey": gcpshared.CryptoKeyImpactInOnly, "config.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "config.nodeGroup": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the node pool is backed by a node group, then changes to the node group may affect the node pool. Changes to the node pool will not affect the node group.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.network": gcpshared.ComputeNetworkImpactInOnly, "config.subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "instanceGroupUrls": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The Node Pool may fail to create new nodes or become invalid. If the Node Pool is updated: The instance group manager remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/container-node-pool_test.go b/sources/gcp/dynamic/adapters/container-node-pool_test.go index d7cb3770..2630d2f6 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool_test.go +++ b/sources/gcp/dynamic/adapters/container-node-pool_test.go @@ -106,10 +106,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, clusterName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // KMS encryption key link { @@ -117,10 +113,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -128,10 +120,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Node group link { @@ -139,10 +127,6 @@ func TestContainerNodePool(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: fmt.Sprintf("%s.%s-a", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/dataform-repository.go b/sources/gcp/dynamic/adapters/dataform-repository.go index 3a67b2b7..a19dffa7 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository.go +++ b/sources/gcp/dynamic/adapters/dataform-repository.go @@ -29,25 +29,21 @@ var _ = registerableAdapter{ "gitRemoteSettings.authenticationTokenSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to authenticate with the Git remote. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The name of the Secret Manager secret version to use as a ssh private key for Git operations. Must be in the format projects/*/secrets/*/versions/*. "gitRemoteSettings.sshAuthenticationConfig.userPrivateKeySecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to authenticate with the Git remote. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Name of the Secret Manager secret version used to interpolate variables into the .npmrc file for package installation operations. "npmrcEnvironmentVariablesSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Dataform Repository may fail to install npm packages. If the Dataform Repository is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The URL of the Git remote repository. Can be HTTPS (e.g., https://github.com/user/repo.git) or SSH (e.g., git@github.com:user/repo.git). "gitRemoteSettings.url": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the Git remote URL becomes inaccessible: The Dataform Repository may fail to sync with the remote. If the Dataform Repository is updated: The Git remote remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // The service account to run workflow invocations under. "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/dataform-repository_test.go b/sources/gcp/dynamic/adapters/dataform-repository_test.go index 1529449b..47011624 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository_test.go +++ b/sources/gcp/dynamic/adapters/dataform-repository_test.go @@ -71,10 +71,6 @@ func TestDataformRepository(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dataform-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // kmsKeyName @@ -82,10 +78,6 @@ func TestDataformRepository(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan.go b/sources/gcp/dynamic/adapters/dataplex-data-scan.go index 83013316..16bb666a 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan.go @@ -36,9 +36,6 @@ var _ = registerableAdapter{ "data.entity": { ToSDPItemType: gcpshared.DataplexEntity, Description: "If the Dataplex Entity is deleted: The data scan will fail to access the data source. If the data scan is updated: The dataplex entity remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "data.resource": { // Note: data.resource can reference either a Storage Bucket (for DataDiscoveryScan) @@ -47,24 +44,15 @@ var _ = registerableAdapter{ // the BigQueryTable linker automatically. ToSDPItemType: gcpshared.StorageBucket, Description: "If the data source (Storage Bucket or BigQuery Table) is deleted or inaccessible: The data scan will fail to access the data source. If the data scan is updated: The data source remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Post-scan action BigQuery table exports "dataQualitySpec.postScanActions.bigqueryExport.resultsTable": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table for exporting data quality scan results is deleted or inaccessible: The post-scan action will fail. If the data scan is updated: The BigQuery table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "dataProfileSpec.postScanActions.bigqueryExport.resultsTable": { ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery table for exporting data profile scan results is deleted or inaccessible: The post-scan action will fail. If the data scan is updated: The BigQuery table remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go index 693ee367..af4552bf 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go @@ -96,10 +96,6 @@ func TestDataplexDataScan(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: bucketName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: data.entity link also exists but DataplexEntity adapter doesn't exist yet } diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster.go b/sources/gcp/dynamic/adapters/dataproc-cluster.go index 25ec4843..fc2b7496 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster.go @@ -40,92 +40,74 @@ var _ = registerableAdapter{ "config.masterConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.masterConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.masterConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.workerConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.imageUri": { ToSDPItemType: gcpshared.ComputeImage, Description: "If the Image is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.managedGroupConfig.instanceGroupManagerUri": { ToSDPItemType: gcpshared.ComputeInstanceGroupManager, Description: "If the Instance Group Manager is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.secondaryWorkerConfig.accelerators.acceleratorTypeUri": { ToSDPItemType: gcpshared.ComputeAcceleratorType, Description: "If the Accelerator Type is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.autoscalingConfig.policyUri": { ToSDPItemType: gcpshared.DataprocAutoscalingPolicy, Description: "If the Autoscaling Policy is deleted or updated: The cluster may fail to scale. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.auxiliaryNodeGroups.nodeGroup.name": { ToSDPItemType: gcpshared.ComputeNodeGroup, Description: "If the Node Group is deleted or updated: The cluster may fail to create new nodes. If the cluster is updated: The existing nodes remain unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.tempBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The cluster may fail to stage data or logs. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.stagingBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The cluster may fail to stage job dependencies, configuration files, or job driver console output. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "config.metastoreConfig.dataprocMetastoreService": { ToSDPItemType: gcpshared.DataprocMetastoreService, Description: "If the Dataproc Metastore Service is deleted or updated: The cluster may lose access to centralized metadata or fail to operate correctly. If the cluster is updated: The metastore service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.kubernetesClusterConfig.gkeClusterConfig.gkeClusterTarget": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Cluster is deleted or updated: The Dataproc virtual cluster may become invalid or inaccessible. If the Dataproc cluster is updated: The GKE cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.kubernetesClusterConfig.gkeClusterConfig.nodePoolTarget.nodePool": { ToSDPItemType: gcpshared.ContainerNodePool, Description: "If the GKE Node Pool is deleted or updated: The Dataproc virtual cluster may fail to schedule workloads or lose capacity. If the Dataproc cluster is updated: The node pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.stagingBucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The virtual cluster may fail to stage job dependencies, configuration files, or job driver console output. If the cluster is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "virtualClusterConfig.auxiliaryServicesConfig.sparkHistoryServerConfig.dataprocCluster": { ToSDPItemType: gcpshared.DataprocCluster, Description: "If the Spark History Server Dataproc Cluster is deleted or updated: The cluster may lose access to Spark job history or fail to monitor Spark applications. If the cluster is updated: The Spark History Server cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go index 9d008e9a..e4ba123a 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go @@ -112,40 +112,24 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default-subnet", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.IAMServiceAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master config (SEARCH with full ImageUri) { @@ -153,10 +137,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/master-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Master accelerator { @@ -164,10 +144,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-t4", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Worker config (SEARCH with full ImageUri) { @@ -175,10 +151,6 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/worker-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Secondary worker config (SEARCH with full ImageUri) { @@ -186,30 +158,18 @@ func TestDataprocCluster(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/secondary-dataproc-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-temp-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.DataprocAutoscalingPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone.go b/sources/gcp/dynamic/adapters/dns-managed-zone.go index 470ae6c2..5d7096c1 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone.go @@ -26,19 +26,11 @@ var _ = registerableAdapter{ "dnsName": { ToSDPItemType: stdlib.NetworkDNS, Description: "Tightly coupled with the DNS Managed Zone.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // nameServers is an array of DNS names assigned to the managed zone (output only) "nameServers": { ToSDPItemType: stdlib.NetworkDNS, Description: "Nameservers assigned to the managed zone are tightly coupled with the DNS Managed Zone.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "privateVisibilityConfig.networks.networkUrl": gcpshared.ComputeNetworkImpactInOnly, // The resource name of the cluster to bind this ManagedZone to. This should be specified in the format like: projects/*/locations/*/clusters/*. @@ -47,7 +39,6 @@ var _ = registerableAdapter{ "privateVisibilityConfig.gkeClusters.gkeClusterName": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Container Cluster is deleted or updated: The DNS Managed Zone may lose visibility for that cluster or fail to resolve names. If the DNS Managed Zone is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "forwardingConfig.targetNameServers.ipv4Address": gcpshared.IPImpactBothWays, "forwardingConfig.targetNameServers.ipv6Address": gcpshared.IPImpactBothWays, @@ -59,7 +50,6 @@ var _ = registerableAdapter{ "serviceDirectoryConfig.namespace.namespaceUrl": { ToSDPItemType: gcpshared.ServiceDirectoryNamespace, Description: "If the Service Directory Namespace is deleted or updated: The DNS Managed Zone may lose its association or fail to resolve names. If the DNS Managed Zone is updated: The namespace remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go index 1000e5a4..620da6c6 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go @@ -91,10 +91,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com.", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // privateVisibilityConfig.networks.networkUrl @@ -102,23 +98,15 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for privateVisibilityConfig.gkeClusters.gkeClusterName → ContainerCluster - // Requires adapter to define BlastPropagation (currently only has ToSDPItemType) + // Link from adapter (ToSDPItemType only) { // forwardingConfig.targetNameServers.ipv4Address ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.10", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // forwardingConfig.targetNameServers.ipv6Address @@ -126,10 +114,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // peeringConfig.targetNetwork.networkUrl @@ -137,10 +121,6 @@ func TestDNSManagedZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "peering-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add test for serviceDirectoryConfig.namespace.namespaceUrl → ServiceDirectoryNamespace // Requires ServiceDirectoryNamespace adapter to be implemented first diff --git a/sources/gcp/dynamic/adapters/eventarc-trigger.go b/sources/gcp/dynamic/adapters/eventarc-trigger.go index 93844c6e..7f892729 100644 --- a/sources/gcp/dynamic/adapters/eventarc-trigger.go +++ b/sources/gcp/dynamic/adapters/eventarc-trigger.go @@ -37,82 +37,46 @@ var eventarcTriggerAdapter = registerableAdapter{ //nolint:unused "channel": { ToSDPItemType: gcpshared.EventarcChannel, Description: "If the Eventarc Channel is deleted or updated: The trigger may fail to receive events. If the trigger is updated: The channel remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Run Service destination "destination.cloudRunService.service": { ToSDPItemType: gcpshared.RunService, Description: "If the Cloud Run Service is deleted or updated: The trigger may fail to deliver events to the service. If the trigger is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Cloud Function destination (fully qualified resource name) "destination.cloudFunction": { ToSDPItemType: gcpshared.CloudFunctionsFunction, Description: "If the Cloud Function is deleted or updated: The trigger may fail to deliver events to the function. If the trigger is updated: The function remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // GKE Cluster destination "destination.gke.cluster": { ToSDPItemType: gcpshared.ContainerCluster, Description: "If the GKE Cluster is deleted or updated: The trigger may fail to deliver events to services in the cluster. If the trigger is updated: The cluster remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Workflow destination (fully qualified resource name) "destination.workflow": { ToSDPItemType: gcpshared.WorkflowsWorkflow, Description: "If the Workflow is deleted or updated: The trigger may fail to invoke the workflow. If the trigger is updated: The workflow remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // HTTP endpoint URI (standard library resource) "destination.httpEndpoint.uri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint is unavailable or misconfigured: The trigger may fail to deliver events. If the trigger is updated: The HTTP endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network Attachment for VPC-internal HTTP endpoints "destination.httpEndpoint.networkConfig.networkAttachment": { ToSDPItemType: gcpshared.ComputeNetworkAttachment, Description: "If the Network Attachment is deleted or updated: The trigger may fail to access VPC-internal HTTP endpoints. If the trigger is updated: The network attachment remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub Topic used as transport mechanism "transport.pubsub.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The trigger may fail to transport events. If the trigger is updated: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Pub/Sub Subscription created and managed by Eventarc (output only) "transport.pubsub.subscription": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription is deleted or updated: The trigger may fail to receive events from the topic. If the trigger is updated: The subscription may be recreated or updated by Eventarc.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/file-instance.go b/sources/gcp/dynamic/adapters/file-instance.go index ba02b1cd..7cb37edb 100644 --- a/sources/gcp/dynamic/adapters/file-instance.go +++ b/sources/gcp/dynamic/adapters/file-instance.go @@ -41,7 +41,6 @@ var _ = registerableAdapter{ "fileShares.sourceBackup": { ToSDPItemType: gcpshared.FileBackup, Description: "If the referenced Backup is deleted or updated: Restores or incremental backups may fail. If the File instance is updated: The backup remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/file-instance_test.go b/sources/gcp/dynamic/adapters/file-instance_test.go index 1c51a916..ec7de556 100644 --- a/sources/gcp/dynamic/adapters/file-instance_test.go +++ b/sources/gcp/dynamic/adapters/file-instance_test.go @@ -112,10 +112,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -123,10 +119,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // KMS key link { @@ -134,10 +126,6 @@ func TestFileInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/logging-bucket_test.go b/sources/gcp/dynamic/adapters/logging-bucket_test.go index d8abb7c5..20ab9db0 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket_test.go +++ b/sources/gcp/dynamic/adapters/logging-bucket_test.go @@ -74,10 +74,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cmekSettings.kmsKeyVersionName @@ -85,10 +81,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cmekSettings.serviceAccountId @@ -96,10 +88,6 @@ func TestLoggingBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "cmek-p123456789@gcp-sa-logging.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/logging-link.go b/sources/gcp/dynamic/adapters/logging-link.go index 3395de37..8229b158 100644 --- a/sources/gcp/dynamic/adapters/logging-link.go +++ b/sources/gcp/dynamic/adapters/logging-link.go @@ -29,15 +29,10 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Logging Link may lose its association or fail to function as expected. If the Logging Link is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "bigqueryDataset.datasetId": { Description: "They are tightly coupled with the Logging Link.", ToSDPItemType: gcpshared.BigQueryDataset, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/logging-link_test.go b/sources/gcp/dynamic/adapters/logging-link_test.go index 34f5bab7..9f586df4 100644 --- a/sources/gcp/dynamic/adapters/logging-link_test.go +++ b/sources/gcp/dynamic/adapters/logging-link_test.go @@ -73,10 +73,6 @@ func TestLoggingLink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, bucketName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryDataset.datasetId @@ -84,10 +80,6 @@ func TestLoggingLink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test_dataset", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go index ba26ed13..c1f0a0b1 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go @@ -39,12 +39,10 @@ var _ = registerableAdapter{ "notificationChannels": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "The notification channels that are used to notify when this alert policy is triggered. If notification channels are deleted, the alert policy will not be able to notify when triggered. If the alert policy is deleted, the notification channels will not be affected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "alertStrategy.notificationChannelStrategy.notificationChannelNames": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "The notification channels specified in the alert strategy for channel-specific renotification behavior. If these notification channels are deleted, the alert policy will not be able to notify when triggered. If the alert policy is deleted, the notification channels will not be affected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index 7519ca32..64f756c4 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -113,20 +113,12 @@ func TestMonitoringAlertPolicy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-channel-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.MonitoringNotificationChannel.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-channel-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go index db62c788..30682972 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go @@ -41,13 +41,11 @@ var _ = registerableAdapter{ "labels.topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Notification Channel may fail to send alerts. If the Notification Channel is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // For webhook_basicauth and webhook_tokenauth type notification channels, the url label contains the HTTP/HTTPS endpoint "labels.url": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint is unavailable or updated: The Notification Channel may fail to send alerts. If the Notification Channel is updated: The endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy.go b/sources/gcp/dynamic/adapters/orgpolicy-policy.go index aca9b5a8..8da96f47 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy.go @@ -46,7 +46,6 @@ var orgPolicyPolicyAdapter = registerableAdapter{ //nolint:unused // the actual type (project, folder, or organization) based on the name prefix ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the parent resource (project, folder, or organization) is deleted or updated: The Org Policy may become invalid or inaccessible. If the Org Policy is updated: The parent resource remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Note: spec.rules[].condition.expression contains CEL expressions that may reference // Tag Keys and Tag Values via resource.matchTag() or resource.matchTagId(). diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index 9bab54fb..15606da7 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -27,17 +27,14 @@ var _ = registerableAdapter{ "topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Subscription may fail to receive messages. If the Subscription is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "deadLetterPolicy.deadLetterTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Dead Letter Topic is deleted or updated: The Subscription may fail to deliver failed messages. If the Subscription is updated: The dead letter topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "pushConfig.pushEndpoint": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP push endpoint is unavailable or updated: The Subscription may fail to deliver messages via push. If the Subscription is updated: The endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "pushConfig.oidcToken.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "bigqueryConfig.table": { @@ -45,19 +42,16 @@ var _ = registerableAdapter{ // We have a manual adapter for this. ToSDPItemType: gcpshared.BigQueryTable, Description: "If the BigQuery Table is deleted or updated: The Subscription may fail to write data. If the Subscription is updated: The table remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "bigqueryConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "cloudStorageConfig.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Subscription may fail to write data. If the Subscription is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "cloudStorageConfig.serviceAccountEmail": gcpshared.IAMServiceAccountImpactInOnly, "analyticsHubSubscriptionInfo.subscription": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription is deleted or updated: The Analytics Hub Subscription may fail to receive messages. If the Analytics Hub Subscription is updated: The Pub/Sub Subscription remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index ed52fdbc..e20edf51 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -89,10 +89,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // deadLetterPolicy.deadLetterTopic @@ -100,10 +96,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dead-letter-topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // pushConfig.pushEndpoint @@ -111,10 +103,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/push-endpoint", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // pushConfig.oidcToken.serviceAccountEmail @@ -122,10 +110,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("push-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryConfig.table @@ -133,10 +117,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test_dataset", "test_table"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // bigqueryConfig.serviceAccountEmail @@ -144,10 +124,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("bq-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cloudStorageConfig.bucket @@ -155,10 +131,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // cloudStorageConfig.serviceAccountEmail @@ -166,10 +138,6 @@ func TestPubSubSubscription(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("storage-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index 4a739305..fa7b001b 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -29,61 +29,47 @@ var _ = registerableAdapter{ "schemaSettings.schema": { ToSDPItemType: gcpshared.PubSubSchema, Description: "If the Pub/Sub Schema is deleted or updated: The Topic may fail to validate messages. If the Topic is updated: The schema remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Settings for ingestion from a data source into this topic. "ingestionDataSourceSettings.cloudStorage.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.streamArn": { ToSDPItemType: aws.KinesisStream, Description: "The Kinesis stream ARN to ingest data from. If the Kinesis stream is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The stream remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.consumerArn": { ToSDPItemType: aws.KinesisStreamConsumer, Description: "The Kinesis consumer ARN used for ingestion in Enhanced Fan-Out mode. The consumer must be already created and ready to be used. If the consumer is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The consumer remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.awsRoleArn": { ToSDPItemType: aws.IAMRole, Description: "AWS role to be used for Federated Identity authentication with Kinesis. If the AWS IAM role is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The role remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsKinesis.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with AWS Kinesis. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.clusterArn": { ToSDPItemType: aws.MSKCluster, Description: "AWS MSK cluster ARN to ingest data from. If the MSK cluster is deleted or updated: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The cluster remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.awsRoleArn": { ToSDPItemType: aws.IAMRole, Description: "AWS role to be used for Federated Identity authentication with AWS MSK. If the AWS IAM role is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The role remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.awsMsk.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with AWS MSK. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "ingestionDataSourceSettings.confluentCloud.bootstrapServer": { ToSDPItemType: stdlib.NetworkDNS, Description: "Confluent Cloud bootstrap server endpoint (hostname:port). The linker automatically detects whether the value is a DNS name or IP address and creates the appropriate link. If the bootstrap server is unreachable: The Pub/Sub Topic may fail to receive data. If the Topic is updated: The bootstrap server remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "ingestionDataSourceSettings.confluentCloud.gcpServiceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "GCP service account used for federated identity authentication with Confluent Cloud. If the service account is deleted or updated: The Pub/Sub Topic may fail to authenticate and receive data. If the Topic is updated: The service account remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/pubsub-topic_test.go b/sources/gcp/dynamic/adapters/pubsub-topic_test.go index d966d592..5a9b6a2a 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic_test.go @@ -73,10 +73,6 @@ func TestPubSubTopic(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // ingestionDataSourceSettings.cloudStorage.bucket @@ -84,10 +80,6 @@ func TestPubSubTopic(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "ingestion-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // TODO: Add tests for AWS Kinesis ingestion settings (streamAr, consumerArn, awsRoleArn) // Requires cross-cloud linking setup diff --git a/sources/gcp/dynamic/adapters/redis-instance.go b/sources/gcp/dynamic/adapters/redis-instance.go index b30d6f1b..143e69f8 100644 --- a/sources/gcp/dynamic/adapters/redis-instance.go +++ b/sources/gcp/dynamic/adapters/redis-instance.go @@ -52,7 +52,6 @@ var _ = registerableAdapter{ "serverCaCerts.cert": { ToSDPItemType: gcpshared.ComputeSSLCertificate, Description: "If the certificate is deleted or updated: The Redis instance may lose secure connectivity. If the Redis instance is updated: The certificate remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index 2eec3db8..e9588cae 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -115,10 +115,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Customer managed key link { @@ -126,10 +122,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Host IP address link { @@ -137,10 +129,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Read endpoint IP address link { @@ -148,10 +136,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.101", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Server CA certificate link { @@ -159,10 +143,6 @@ func TestRedisInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "-----BEGIN CERTIFICATE-----\nMIIC...test certificate data...\n-----END CERTIFICATE-----", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/run-revision.go b/sources/gcp/dynamic/adapters/run-revision.go index 6e56d253..38893cea 100644 --- a/sources/gcp/dynamic/adapters/run-revision.go +++ b/sources/gcp/dynamic/adapters/run-revision.go @@ -33,55 +33,45 @@ var _ = registerableAdapter{ "service": { ToSDPItemType: gcpshared.RunService, Description: "If the Run Service is deleted or updated: The Revision may lose its association or fail to run. If the Revision is updated: The service remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The network remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The subnetwork remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The Revision may lose connectivity or fail to run as expected. If the Revision is updated: The connector remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "containers.image": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The Revision may fail to pull the image. If the Revision is updated: The Docker image remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.cloudSqlInstance.instances": { // Format: {project}:{location}:{instance} // The manual adapter linker handles this format automatically. ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The Revision may fail to access the database. If the Revision is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The Revision may fail to access the GCS volume. If the Revision is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The Revision may fail to access sensitive data mounted as a volume. If the Revision is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "volumes.nfs.server": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NFS server (IP address or hostname) becomes unavailable: The Revision may fail to mount the NFS volume. If the Revision is updated: The NFS server remains unaffected. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: gcpshared.ImpactInOnly, }, "logUri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the log URI endpoint becomes unavailable: The Revision logs may not be accessible. If the Revision is updated: The log URI endpoint remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionKey": gcpshared.CryptoKeyImpactInOnly, }, diff --git a/sources/gcp/dynamic/adapters/run-revision_test.go b/sources/gcp/dynamic/adapters/run-revision_test.go index 8848555f..3a5a3d65 100644 --- a/sources/gcp/dynamic/adapters/run-revision_test.go +++ b/sources/gcp/dynamic/adapters/run-revision_test.go @@ -72,10 +72,6 @@ func TestRunRevision(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // serviceAccount @@ -83,10 +79,6 @@ func TestRunRevision(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "run-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/run-service.go b/sources/gcp/dynamic/adapters/run-service.go index 0cfa1cba..252cd7e5 100644 --- a/sources/gcp/dynamic/adapters/run-service.go +++ b/sources/gcp/dynamic/adapters/run-service.go @@ -35,68 +35,53 @@ var _ = registerableAdapter{ "template.vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The connector remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The network remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The service may lose connectivity or fail to route traffic correctly. If the service is updated: The subnetwork remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.containers.image": { ToSDPItemType: gcpshared.ArtifactRegistryDockerImage, Description: "If the Artifact Registry Docker Image is deleted or updated: The service may fail to deploy new revisions. If the service is updated: The Docker image remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.containers.env.valueSource.secretKeyRef.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the referenced Secret Manager Secret is deleted or updated: the container may fail to start or access sensitive configuration. If the service is updated: the secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The service may fail to access sensitive data. If the service is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.cloudSqlInstance.instances": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The service may fail to access the database. If the service is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The service may fail to access stored data. If the service is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, "template.encryptionKey": gcpshared.CryptoKeyImpactInOnly, "latestCreatedRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The service status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "latestReadyRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The service status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, "traffic.revision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: Traffic allocation to revisions will be lost. If revisions are updated: The service traffic configuration may need updates.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Forward link from parent to child via SEARCH // Link to all revisions in this service "name": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Service is deleted or updated: All associated Revisions may become invalid or inaccessible. If a Revision is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to Binary Authorization platform policy (when explicitly specified via policy field) @@ -105,31 +90,21 @@ var _ = registerableAdapter{ "binaryAuthorization.policy": { ToSDPItemType: gcpshared.BinaryAuthorizationPlatformPolicy, Description: "If the Binary Authorization platform policy is updated: The service may fail to deploy new revisions if images don't meet policy requirements. If the service is updated: The policy remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Link to Cloud Storage bucket used in buildConfig source (if buildConfig is used) "buildConfig.source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket containing source code is deleted or updated: The service may fail to build new revisions. If the service is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Link to HTTP/HTTPS URLs serving traffic for this service "urls": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint becomes unavailable: The service cannot serve traffic. If the service is updated: The endpoint URL may change.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Link to main URI serving traffic for this service "uri": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the HTTP endpoint becomes unavailable: The service cannot serve traffic. If the service is updated: The endpoint URI may change.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/run-service_test.go b/sources/gcp/dynamic/adapters/run-service_test.go index 05e42b12..f8f09e55 100644 --- a/sources/gcp/dynamic/adapters/run-service_test.go +++ b/sources/gcp/dynamic/adapters/run-service_test.go @@ -156,10 +156,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.vpcAccess.networkInterfaces.network { @@ -167,10 +163,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.vpcAccess.networkInterfaces.subnetwork { @@ -178,10 +170,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, location), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.containers.env.valueSource.secretKeyRef.secret { @@ -189,10 +177,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "api-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.secret.secret { @@ -200,10 +184,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "db-creds", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.cloudSqlInstance.instances { @@ -211,10 +191,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-db", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.volumes.gcs.bucket { @@ -222,10 +198,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // template.encryptionKey { @@ -233,10 +205,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // latestReadyRevision { @@ -244,10 +212,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // latestCreatedRevision { @@ -255,10 +219,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-2"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // traffic.revision { @@ -266,10 +226,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, serviceName, "rev-3"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // name (parent to child search) { @@ -277,10 +233,6 @@ func TestRunService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(location, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/run-worker-pool.go b/sources/gcp/dynamic/adapters/run-worker-pool.go index 412cd99f..fdec386b 100644 --- a/sources/gcp/dynamic/adapters/run-worker-pool.go +++ b/sources/gcp/dynamic/adapters/run-worker-pool.go @@ -40,88 +40,71 @@ var _ = registerableAdapter{ "template.vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, Description: "If the VPC Access Connector is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The connector remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // VPC Network for direct VPC egress "template.vpcAccess.networkInterfaces.network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "If the Compute Network is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The network remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // VPC Subnetwork for direct VPC egress "template.vpcAccess.networkInterfaces.subnetwork": { ToSDPItemType: gcpshared.ComputeSubnetwork, Description: "If the Compute Subnetwork is deleted or updated: The worker pool may lose connectivity or fail to route traffic correctly. If the worker pool is updated: The subnetwork remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Service Mesh for advanced networking "template.serviceMesh.mesh": { ToSDPItemType: gcpshared.NetworkServicesMesh, Description: "If the Network Services Mesh is deleted or updated: The worker pool may lose service mesh connectivity or fail to communicate with other mesh services. If the worker pool is updated: The mesh remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets mounted as volumes "template.volumes.secret.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager Secret is deleted or updated: The worker pool may fail to access sensitive data mounted as volumes. If the worker pool is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Cloud SQL instances mounted as volumes "template.volumes.cloudSqlInstance.instances": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The worker pool may fail to access the database. If the worker pool is updated: The instance remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // GCS buckets mounted as volumes "template.volumes.gcs.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Cloud Storage Bucket is deleted or updated: The worker pool may fail to access stored data. If the worker pool is updated: The bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // NFS server (IP address or DNS name) - auto-detected by linker "template.volumes.nfs.server": { ToSDPItemType: stdlib.NetworkIP, Description: "If the NFS server (IP address or hostname) becomes unavailable: The worker pool may fail to mount the NFS volume. If the worker pool is updated: The NFS server remains unaffected. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Secret Manager secrets used in environment variables "template.containers.env.valueSource.secretKeyRef.secret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the referenced Secret Manager Secret is deleted or updated: The container may fail to start or access sensitive configuration. If the worker pool is updated: The secret remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Binary Authorization policy "binaryAuthorization.policy": { ToSDPItemType: gcpshared.BinaryAuthorizationPlatformPolicy, Description: "If the Binary Authorization policy is deleted or updated: The worker pool may fail to deploy new revisions if they don't meet policy requirements. If the worker pool is updated: The policy remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // Latest ready revision - child resource "latestReadyRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Latest created revision - child resource "latestCreatedRevision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Instance split revisions - child resources "instanceSplits.revision": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: Associated revisions may become orphaned or be deleted. If revisions are updated: The worker pool status may reflect the changes.", - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Forward link from parent to child via SEARCH - discover all revisions in this worker pool "name": { ToSDPItemType: gcpshared.RunRevision, Description: "If the Cloud Run Worker Pool is deleted or updated: All associated Revisions may become invalid or inaccessible. If a Revision is updated: The worker pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret.go b/sources/gcp/dynamic/adapters/secret-manager-secret.go index 43389084..b83d2107 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret.go @@ -41,10 +41,6 @@ var _ = registerableAdapter{ "topics.name": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or its policy changes: Secret event notifications may fail. If the Secret changes: The topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go index a7e360f0..5e34b28e 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go @@ -135,10 +135,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // topics.name { @@ -146,10 +142,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secret-events", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -184,10 +176,6 @@ func TestSecretManagerSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us-central1", "region-ring", "region-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go index ac2ee399..17e4331b 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go @@ -40,10 +40,6 @@ var _ = registerableAdapter{ "name": { Description: "If the parent Project, Folder, or Organization is deleted or updated: The Security Center Service may become invalid or inaccessible. If the Security Center Service is updated: The parent resource remains unaffected.", ToSDPItemType: gcpshared.CloudResourceManagerProject, // Manual linker handles detection of project/folder/organization from name prefix - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Note: Custom modules (SecurityHealthAnalyticsCustomModule, EventThreatDetectionCustomModule, etc.) // are not direct children in the API path structure - they are sibling resources under the same diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go index e5446b55..7f3df818 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go @@ -83,10 +83,6 @@ func TestSecurityCenterManagementSecurityCenterService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: projectID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint.go b/sources/gcp/dynamic/adapters/service-directory-endpoint.go index 754acc9a..a73140f4 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint.go @@ -27,7 +27,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ServiceDirectoryService, Description: "If the Service Directory Service is deleted or updated: The Endpoint may lose its association or fail to resolve names. If the Endpoint is updated: The service remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // An IPv4 or IPv6 address. "address": gcpshared.IPImpactBothWays, diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go index 71e078e8..5277bc3b 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go @@ -74,10 +74,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, namespace, serviceName), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // address @@ -85,10 +81,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // network @@ -96,10 +88,6 @@ func TestServiceDirectoryEndpoint(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/service-directory-service.go b/sources/gcp/dynamic/adapters/service-directory-service.go index dea25378..7ff59dbf 100644 --- a/sources/gcp/dynamic/adapters/service-directory-service.go +++ b/sources/gcp/dynamic/adapters/service-directory-service.go @@ -29,10 +29,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.ServiceDirectoryEndpoint, Description: "If the Service Directory Service is deleted or updated: All associated endpoints may become invalid or inaccessible. If an endpoint is updated: The service remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, // Link to IP addresses in endpoint addresses (if endpoints are included in the response) diff --git a/sources/gcp/dynamic/adapters/service-usage-service.go b/sources/gcp/dynamic/adapters/service-usage-service.go index 26e5141c..3da5bfec 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service.go +++ b/sources/gcp/dynamic/adapters/service-usage-service.go @@ -39,66 +39,40 @@ var _ = registerableAdapter{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Project is deleted or updated: The Service Usage Service may become invalid or inaccessible. If the Service Usage Service is updated: The project remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.name": { ToSDPItemType: stdlib.NetworkDNS, Description: "The DNS address at which this service is available. They are tightly coupled with the Service Usage Service.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.usage.producerNotificationChannel": { // Google Service Management currently only supports Google Cloud Pub/Sub as a notification channel. // To use Google Cloud Pub/Sub as the channel, this must be the name of a Cloud Pub/Sub topic ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Service Usage Service may fail to send notifications. If the Service Usage Service is updated: The topic remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, "config.endpoints.name": { ToSDPItemType: stdlib.NetworkDNS, Description: "The canonical DNS name of the endpoint. DNS names and endpoints are tightly coupled - if DNS resolution fails, the endpoint becomes inaccessible.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.endpoints.target": { // The target field can contain either an IP address or FQDN. // The linker automatically detects which type the value is and creates the appropriate link. ToSDPItemType: stdlib.NetworkIP, Description: "The address of the API frontend (IP address or FQDN). Network connectivity to this address is required for the endpoint to function. The linker automatically detects whether the value is an IP address or DNS name.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.endpoints.aliases": { // Note: This field is deprecated but may still be present in existing configurations. // The linker will process each alias in the array. ToSDPItemType: stdlib.NetworkDNS, Description: "Additional DNS names/aliases for the endpoint. DNS names and endpoints are tightly coupled - if DNS resolution fails, the endpoint becomes inaccessible.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "config.documentation.documentationRootUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "The HTTP/HTTPS URL to the root of the service documentation. HTTP connectivity to this URL is required to access the documentation.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "config.documentation.serviceRootUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "The HTTP/HTTPS service root URL. HTTP connectivity to this URL may be required for service operations.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/service-usage-service_test.go b/sources/gcp/dynamic/adapters/service-usage-service_test.go index 39c9029e..ee09fc9d 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service_test.go +++ b/sources/gcp/dynamic/adapters/service-usage-service_test.go @@ -72,10 +72,6 @@ func TestServiceUsageService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serviceName, ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/dynamic/adapters/spanner-backup.go b/sources/gcp/dynamic/adapters/spanner-backup.go index 970df1f6..69a10c59 100644 --- a/sources/gcp/dynamic/adapters/spanner-backup.go +++ b/sources/gcp/dynamic/adapters/spanner-backup.go @@ -26,25 +26,21 @@ var _ = registerableAdapter{ "name": { Description: "If the Spanner Instance is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance remains unaffected.", ToSDPItemType: gcpshared.SpannerInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, // Name of the database from which this backup is created. "database": { Description: "If the Spanner Database is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The database remains unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, // Names of databases restored from this backup. May be across instances. "referencingDatabases": { Description: "If any of the databases restored from this backup are deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The restored databases remain unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, // Names of destination backups copying this source backup. "referencingBackups": { Description: "If any of the destination backups copying this source backup are deleted or updated: The source backup may become invalid or inaccessible. If the source backup is updated: The destination backups remain unaffected.", ToSDPItemType: gcpshared.SpannerBackup, - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, // All Cloud KMS key versions used for encrypting the backup. @@ -53,13 +49,11 @@ var _ = registerableAdapter{ "backupSchedules": { Description: "If any of the backup schedules associated with this backup are deleted or updated: The Backup may stop being created automatically. If the Backup is updated: The backup schedules remain unaffected.", ToSDPItemType: gcpshared.SpannerBackupSchedule, - BlastPropagation: gcpshared.ImpactInOnly, }, // The instance partitions storing the backup (from the state at versionTime). "instancePartitions.instancePartition": { Description: "If any of the instance partitions storing this backup are deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance partitions remain unaffected.", ToSDPItemType: gcpshared.SpannerInstancePartition, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/spanner-database.go b/sources/gcp/dynamic/adapters/spanner-database.go index fab2e1ab..8cfb4b5f 100644 --- a/sources/gcp/dynamic/adapters/spanner-database.go +++ b/sources/gcp/dynamic/adapters/spanner-database.go @@ -30,13 +30,11 @@ var _ = registerableAdapter{ "restoreInfo.backupInfo.backup": { Description: "If the Spanner Backup is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The backup remains unaffected.", ToSDPItemType: gcpshared.SpannerBackup, - BlastPropagation: gcpshared.ImpactInOnly, }, // Source database from which the backup was taken (if database was restored from backup). "restoreInfo.backupInfo.sourceDatabase": { Description: "If the source Database is deleted or updated: The restored Database may become invalid or lose its restore point reference. If the restored Database is updated: The source database remains unaffected.", ToSDPItemType: gcpshared.SpannerDatabase, - BlastPropagation: gcpshared.ImpactInOnly, }, "encryptionInfo.kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, // This is a backlink to instance. @@ -48,7 +46,6 @@ var _ = registerableAdapter{ "name": { Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", ToSDPItemType: gcpshared.SpannerInstance, - BlastPropagation: gcpshared.ImpactInOnly, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/spanner-database_test.go b/sources/gcp/dynamic/adapters/spanner-database_test.go index a36db73b..53b76d3b 100644 --- a/sources/gcp/dynamic/adapters/spanner-database_test.go +++ b/sources/gcp/dynamic/adapters/spanner-database_test.go @@ -87,40 +87,24 @@ func TestSpannerDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-instance", "my-backup"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "array-key-1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // name field creates a backlink to the Spanner instance @@ -128,10 +112,6 @@ func TestSpannerDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/spanner-instance.go b/sources/gcp/dynamic/adapters/spanner-instance.go index 87aea189..6523ad15 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance.go +++ b/sources/gcp/dynamic/adapters/spanner-instance.go @@ -26,20 +26,12 @@ var spannerInstanceAdapter = registerableAdapter{ //nolint:unused "config": { ToSDPItemType: gcpshared.SpannerInstanceConfig, Description: "If the Spanner Instance Config is deleted or updated: The Spanner Instance may fail to operate correctly. If the Spanner Instance is updated: The config remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // This is a link from parent to child via SEARCH // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. "name": { ToSDPItemType: gcpshared.SpannerDatabase, Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/spanner-instance_test.go b/sources/gcp/dynamic/adapters/spanner-instance_test.go index 7990fbf1..ce1cc38b 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance_test.go +++ b/sources/gcp/dynamic/adapters/spanner-instance_test.go @@ -124,19 +124,12 @@ func TestSpannerInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "regional-us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.SpannerDatabase.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - Out: true, - }, + ExpectedScope: projectID, }, } diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go index 6f6a90c2..853d1569 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go @@ -28,10 +28,6 @@ var _ = registerableAdapter{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "They are tightly coupled", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "diskEncryptionConfiguration.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // The Cloud KMS key version used to encrypt the backup. diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup.go b/sources/gcp/dynamic/adapters/sql-admin-backup.go index 1edda8ad..4ee80888 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup.go @@ -28,10 +28,6 @@ var _ = registerableAdapter{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance cannot recover from the backup.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, "kmsKey": gcpshared.CryptoKeyImpactInOnly, "kmsKeyVersion": gcpshared.CryptoKeyVersionImpactInOnly, @@ -45,10 +41,6 @@ var _ = registerableAdapter{ "instanceSettings.settings.ipConfiguration.allocatedIpRange": { ToSDPItemType: gcpshared.NetworkConnectivityInternalRange, Description: "If the Reserved Internal Range is deleted or updated: The backup's instance settings snapshot may reference an invalid IP range configuration. If the backup is updated: The internal range remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index 05db9a4d..c84906f9 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -90,10 +90,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // kmsKey @@ -101,10 +97,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // kmsKeyVersion @@ -112,10 +104,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key", "1"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // instanceSettings.settings.ipConfiguration.privateNetwork @@ -123,10 +111,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // instanceSettings.settings.ipConfiguration.authorizedNetworks.value (first entry) @@ -134,10 +118,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // instanceSettings.settings.ipConfiguration.authorizedNetworks.value (second entry) @@ -145,10 +125,6 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "198.51.100.5/32", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Note: allocatedIpRange link is not tested here because the NetworkConnectivityInternalRange adapter doesn't exist yet. // The blast propagation is defined in the adapter so it will work automatically when the adapter is created. diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance.go b/sources/gcp/dynamic/adapters/sql-admin-instance.go index c0294b6c..ea4f8094 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance.go @@ -42,25 +42,21 @@ var _ = registerableAdapter{ "settings.sqlServerAuditConfig.bucket": { Description: "If the Storage Bucket is deleted or updated: The Cloud SQL Instance may fail to write audit logs. If the Cloud SQL Instance is updated: The bucket remains unaffected.", ToSDPItemType: gcpshared.StorageBucket, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Name of the primary (master) instance this replica depends on. "masterInstanceName": { Description: "If the master instance is deleted or updated: This replica may lose replication or become stale. If this replica is updated: The master remains unaffected.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Failover replica for high availability; changes in the failover target can impact this instance's HA posture. "failoverReplica.name": { Description: "If the failover replica is deleted or updated: High availability for this instance may be reduced or fail. If this instance is updated: The failover replica remains unaffected.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{In: true}, }, // Read replicas sourced from this primary instance. Changes to this instance can impact replicas, but replica changes typically do not impact the primary. "replicaNames": { Description: "If this primary instance is deleted or materially updated: Its replicas may become unavailable or invalid. Changes on replicas generally do not impact the primary.", ToSDPItemType: gcpshared.SQLAdminInstance, - BlastPropagation: &sdp.BlastPropagation{Out: true}, }, // Added: All assigned IP addresses (public or private). Treated as tightly coupled network identifiers. "ipAddresses.ipAddress": gcpshared.IPImpactBothWays, @@ -71,10 +67,6 @@ var _ = registerableAdapter{ "dnsName": { Description: "Tightly coupled with the Cloud SQL Instance endpoint.", ToSDPItemType: stdlib.NetworkDNS, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Authorized networks (CIDR ranges) allowed to connect to the instance. "settings.ipConfiguration.authorizedNetworks.value": gcpshared.IPImpactBothWays, @@ -82,21 +74,13 @@ var _ = registerableAdapter{ "settings.ipConfiguration.allocatedIpRange": { Description: "If the Subnetwork's secondary IP range is deleted or updated: The Cloud SQL Instance may fail to allocate private IP addresses. If the instance is updated: The subnetwork remains unaffected.", ToSDPItemType: gcpshared.ComputeSubnetwork, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // CA pool resource name when using customer-managed CAs. // Format: projects/{project}/locations/{region}/caPools/{caPoolId} // TODO: Private CA resource type (PrivateCACAPool) does not exist yet. Uncomment when created. // "settings.ipConfiguration.serverCaPool": { // Description: "If the Private CA Pool is deleted or updated: The Cloud SQL Instance may fail to use customer-managed certificates. If the instance is updated: The CA pool remains unaffected.", - // ToSDPItemType: gcpshared.PrivateCACAPool, - // BlastPropagation: &sdp.BlastPropagation{ - // In: false, - // Out: true, - // }, + // ToSDPItemType: gcpshared.PrivateCACAPool, // }, // Forward link from parent to child via SEARCH // Link to all backup runs for this instance @@ -111,10 +95,6 @@ var _ = registerableAdapter{ "name": { ToSDPItemType: gcpshared.SQLAdminBackupRun, Description: "If the Cloud SQL Instance is deleted or updated: All associated Backup Runs may become invalid or inaccessible. If a Backup Run is updated: The instance remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, IsParentToChild: true, }, }, diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index aef3f3eb..f27ce785 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -120,10 +120,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // diskEncryptionConfiguration.kmsKeyName { @@ -131,10 +127,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // settings.sqlServerAuditConfig.bucket { @@ -142,10 +134,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "audit-logs-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // ipAddresses.ipAddress { @@ -153,10 +141,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.50", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ipv6Address { @@ -164,10 +148,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // serviceAccountEmailAddress { @@ -175,10 +155,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // dnsName { @@ -186,10 +162,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-sql-instance.database.google.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // masterInstanceName { @@ -197,10 +169,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "master-instance", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // failoverReplica.name { @@ -208,10 +176,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "failover-replica", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // replicaNames[0] { @@ -219,10 +183,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "replica-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // replicaNames[1] { @@ -230,10 +190,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "replica-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, // name (parent to child search) { @@ -241,10 +197,6 @@ func TestSQLAdminInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: instanceName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index 5b8b862a..11c2249d 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -39,7 +39,6 @@ var _ = registerableAdapter{ "logging.logBucket": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Storage Bucket may fail to write logs. If the Storage Bucket is updated: The Logging Bucket remains unaffected.", - BlastPropagation: gcpshared.ImpactInOnly, }, // TODO: Add parent-to-child links once the child adapters are implemented: // - StorageBucketAccessControl (requires adapter implementation) diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index 30d9ac1a..5bac76a0 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -70,10 +70,6 @@ func TestStorageBucket(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index db91071e..692649d2 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -40,16 +40,10 @@ var _ = registerableAdapter{ "transferSpec.gcsDataSource.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the source GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The source bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "transferSpec.gcsDataSink.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the destination GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The destination bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: Investigate how we can link to AWS and Azure source when the account id (scope) is not available // https://cloud.google.com/storage-transfer/docs/reference/rest/v1/TransferSpec#AwsS3Data @@ -58,97 +52,59 @@ var _ = registerableAdapter{ "transferSpec.awsS3DataSource.credentialsSecret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager secret containing AWS credentials is deleted or updated: The transfer job may fail to authenticate with AWS S3. If the transfer job is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // AWS S3 data source CloudFront domain (HTTP endpoint) "transferSpec.awsS3DataSource.cloudfrontDomain": { ToSDPItemType: stdlib.NetworkHTTP, Description: "If the CloudFront domain endpoint is unreachable: The transfer job will fail to access the source data via CloudFront. If the transfer job is updated: The CloudFront endpoint remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Azure Blob Storage data source credentials secret (Secret Manager) "transferSpec.azureBlobStorageDataSource.credentialsSecret": { ToSDPItemType: gcpshared.SecretManagerSecret, Description: "If the Secret Manager secret containing Azure SAS token is deleted or updated: The transfer job may fail to authenticate with Azure Blob Storage. If the transfer job is updated: The secret remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Agent pool for POSIX source "transferSpec.sourceAgentPoolName": { ToSDPItemType: gcpshared.StorageTransferAgentPool, Description: "If the source Agent Pool is deleted or updated: The transfer job may fail to access POSIX source file systems. If the transfer job is updated: The agent pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Agent pool for POSIX sink "transferSpec.sinkAgentPoolName": { ToSDPItemType: gcpshared.StorageTransferAgentPool, Description: "If the sink Agent Pool is deleted or updated: The transfer job may fail to write to POSIX sink file systems. If the transfer job is updated: The agent pool remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Transfer manifest location (gs:// URI pointing to manifest file) "transferSpec.transferManifest.location": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket containing the transfer manifest is deleted or inaccessible: The transfer job may fail to read the manifest file. If the transfer job is updated: The bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // HTTP data source URL - link to HTTP endpoint using stdlib "transferSpec.httpDataSource.listUrl": { ToSDPItemType: stdlib.NetworkHTTP, Description: "HTTP data source URL for transfer operations. If the HTTP endpoint is unreachable: The transfer job will fail to access the source data.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, "transferSpec.gcsIntermediateDataLocation.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the intermediate GCS bucket is deleted or inaccessible: The transfer job will fail. If the transfer job is updated: The intermediate bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Replication spec source bucket "replicationSpec.gcsDataSource.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the source GCS bucket for replication is deleted or inaccessible: The replication job will fail. If the replication job is updated: The source bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Replication spec destination bucket "replicationSpec.gcsDataSink.bucketName": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the destination GCS bucket for replication is deleted or inaccessible: The replication job will fail. If the replication job is updated: The destination bucket remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, "serviceAccount": { ToSDPItemType: gcpshared.IAMServiceAccount, Description: "If the Service Account is deleted or permissions are revoked: The transfer job may fail to execute. If the transfer job is updated: The service account remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Notification configuration "notificationConfig.pubsubTopic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted: Transfer job notifications will fail. If the transfer job is updated: The Pub/Sub topic remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // TODO: Investigate whether we can/should support multiple items for a given key. // In this case, the eventStream can be an AWS SQS ARN in the form 'arn:aws:sqs:region:account_id:queue_name' @@ -158,18 +114,11 @@ var _ = registerableAdapter{ "eventStream.name": { ToSDPItemType: gcpshared.PubSubSubscription, Description: "If the Pub/Sub Subscription for event streaming is deleted: Transfer job events will not be consumed. If the transfer job is updated: The Pub/Sub subscription remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - }, }, // Latest transfer operation (child resource) "latestOperationName": { ToSDPItemType: gcpshared.StorageTransferTransferOperation, Description: "If the Transfer Operation is deleted or updated: The transfer job's latest operation reference may become invalid. If the transfer job is updated: The operation remains unaffected.", - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go index 8459a0fc..decea52a 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go @@ -120,10 +120,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsDataSink.bucketName { @@ -131,10 +127,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "dest-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // serviceAccount { @@ -142,10 +134,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // notificationConfig.pubsubTopic { @@ -153,10 +141,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "transfer-notifications", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -190,10 +174,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/urllist.tsv", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsDataSink.bucketName { @@ -201,10 +181,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "http-dest-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // transferSpec.gcsIntermediateDataLocation.bucketName { @@ -212,10 +188,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "intermediate-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // serviceAccount { @@ -223,10 +195,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa2@test-project.iam.gserviceaccount.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // eventStream.name { @@ -234,10 +202,6 @@ func TestStorageTransferTransferJob(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "transfer-events", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/integration-tests/compute-node-group_test.go b/sources/gcp/integration-tests/compute-node-group_test.go index e5431aa0..120cdcb0 100644 --- a/sources/gcp/integration-tests/compute-node-group_test.go +++ b/sources/gcp/integration-tests/compute-node-group_test.go @@ -204,11 +204,7 @@ func TestComputeNodeGroupIntegration(t *testing.T) { ExpectedType: gcpshared.ComputeNodeGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nodeTemplateName, - ExpectedScope: "*", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, + ExpectedScope: "*", }, } diff --git a/sources/gcp/manual/README.md b/sources/gcp/manual/README.md index 8b4f9aac..dc2e8b09 100644 --- a/sources/gcp/manual/README.md +++ b/sources/gcp/manual/README.md @@ -114,10 +114,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-dataset", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ... more test cases } diff --git a/sources/gcp/manual/big-query-dataset.go b/sources/gcp/manual/big-query-dataset.go index e541e64a..376011df 100644 --- a/sources/gcp/manual/big-query-dataset.go +++ b/sources/gcp/manual/big-query-dataset.go @@ -159,10 +159,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to contained tables. @@ -173,10 +169,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to contained routines. @@ -187,10 +179,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: parts[1], Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) for _, access := range metadata.Access { @@ -205,10 +193,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: access.Entity, Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -222,16 +206,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: access.Dataset.Dataset.DatasetID, Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - /* - A grant authorizing all resources of a particular type in a particular dataset access to this dataset. - Only views are supported for now. - The role field is not required when this field is set. - If that dataset is deleted and re-created, its access needs to be granted again via an update operation. - */ - In: false, - Out: true, - }, }) } } @@ -247,10 +221,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: shared.CompositeLookupKey(values...), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -267,10 +237,6 @@ func (b BigQueryDatasetWrapper) gcpBigQueryDatasetToItem(metadata *bigquery.Data Query: shared.CompositeLookupKey(values...), Scope: location.ToScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/big-query-dataset_test.go b/sources/gcp/manual/big-query-dataset_test.go index f69e1460..405939dc 100644 --- a/sources/gcp/manual/big-query-dataset_test.go +++ b/sources/gcp/manual/big-query-dataset_test.go @@ -45,70 +45,42 @@ func TestBigQueryDataset(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-user@example.com", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryDataset.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryModel.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryRoutine.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryTable.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/manual/big-query-model.go b/sources/gcp/manual/big-query-model.go index 2c6775ab..351e6631 100644 --- a/sources/gcp/manual/big-query-model.go +++ b/sources/gcp/manual/big-query-model.go @@ -91,10 +91,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc }, // Model is in a dataset, if dataset is deleted, model is deleted. // If the model is deleted, the dataset is not deleted. - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { @@ -107,10 +103,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Scope: location.ProjectID, Query: shared.CompositeLookupKey(values...), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -127,10 +119,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.EvaluationTable.TableId), }, // If the evaluation table is deleted or updated: The model's evaluation results may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -144,10 +132,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.TrainingTable.TableId), }, // If the training table is deleted or updated: The model's training data may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -161,10 +145,6 @@ func (m BigQueryModelWrapper) GCPBigQueryMetadataToItem(ctx context.Context, loc Query: shared.CompositeLookupKey(dataSetId, row.DataSplitResult.TestTable.TableId), }, // If the test table is deleted or updated: The model's test results may become invalid or inaccessible. If the model is updated: The table remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-model_test.go b/sources/gcp/manual/big-query-model_test.go index 0a96aac8..b8777462 100644 --- a/sources/gcp/manual/big-query-model_test.go +++ b/sources/gcp/manual/big-query-model_test.go @@ -52,20 +52,12 @@ func TestBigQueryModel(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryDataset.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/manual/big-query-routine.go b/sources/gcp/manual/big-query-routine.go index 12bc8d1e..d13b3692 100644 --- a/sources/gcp/manual/big-query-routine.go +++ b/sources/gcp/manual/big-query-routine.go @@ -177,23 +177,15 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: datasetID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to imported libraries (GCS buckets) for JavaScript routines // Format: gs://bucket-name/path/to/file.js if len(metadata.ImportedLibraries) > 0 { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { for _, libraryURI := range metadata.ImportedLibraries { if libraryURI != "" { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), libraryURI, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), libraryURI) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -232,11 +224,6 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: shared.CompositeLookupKey(location, connectionID), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the Connection is deleted or updated: The remote function may fail to authenticate. If the routine is updated: The connection remains unaffected. - In: true, - Out: false, - }, }) } } @@ -253,11 +240,6 @@ func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.Rout Query: endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // If the HTTP endpoint is unreachable: The remote function will fail to execute. If the routine is updated: The endpoint remains unaffected. - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-routine_test.go b/sources/gcp/manual/big-query-routine_test.go index 1832f751..05fd8d6e 100644 --- a/sources/gcp/manual/big-query-routine_test.go +++ b/sources/gcp/manual/big-query-routine_test.go @@ -53,10 +53,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Imported library GCS bucket link { @@ -64,10 +60,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Remote function connection link { @@ -75,10 +67,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|example-conn", ExpectedScope: "example", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Remote function HTTP endpoint link { @@ -86,10 +74,6 @@ func TestBigQueryRoutine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/run", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/manual/big-query-table.go b/sources/gcp/manual/big-query-table.go index e5caf391..d37fddb7 100644 --- a/sources/gcp/manual/big-query-table.go +++ b/sources/gcp/manual/big-query-table.go @@ -177,11 +177,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: parts[0], // dataset ID Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: true, - }, }) if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { @@ -197,11 +192,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(values...), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: false, - }, }) } } @@ -239,11 +229,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(connectionLocation, connectionID), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled. - In: true, - Out: true, - }, }) } } @@ -260,11 +245,7 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location // Use the StorageBucket linker to extract bucket name from various URI formats if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { // The linker handles gs:// URIs and extracts bucket names - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceURI, &sdp.BlastPropagation{ - // If the Storage Bucket is deleted or updated: The external table may fail to read data. If the table is updated: The bucket remains unaffected. - In: true, - Out: false, - }) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceURI) if linkedQuery != nil { // Create a unique key from query and scope to deduplicate bucketKey := fmt.Sprintf("%s|%s", linkedQuery.GetQuery().GetQuery(), linkedQuery.GetQuery().GetScope()) @@ -293,11 +274,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(baseTableRef.DatasetID, baseTableRef.TableID), Scope: baseTableRef.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the base table is deleted or updated: The snapshot may become invalid or inaccessible. If the snapshot is updated: The base table remains unaffected. - In: true, - Out: false, - }, }) } } @@ -316,11 +292,6 @@ func (b BigQueryTableWrapper) GCPBigQueryTableToItem(location gcpshared.Location Query: shared.CompositeLookupKey(baseTableRef.DatasetID, baseTableRef.TableID), Scope: baseTableRef.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If the base table is deleted or updated: The clone may become invalid or inaccessible. If the clone is updated: The base table remains unaffected. - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/big-query-table_test.go b/sources/gcp/manual/big-query-table_test.go index 9c7cb922..5b35e4fb 100644 --- a/sources/gcp/manual/big-query-table_test.go +++ b/sources/gcp/manual/big-query-table_test.go @@ -51,30 +51,18 @@ func TestBigQueryTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -104,30 +92,18 @@ func TestBigQueryTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: datasetID, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-ring", "test-key"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.BigQueryConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("us", "test-connection"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) 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 00e58eba..851e306d 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -351,10 +351,6 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { Query: "us|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -378,10 +374,6 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index f0495470..d468e638 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -349,10 +349,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -361,10 +357,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -373,10 +365,6 @@ func TestCloudKMSCryptoKey(t *testing.T) { Query: "global|test-keyring|test-key", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } @@ -400,30 +388,18 @@ func TestCloudKMSCryptoKey(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSKeyRing.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index cdd5518a..40f70979 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -353,10 +353,6 @@ func TestCloudKMSKeyRing(t *testing.T) { Query: "us|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -365,10 +361,6 @@ func TestCloudKMSKeyRing(t *testing.T) { Query: "us|test-keyring", Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } @@ -392,20 +384,12 @@ func TestCloudKMSKeyRing(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "us|test-keyring", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index f3c120d7..5308e640 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -246,10 +246,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -267,10 +263,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -284,10 +276,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -298,7 +286,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a ctx, location.ProjectID, userURI, - &sdp.BlastPropagation{In: true, Out: true}, ) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) @@ -319,10 +306,6 @@ func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, a Query: prefixName, Scope: fmt.Sprintf("%s.%s", location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index 41faf103..b40bc80d 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -54,30 +54,18 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -213,10 +201,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -224,10 +208,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -235,10 +215,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Regional forwarding rule link (from users) { @@ -246,10 +222,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-forwarding-rule", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Global forwarding rule link (from users) { @@ -257,10 +229,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-global-forwarding-rule", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Instance link (from users) { @@ -268,10 +236,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.us-central1-a", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Target VPN Gateway link (from users) { @@ -279,10 +243,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vpn-gateway", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Router link (from users) { @@ -290,10 +250,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-router", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -323,10 +279,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -334,10 +286,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IP address link { @@ -345,10 +293,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Public Delegated Prefix link (from ipCollection) { @@ -356,10 +300,6 @@ func TestComputeAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index ee85938e..43ecc387 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -240,10 +240,6 @@ func (c computeAutoscalerWrapper) gcpComputeAutoscalerToSDPItem(ctx context.Cont Query: igmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index 35422502..a386b311 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -89,10 +89,6 @@ func TestComputeAutoscalerWrapper(t *testing.T) { // [SPEC] Autoscalers are tightly coupled with the instance group manager // (albeit less strength on the IN direction). - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index 01a19395..0caf2267 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -333,10 +333,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc // This is a global resource Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -359,10 +355,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: securityPolicyName, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -378,10 +370,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: edgeSecurityPolicyName, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -410,10 +398,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, policyName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -448,10 +432,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: healthCheckName, Scope: healthCheckScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -475,10 +455,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: groupName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -502,10 +478,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } else { @@ -519,10 +491,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: negScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -541,10 +509,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: groupName, Scope: zone, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -567,10 +531,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, policyName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -593,10 +553,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: shared.CompositeLookupKey(location, bindingName), Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -624,10 +580,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: negName, Scope: negScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -653,10 +605,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc Query: instanceName, Scope: instanceScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -681,10 +629,6 @@ func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, sc // Regions are project-scoped resources Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 67adf147..49852200 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -122,60 +122,36 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: "test-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -300,70 +276,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeHealthCheck.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-health-check", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -396,70 +344,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeHealthCheck.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-regional-health-check", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -496,70 +416,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance-group", ExpectedScope: zone, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -600,80 +492,48 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetworkEndpointGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-neg", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceName, ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -706,70 +566,42 @@ func TestComputeBackendService(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeSecurityPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-edge-security-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkSecurityClientTlsPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-client-tls-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceLbPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-lb-policy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.NetworkServicesServiceBinding.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-location|test-service-binding", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: region, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index d3421599..0370c4d6 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -248,10 +248,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: diskTypeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -273,10 +269,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -298,10 +290,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +312,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: instantSnapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -350,10 +334,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: sourceDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -376,10 +356,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: rpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -401,10 +377,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: instanceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -433,10 +405,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its associated encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -464,10 +432,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its source image's encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -495,10 +459,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c }, // Deleting a key might break the disk’s ability to function and have its data read // Deleting a disk in GCP does not affect its source image's encryption key - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -520,10 +480,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: rpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -543,9 +499,8 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c // - gs://bucket-name/path/to/file // - bucket-name (without gs:// prefix) if sourceStorageObject := disk.GetSourceStorageObject(); sourceStorageObject != "" { - blastPropagation := &sdp.BlastPropagation{In: true, Out: false} if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceStorageObject, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), sourceStorageObject) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -569,10 +524,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Scope: scope, }, // If the Storage Pool is deleted or updated: The disk may fail to operate correctly or become invalid. If the disk is updated: The Storage Pool remains unaffected. - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -594,10 +545,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: primaryDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -617,10 +564,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -644,10 +587,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: secondaryDiskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -667,10 +606,6 @@ func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *c Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index 4d9daff0..f2cfe6df 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -67,7 +67,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -81,7 +80,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -95,7 +93,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instant-snapshot", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -109,7 +106,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, }, }, @@ -121,49 +117,42 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } userTest := shared.QueryTest{ ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, } diskTypeTest := shared.QueryTest{ ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } diskEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceImageEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceSnapshotEncryptionKeyTest := shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } sourceConsistencyGroupPolicy := shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, } for _, tc := range cases { @@ -364,56 +353,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -423,7 +404,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -454,56 +434,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -513,7 +485,6 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-pool", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -548,56 +519,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -608,14 +571,12 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "primary-disk", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, ) @@ -661,56 +622,48 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeInstance.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, { ExpectedType: gcpshared.ComputeDiskType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "pd-standard", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-group-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "projects/test-project-id/global/images/test-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } @@ -721,21 +674,18 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secondary-disk-1", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-consistency-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "secondary-disk-2", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, ) diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index fc5fa1eb..e03510e7 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -247,10 +247,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: rule.GetIPAddress(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -266,10 +262,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: backendServiceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -303,10 +295,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +312,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -335,10 +319,7 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont // Link to target resource (polymorphic) if target := rule.GetTarget(); target != "" { - linkedQuery := gcpshared.ForwardingRuleTargetLinker(location.ProjectID, location.ToScope(), target, &sdp.BlastPropagation{ - In: true, - Out: true, - }) + linkedQuery := gcpshared.ForwardingRuleTargetLinker(location.ProjectID, location.ToScope(), target) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -357,10 +338,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: forwardingRuleName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -379,10 +356,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: prefixName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -402,10 +375,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: query, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -424,10 +393,6 @@ func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx cont Query: query, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index 4cca216e..34ae9940 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -54,40 +54,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -212,40 +196,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -255,10 +223,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-target-proxy", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -289,40 +253,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -332,10 +280,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "base-forwarding-rule", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -366,40 +310,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -409,10 +337,6 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -449,40 +373,24 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeBackendService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "backend-service", ExpectedScope: fmt.Sprintf("%s.%s", projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -493,20 +401,12 @@ func TestComputeForwardingRule(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1|test-namespace", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ServiceDirectoryService.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1|test-namespace|test-service", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index ecaa9709..432a4b68 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -345,10 +345,6 @@ func GcpComputeHealthCheckToSDPItem(healthCheck *computepb.HealthCheck, location Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -362,10 +358,6 @@ func GcpComputeHealthCheckToSDPItem(healthCheck *computepb.HealthCheck, location Query: region, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -385,10 +377,6 @@ func linkHostToNetworkResource(sdpItem *sdp.Item, host string) { Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -398,10 +386,6 @@ func linkHostToNetworkResource(sdpItem *sdp.Item, host string) { Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 214df98f..16498bae 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -148,10 +148,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -180,10 +176,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -212,30 +204,18 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-east1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "europe-west1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -264,10 +244,6 @@ func TestComputeHealthCheck(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-image.go b/sources/gcp/manual/compute-image.go index 4bf32ac4..0138f67e 100644 --- a/sources/gcp/manual/compute-image.go +++ b/sources/gcp/manual/compute-image.go @@ -263,10 +263,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -283,10 +279,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: snapshotName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -306,10 +298,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -329,10 +317,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: licenseName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -340,9 +324,8 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image // Link to raw disk storage bucket if rawDisk := image.GetRawDisk(); rawDisk != nil { if rawDiskSource := rawDisk.GetSource(); rawDiskSource != "" { - blastPropagation := &sdp.BlastPropagation{In: true, Out: false} if linkFunc, ok := gcpshared.ManualAdapterLinksByAssetType[gcpshared.StorageBucket]; ok { - linkedQuery := linkFunc(location.ProjectID, location.ToScope(), rawDiskSource, blastPropagation) + linkedQuery := linkFunc(location.ProjectID, location.ToScope(), rawDiskSource) if linkedQuery != nil { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, linkedQuery) } @@ -381,10 +364,6 @@ func (c computeImageWrapper) gcpComputeImageToSDPItem(ctx context.Context, image Query: replacement, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -407,10 +386,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } else if loc != "" && keyRing != "" && cryptoKey != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -420,10 +395,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -448,10 +419,6 @@ func (c computeImageWrapper) addKMSKeyLinks(sdpItem *sdp.Item, keyName, kmsKeySe Query: serviceAccountEmail, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index df87913e..841133fd 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -55,10 +55,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-disk", ExpectedScope: fmt.Sprintf("%s.us-central1-a", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshot link { @@ -66,10 +62,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImage link (SEARCH handles full URI; createComputeImageWithLinks uses https URL) { @@ -77,10 +69,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/test-source-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // licenses link (first license) { @@ -88,10 +76,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license-1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // licenses link (second license) { @@ -99,10 +83,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license-2", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // rawDisk.source (GCS bucket) link { @@ -110,10 +90,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("%s-raw-disk-bucket", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // imageEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -121,10 +97,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-image-key|test-version-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // imageEncryptionKey.kmsKeyServiceAccount link { @@ -132,10 +104,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-image-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImageEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -143,10 +111,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-source-image-key|test-version-source-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceImageEncryptionKey.kmsKeyServiceAccount link { @@ -154,10 +118,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-source-image-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshotEncryptionKey.kmsKeyName (CryptoKeyVersion) link { @@ -165,10 +125,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-source-snapshot-key|test-version-source-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // sourceSnapshotEncryptionKey.kmsKeyServiceAccount link { @@ -176,10 +132,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: fmt.Sprintf("test-source-snapshot-kms-sa@%s.iam.gserviceaccount.com", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // deprecated.replacement link (SEARCH handles full URI) { @@ -187,10 +139,6 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/test-replacement-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-instance-group-manager-shared.go b/sources/gcp/manual/compute-instance-group-manager-shared.go index bd03d01e..e4a5b133 100644 --- a/sources/gcp/manual/compute-instance-group-manager-shared.go +++ b/sources/gcp/manual/compute-instance-group-manager-shared.go @@ -51,7 +51,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: instanceTemplateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -68,7 +67,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: instanceGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } @@ -84,7 +82,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -100,7 +97,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -118,7 +114,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -137,7 +132,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: targetPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } @@ -155,7 +149,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: resourcePolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -180,7 +173,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: versionTemplateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }) } } @@ -201,10 +193,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: healthCheckName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -224,7 +212,6 @@ func InstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *co Query: autoscalerName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index d6574624..0362f09b 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -67,50 +67,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -136,50 +116,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: gcpshared.RegionalScope(projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -228,10 +188,6 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "canary-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Stable version template (regional) { @@ -239,40 +195,24 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "stable-template", ExpectedScope: gcpshared.RegionalScope(projectID, region), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -318,10 +258,6 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Health check from auto-healing policy { @@ -329,50 +265,30 @@ func TestComputeInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-health-check", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1-a", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index e40b115e..0e9b7d47 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -234,10 +234,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: networkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -252,10 +248,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: subnetworkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -270,10 +262,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -288,10 +276,6 @@ func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGr Query: regionName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index 694be0ea..f0c86bc6 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -54,30 +54,18 @@ func TestComputeInstanceGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeZone.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: zone, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 64b65b46..408acb86 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -253,10 +253,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -277,10 +273,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -299,10 +291,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -324,10 +312,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -349,10 +333,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -376,10 +356,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -389,10 +365,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -410,10 +382,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkInterface.GetNetworkIP(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -425,10 +393,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkInterface.GetIpv6Address(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -442,10 +406,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: natIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if externalIPv6 := accessConfig.GetExternalIpv6(); externalIPv6 != "" { @@ -456,10 +416,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: externalIPv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -474,10 +430,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: externalIPv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -494,10 +446,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: subnetworkName, Scope: gcpshared.RegionalScope(location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -514,10 +462,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: networkName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -539,10 +483,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: resourcePolicyName, Scope: gcpshared.RegionalScope(location.ProjectID, region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -558,10 +498,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: email, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -577,10 +513,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: zoneName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -609,10 +541,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: templateName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -629,10 +557,6 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, Query: igmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index e15fd4ee..087d9a94 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -55,60 +55,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -329,60 +305,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -393,40 +345,24 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("projects/%s/global/images/test-image", projectID), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-image", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-snapshot", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) @@ -468,60 +404,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -531,10 +443,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-disk", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -575,60 +483,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -638,10 +522,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -678,60 +558,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -741,10 +597,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serviceAccountEmail, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -791,60 +643,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -855,20 +683,12 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceTemplateName, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, shared.QueryTest{ ExpectedType: gcpshared.ComputeInstanceGroupManager.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: igmName, ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) @@ -910,60 +730,36 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.3", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeSubnetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "default", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "network", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -974,10 +770,6 @@ func TestComputeInstance(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: instanceTemplateName, ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, ) diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index b5812b7d..ae284df9 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -235,10 +235,6 @@ func (c computeInstantSnapshotWrapper) gcpComputeInstantSnapshotToSDPItem(ctx co Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index 0a0f1b08..55d33474 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -75,10 +75,6 @@ func TestComputeInstantSnapshot(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-machine-image.go b/sources/gcp/manual/compute-machine-image.go index fb03630b..9b48c253 100644 --- a/sources/gcp/manual/compute-machine-image.go +++ b/sources/gcp/manual/compute-machine-image.go @@ -172,10 +172,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -193,10 +189,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: subnetworkName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -214,10 +206,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkAttachmentName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -231,10 +219,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: networkIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -246,10 +230,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -262,10 +242,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: natIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -279,10 +255,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: externalIpv6, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -302,10 +274,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) if sourceDiskEncryptionKey := disk.GetDiskEncryptionKey(); sourceDiskEncryptionKey != nil { @@ -330,10 +298,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: sourceImage, // Pass full URI so Search can detect format Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -351,10 +315,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: snapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -384,10 +344,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: saEmail, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -406,10 +362,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: acceleratorTypeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -433,10 +385,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: sourceInstanceName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -455,10 +403,6 @@ func (c computeMachineImageWrapper) gcpComputeMachineImageToSDPItem(ctx context. Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -496,10 +440,6 @@ func (c computeMachineImageWrapper) addKMSKeyLink(sdpItem *sdp.Item, keyName str Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index d6c53c04..a465f2cd 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -53,10 +53,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Subnetwork link { @@ -64,10 +60,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-subnetwork", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Network Attachment link { @@ -75,10 +67,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-network-attachment", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IPv4 internal IP address { @@ -86,10 +74,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // IPv6 internal address { @@ -97,10 +81,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // External IPv4 address (NAT IP) { @@ -108,10 +88,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // External IPv6 address { @@ -119,10 +95,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "2001:db8::2", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // Disk source link { @@ -130,10 +102,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Disk encryption key { @@ -141,10 +109,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source image link (SEARCH handles full URI) { @@ -152,10 +116,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://www.googleapis.com/compute/v1/projects/test-project-id/global/images/test-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source snapshot link { @@ -163,10 +123,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source image encryption key { @@ -174,10 +130,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source snapshot encryption key { @@ -185,10 +137,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Service account link { @@ -196,10 +144,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-sa@test-project-id.iam.gserviceaccount.com", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Accelerator type link { @@ -207,10 +151,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-k80", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Machine image encryption key { @@ -218,10 +158,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-machine-encryption-key", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Source instance link { @@ -229,10 +165,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instance", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // Saved disk link (from savedDisks) { @@ -240,10 +172,6 @@ func TestComputeMachineImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-saved-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index 6f166bc1..4cbe396a 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -298,10 +298,6 @@ func (c computeNodeGroupWrapper) gcpComputeNodeGroupToSDPItem(ctx context.Contex Query: name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index 798eb958..a8bb9987 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -52,10 +52,6 @@ func TestComputeNodeGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "node-template-1", ExpectedScope: "test-project.northamerica-northeast1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index bd158950..f8985a1e 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -229,10 +229,6 @@ func (c computeNodeTemplateWrapper) gcpComputeNodeTemplateToSDPItem(nodeTemplate Query: nodeTemplate.GetName(), Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) return sdpItem, nil diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index dc588cea..15f8ff72 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -90,10 +90,6 @@ func TestComputeNodeTemplate(t *testing.T) { ExpectedScope: "*", // [SPEC] The node groups does not affect the node template. - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go index edcdbe7e..52e9e64a 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -65,60 +65,36 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "unit-test-template", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeAutoscaler.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-autoscaler", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -143,60 +119,36 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "regional-template", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstanceGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-group", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeRegion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "us-central1", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeTargetPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeAutoscaler.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-autoscaler", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index 4cee42d3..e6d71c1b 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -236,10 +236,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: commitmentName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -261,10 +257,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: acceleratorName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -286,10 +278,6 @@ func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Co Query: policyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 84d76585..2af80217 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -48,30 +48,18 @@ func TestComputeReservation(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-commitment", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeAcceleratorType.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "nvidia-tesla-k80", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/compute-security-policy.go b/sources/gcp/manual/compute-security-policy.go index e1d7af7c..c14718d6 100644 --- a/sources/gcp/manual/compute-security-policy.go +++ b/sources/gcp/manual/compute-security-policy.go @@ -157,10 +157,6 @@ func (c computeSecurityPolicyWrapper) gcpComputeSecurityPolicyToSDPItem(security Query: shared.CompositeLookupKey(policyName, rulePriority), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index a17e3587..fe167514 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -51,10 +51,6 @@ func TestComputeSecurityPolicy(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-security-policy|1000", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } diff --git a/sources/gcp/manual/compute-snapshot.go b/sources/gcp/manual/compute-snapshot.go index 48da95fc..c6561b37 100644 --- a/sources/gcp/manual/compute-snapshot.go +++ b/sources/gcp/manual/compute-snapshot.go @@ -161,10 +161,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: licenseName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -182,10 +178,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: instantSnapshotName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -208,10 +200,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: diskName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -234,10 +222,6 @@ func (c computeSnapshotWrapper) gcpComputeSnapshotToSDPItem(ctx context.Context, Query: snapshotSchedulePolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -283,10 +267,6 @@ func (c computeSnapshotWrapper) addKMSKeyLink(sdpItem *sdp.Item, keyName string, Query: shared.CompositeLookupKey(loc, keyRing, cryptoKey, cryptoKeyVersion), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 194b1659..099afe76 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -51,67 +51,42 @@ func TestComputeSnapshot(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-license", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeInstantSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-instant-snapshot", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { ExpectedType: gcpshared.ComputeResourcePolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-snapshot-schedule-policy", ExpectedScope: "test-project-id.us-central1", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "global|test-keyring|test-key|test-version-snapshot", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } diff --git a/sources/gcp/manual/iam-service-account-key.go b/sources/gcp/manual/iam-service-account-key.go index ae64792e..5d7b72ec 100644 --- a/sources/gcp/manual/iam-service-account-key.go +++ b/sources/gcp/manual/iam-service-account-key.go @@ -200,13 +200,6 @@ func (c iamServiceAccountKeyWrapper) gcpIAMServiceAccountKeyToSDPItem(key *admin Query: serviceAccountName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // If service account is deleted, all keys that belong to it are deleted - // If key is deleted, resources using that particular key lose access to service-account. - // But account itself keeps working. - In: true, - Out: false, - }, }) return sdpItem, nil diff --git a/sources/gcp/manual/iam-service-account-key_test.go b/sources/gcp/manual/iam-service-account-key_test.go index 7fbcce52..82436a21 100644 --- a/sources/gcp/manual/iam-service-account-key_test.go +++ b/sources/gcp/manual/iam-service-account-key_test.go @@ -50,7 +50,6 @@ func TestIAMServiceAccountKey(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: testServiceAccount, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, }, } diff --git a/sources/gcp/manual/iam-service-account.go b/sources/gcp/manual/iam-service-account.go index 3703dca6..a567a723 100644 --- a/sources/gcp/manual/iam-service-account.go +++ b/sources/gcp/manual/iam-service-account.go @@ -172,10 +172,6 @@ func (c iamServiceAccountWrapper) gcpIAMServiceAccountToSDPItem(serviceAccount * Query: projectID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -190,10 +186,6 @@ func (c iamServiceAccountWrapper) gcpIAMServiceAccountToSDPItem(serviceAccount * Query: serviceAccountID, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } diff --git a/sources/gcp/manual/iam-service-account_test.go b/sources/gcp/manual/iam-service-account_test.go index c91fa7e8..9b89cf24 100644 --- a/sources/gcp/manual/iam-service-account_test.go +++ b/sources/gcp/manual/iam-service-account_test.go @@ -50,14 +50,12 @@ func TestIAMServiceAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-project-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }, { ExpectedType: gcpshared.IAMServiceAccountKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-service-account-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, } @@ -83,14 +81,12 @@ func TestIAMServiceAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-project-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }, { ExpectedType: gcpshared.IAMServiceAccountKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-service-account-id", ExpectedScope: "test-project-id", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: false, Out: true}, }, } diff --git a/sources/gcp/manual/logging-sink.go b/sources/gcp/manual/logging-sink.go index 9b058ade..2ffb53b8 100644 --- a/sources/gcp/manual/logging-sink.go +++ b/sources/gcp/manual/logging-sink.go @@ -157,10 +157,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: parts[1], // Bucket name Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to bucket affect sink - Out: false, // Changes to sink don't affect bucket - }, }) } case strings.HasPrefix(sink.GetDestination(), "bigquery.googleapis.com"): @@ -174,10 +170,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: values[1], // Dataset ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to dataset affect sink - Out: false, // Changes to sink don't affect dataset - }, }) } case strings.HasPrefix(sink.GetDestination(), "pubsub.googleapis.com"): @@ -191,10 +183,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: values[1], // Topic ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to topic affect sink - Out: false, // Changes to sink don't affect topic - }, }) } case strings.HasPrefix(sink.GetDestination(), "logging.googleapis.com"): @@ -208,10 +196,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: shared.CompositeLookupKey(values[1], values[2]), // location|bucket_ID Scope: values[0], // Project ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Changes to bucket affect sink - Out: false, // Changes to sink don't affect bucket - }, }) } } @@ -238,10 +222,6 @@ func (l loggingSinkWrapper) gcpLoggingSinkToItem(sink *loggingpb.LogSink, locati Query: writerIdentity, // Service account email Scope: projectID, // Project ID extracted from email }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If the service account is deleted or its permissions are changed: The sink may fail to export logs - Out: false, // Changes to the sink don't affect the service account - }, }) } } diff --git a/sources/gcp/manual/logging-sink_test.go b/sources/gcp/manual/logging-sink_test.go index 764f5888..ed908658 100644 --- a/sources/gcp/manual/logging-sink_test.go +++ b/sources/gcp/manual/logging-sink_test.go @@ -43,10 +43,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -57,10 +53,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_dataset", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -71,10 +63,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_topic", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -85,10 +73,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("global", "my_bucket"), ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -145,10 +129,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "my_bucket", ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, // IAM Service Account link from writerIdentity { @@ -156,10 +136,6 @@ func TestNewLoggingSink(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: writerIdentity, ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/shared/blast-propagations.go b/sources/gcp/shared/blast-propagations.go index a094f40f..ef5d9aec 100644 --- a/sources/gcp/shared/blast-propagations.go +++ b/sources/gcp/shared/blast-propagations.go @@ -1,63 +1,48 @@ package shared import ( - "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) type Impact struct { - ToSDPItemType shared.ItemType - Description string - BlastPropagation *sdp.BlastPropagation - IsParentToChild bool + ToSDPItemType shared.ItemType + Description string + IsParentToChild bool } -var ( - ImpactInOnly = &sdp.BlastPropagation{In: true} - impactBothWays = &sdp.BlastPropagation{In: true, Out: true} -) - var ( IPImpactBothWays = &Impact{ - Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in blast propagation - both will automatically detect the actual type.", - ToSDPItemType: stdlib.NetworkIP, - BlastPropagation: impactBothWays, + Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in blast propagation - both will automatically detect the actual type.", + ToSDPItemType: stdlib.NetworkIP, } SecurityPolicyImpactInOnly = &Impact{ - Description: "Any change on the security policy impacts the source, but not the other way around.", - ToSDPItemType: ComputeSecurityPolicy, - BlastPropagation: ImpactInOnly, + Description: "Any change on the security policy impacts the source, but not the other way around.", + ToSDPItemType: ComputeSecurityPolicy, } CryptoKeyImpactInOnly = &Impact{ - Description: "If the crypto key is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key remains unaffected.", - ToSDPItemType: CloudKMSCryptoKey, - BlastPropagation: ImpactInOnly, + Description: "If the crypto key is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key remains unaffected.", + ToSDPItemType: CloudKMSCryptoKey, } CryptoKeyVersionImpactInOnly = &Impact{ - Description: "If the crypto key version is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key version remains unaffected.", - ToSDPItemType: CloudKMSCryptoKeyVersion, - BlastPropagation: ImpactInOnly, + Description: "If the crypto key version is updated: The source may not be able to access encrypted data. If the source is updated: The crypto key version remains unaffected.", + ToSDPItemType: CloudKMSCryptoKeyVersion, } IAMServiceAccountImpactInOnly = &Impact{ - Description: "If the service account is updated: The source may not be able to access encrypted data. If the source is updated: The service account remains unaffected.", - ToSDPItemType: IAMServiceAccount, - BlastPropagation: ImpactInOnly, + Description: "If the service account is updated: The source may not be able to access encrypted data. If the source is updated: The service account remains unaffected.", + ToSDPItemType: IAMServiceAccount, } ResourcePolicyImpactInOnly = &Impact{ - Description: "If the resource policy is updated: The source may not be able to access the resource as expected. If the source is updated: The resource policy remains unaffected.", - ToSDPItemType: ComputeResourcePolicy, - BlastPropagation: ImpactInOnly, + Description: "If the resource policy is updated: The source may not be able to access the resource as expected. If the source is updated: The resource policy remains unaffected.", + ToSDPItemType: ComputeResourcePolicy, } ComputeNetworkImpactInOnly = &Impact{ - Description: "If the Compute Network is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The network remains unaffected.", - ToSDPItemType: ComputeNetwork, - BlastPropagation: ImpactInOnly, + Description: "If the Compute Network is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The network remains unaffected.", + ToSDPItemType: ComputeNetwork, } ComputeSubnetworkImpactInOnly = &Impact{ - Description: "If the Compute Subnetwork is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The subnetwork remains unaffected.", - ToSDPItemType: ComputeSubnetwork, - BlastPropagation: ImpactInOnly, + Description: "If the Compute Subnetwork is updated: The source may lose connectivity or fail to run as expected. If the source is updated: The subnetwork remains unaffected.", + ToSDPItemType: ComputeSubnetwork, } ) diff --git a/sources/gcp/shared/cross_project_linking_test.go b/sources/gcp/shared/cross_project_linking_test.go index 6a8d3c91..0b05964e 100644 --- a/sources/gcp/shared/cross_project_linking_test.go +++ b/sources/gcp/shared/cross_project_linking_test.go @@ -10,11 +10,6 @@ import ( // TestProjectBaseLinkedItemQueryByName_CrossProject verifies that project-level // resources correctly extract the project ID from cross-project URIs func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -34,7 +29,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-image", Scope: "my-project", }, - BlastPropagation: blastPropagation, }, }, { @@ -49,7 +43,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-image", Scope: "my-project", }, - BlastPropagation: blastPropagation, }, }, { @@ -64,7 +57,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "pcs-clamav-box", Scope: "box-dev-baseos", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -79,7 +71,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-image", Scope: "other-project", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -101,7 +92,7 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := ProjectBaseLinkedItemQueryByName(ComputeImage) - got := linkerFunc(tt.projectID, "", tt.query, blastPropagation) + got := linkerFunc(tt.projectID, "", tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ProjectBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } @@ -112,11 +103,6 @@ func TestProjectBaseLinkedItemQueryByName_CrossProject(t *testing.T) { // TestRegionBaseLinkedItemQueryByName_CrossProject verifies that regional // resources correctly extract the project ID from cross-project URIs func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -138,7 +124,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-address", Scope: "my-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -154,7 +139,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-address", Scope: "other-project.europe-west1", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -170,7 +154,7 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := RegionBaseLinkedItemQueryByName(ComputeAddress) - got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query, blastPropagation) + got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("RegionBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } @@ -181,11 +165,6 @@ func TestRegionBaseLinkedItemQueryByName_CrossProject(t *testing.T) { // TestZoneBaseLinkedItemQueryByName_CrossProject verifies that zonal // resources correctly extract the project ID from cross-project URIs func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string projectID string @@ -207,7 +186,6 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "my-disk", Scope: "my-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, { @@ -223,7 +201,6 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { Query: "other-disk", Scope: "other-project.europe-west1-b", // Should use extracted project, not context project }, - BlastPropagation: blastPropagation, }, }, { @@ -239,7 +216,7 @@ func TestZoneBaseLinkedItemQueryByName_CrossProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { linkerFunc := ZoneBaseLinkedItemQueryByName(ComputeDisk) - got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query, blastPropagation) + got := linkerFunc(tt.projectID, tt.fromItemScope, tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ZoneBaseLinkedItemQueryByName() = %v, want %v\nDescription: %s", got, tt.want, tt.description) } diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go index ca9a9d30..5e646232 100644 --- a/sources/gcp/shared/kms-asset-loader.go +++ b/sources/gcp/shared/kms-asset-loader.go @@ -463,10 +463,6 @@ func (l *CloudKMSAssetLoader) keyRingLinkedQueries(keyRingVals []string, scope s Query: shared.CompositeLookupKey(keyRingVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to CryptoKeys in this KeyRing @@ -477,10 +473,6 @@ func (l *CloudKMSAssetLoader) keyRingLinkedQueries(keyRingVals []string, scope s Query: shared.CompositeLookupKey(keyRingVals[0], keyRingVals[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) return queries @@ -501,10 +493,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to parent KeyRing @@ -515,10 +503,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to all CryptoKeyVersions @@ -529,10 +513,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(kmsLocation, keyRing, cryptoKeyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Link to primary CryptoKeyVersion if present @@ -547,10 +527,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(keyVersionVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -566,10 +542,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(importJobVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -586,10 +558,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyLinkedQueries(values []string, cryptoKey Query: shared.CompositeLookupKey(backendVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -611,10 +579,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(values[0], values[1], values[2]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to ImportJob if present @@ -628,10 +592,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(importJobVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -649,10 +609,6 @@ func (l *CloudKMSAssetLoader) cryptoKeyVersionLinkedQueries(values []string, key Query: shared.CompositeLookupKey(ekmVals...), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 6e09cf0b..1a1f5848 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -33,7 +33,7 @@ type ItemLookup map[string]ItemTypeMeta type Linker struct { sdpAssetTypeToAdapterMeta map[shared.ItemType]AdapterMeta explicitBlastPropagations map[shared.ItemType]map[string]*Impact - manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string, bp *sdp.BlastPropagation) *sdp.LinkedItemQuery + manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string) *sdp.LinkedItemQuery } // NewLinker creates a new Linker instance with the provided item lookup and predefined mappings. @@ -92,11 +92,11 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd // You can specify either IP or DNS in the blast propagation, and it will automatically // detect which type the value actually is and create the appropriate link if impact.ToSDPItemType == stdlib.NetworkIP || impact.ToSDPItemType == stdlib.NetworkDNS { - l.linkIPOrDNS(ctx, fromSDPItem, toItemGCPResourceName, impact.BlastPropagation) + l.linkIPOrDNS(ctx, fromSDPItem, toItemGCPResourceName) return } - linkedItemQuery := linkFunc(projectID, fromSDPItem.GetScope(), toItemGCPResourceName, impact.BlastPropagation) + linkedItemQuery := linkFunc(projectID, fromSDPItem.GetScope(), toItemGCPResourceName) if linkedItemQuery == nil { log.WithContext(ctx).WithFields(lf).Warn( "manual adapter linker failed to create a linked item query", @@ -209,14 +209,13 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd Query: query, Scope: scope, }, - BlastPropagation: impact.BlastPropagation, }) } // linkIPOrDNS detects whether the value is an IP address or DNS name and creates // the appropriate linked item query. This is used for fields like "host" that // could contain either type of value. -func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemValue string, blastPropagation *sdp.BlastPropagation) { +func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemValue string) { if toItemValue == "" { return } @@ -230,7 +229,6 @@ func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemV Query: toItemValue, Scope: "global", }, - BlastPropagation: blastPropagation, }) return } @@ -244,14 +242,13 @@ func (l *Linker) linkIPOrDNS(ctx context.Context, fromSDPItem *sdp.Item, toItemV Query: toItemValue, Scope: "global", }, - BlastPropagation: blastPropagation, }) return } // If neither IP nor DNS, try the manual adapter linker as fallback if linkFunc, ok := l.manualAdapterLinker[stdlib.NetworkIP]; ok { - linkedItemQuery := linkFunc("", fromSDPItem.GetScope(), toItemValue, blastPropagation) + linkedItemQuery := linkFunc("", fromSDPItem.GetScope(), toItemValue) if linkedItemQuery != nil { fromSDPItem.LinkedItemQueries = append( fromSDPItem.LinkedItemQueries, diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index 9c0ef402..27c7e8dd 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -13,8 +13,8 @@ import ( "github.com/overmindtech/cli/sources/stdlib" ) -func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { + return func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) zone := ExtractPathParam("zones", query) // Extract project ID from URI if present (for cross-project references) @@ -34,7 +34,6 @@ func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, from Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -42,8 +41,8 @@ func ZoneBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, from } } -func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { + return func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) scope := fromItemScope region := ExtractPathParam("regions", query) @@ -63,7 +62,6 @@ func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fr Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -71,8 +69,8 @@ func RegionBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, fr } } -func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _, query string) *sdp.LinkedItemQuery { + return func(projectID, _, query string) *sdp.LinkedItemQuery { name := LastPathComponent(query) // Extract project ID from URI if present (for cross-project references) extractedProjectID := ExtractPathParam("projects", query) @@ -88,7 +86,6 @@ func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _ Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -99,7 +96,7 @@ func ProjectBaseLinkedItemQueryByName(sdpItem shared.ItemType) func(projectID, _ // ComputeImageLinker handles linking to compute images using SEARCH method. // SEARCH supports any format: full URIs, family names, or specific image names. // The adapter's Search method will intelligently detect the format and use the appropriate API. -func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ComputeImageLinker(projectID, _, query string) *sdp.LinkedItemQuery { // Extract project ID from the URI if present, otherwise use the provided projectID imageProjectID := ExtractPathParam("projects", query) if imageProjectID == "" { @@ -116,7 +113,6 @@ func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastP Query: query, // Pass the full query string so Search can detect the format Scope: imageProjectID, }, - BlastPropagation: blastPropagation, } } @@ -128,7 +124,7 @@ func ComputeImageLinker(projectID, _, query string, blastPropagation *sdp.BlastP // TargetTcpProxy, TargetSslProxy, TargetPool, TargetVpnGateway, TargetInstance, ServiceAttachment). // This function parses the URI to determine the target type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string) *sdp.LinkedItemQuery { if targetURI == "" { return nil } @@ -221,7 +217,6 @@ func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blas Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -232,7 +227,7 @@ func ForwardingRuleTargetLinker(projectID, fromItemScope, targetURI string, blas // The service field can reference either a BackendService (global or regional) or a BackendBucket (global). // This function parses the URI to determine the target type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string) *sdp.LinkedItemQuery { if backendURI == "" { return nil } @@ -287,7 +282,6 @@ func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, b Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -298,7 +292,7 @@ func BackendServiceOrBucketLinker(projectID, fromItemScope, backendURI string, b // Health checks can be either global (project-scoped) or regional (project.region-scoped). // This function parses the URI to determine the scope and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string) *sdp.LinkedItemQuery { if healthCheckURI == "" { return nil } @@ -341,7 +335,6 @@ func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPro Query: name, Scope: scope, }, - BlastPropagation: blastPropagation, } } @@ -353,7 +346,7 @@ func HealthCheckLinker(projectID, fromItemScope, healthCheckURI string, blastPro // This can include: forwarding rules (regional/global), instances, target VPN gateways, routers. // This function parses the URI to determine the resource type and creates the appropriate link. // Supports both full HTTPS URLs and resource name formats. -func AddressUsersLinker(ctx context.Context, projectID, userURI string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func AddressUsersLinker(ctx context.Context, projectID, userURI string) *sdp.LinkedItemQuery { if userURI == "" { return nil } @@ -464,15 +457,14 @@ func AddressUsersLinker(ctx context.Context, projectID, userURI string, blastPro Query: query, Scope: scope, }, - BlastPropagation: blastPropagation, } } return nil } -func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return func(_, _, arn string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func AWSLinkByARN(awsItem string) func(_, _, arn string) *sdp.LinkedItemQuery { + return func(_, _, arn string) *sdp.LinkedItemQuery { // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html#arns-syntax parts := strings.Split(arn, ":") if len(parts) < 5 { @@ -497,7 +489,6 @@ func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.B Query: arn, // By default, we search by the full ARN Scope: scope, }, - BlastPropagation: blastPropagation, } } } @@ -507,7 +498,7 @@ func AWSLinkByARN(awsItem string) func(_, _, arn string, blastPropagation *sdp.B // So we need to manually define how to create the linked item query based on the item type and the query string. // // Expects that the query will have all the necessary information to create the linked item query. -var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery{ +var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery{ ComputeInstance: ZoneBaseLinkedItemQueryByName(ComputeInstance), ComputeInstanceGroup: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroup), ComputeInstanceGroupManager: ZoneBaseLinkedItemQueryByName(ComputeInstanceGroupManager), @@ -540,7 +531,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem ComputeTargetInstance: ForwardingRuleTargetLinker, // Service Attachment (regional) - use polymorphic linker ComputeServiceAttachment: ForwardingRuleTargetLinker, - CloudKMSCryptoKeyVersion: func(projectID, _, keyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudKMSCryptoKeyVersion: func(projectID, _, keyName string) *sdp.LinkedItemQuery { location := ExtractPathParam("locations", keyName) keyRing := ExtractPathParam("keyRings", keyName) cryptoKey := ExtractPathParam("cryptoKeys", keyName) @@ -554,7 +545,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(location, keyRing, cryptoKey, cryptoKeyVersion), Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil @@ -566,7 +556,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // The name field can reference projects, folders, or organizations depending on the resource scope. // This function parses the name to determine the target type and creates the appropriate link. // This is registered for CloudResourceManagerProject but can detect and link to all three types. - CloudResourceManagerProject: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerProject: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -581,8 +571,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: projectIDFromName, Scope: projectIDFromName, // Project scope uses project ID as scope }, - BlastPropagation: blastPropagation, - } + } } } else if strings.HasPrefix(name, "folders/") { folderID := ExtractPathParam("folders", name) @@ -594,8 +583,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: folderID, Scope: projectID, // Folder scope uses project ID (may need adjustment when folder adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } else if strings.HasPrefix(name, "organizations/") { orgID := ExtractPathParam("organizations", name) @@ -607,13 +595,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: orgID, Scope: projectID, // Organization scope uses project ID (may need adjustment when org adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudResourceManagerFolder: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerFolder: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -628,13 +615,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: folderID, Scope: projectID, // Folder scope uses project ID (may need adjustment when folder adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudResourceManagerOrganization: func(projectID, _, name string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudResourceManagerOrganization: func(projectID, _, name string) *sdp.LinkedItemQuery { if name == "" { return nil } @@ -649,13 +635,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: orgID, Scope: projectID, // Organization scope uses project ID (may need adjustment when org adapter is created) }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - stdlib.NetworkIP: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkIP: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { return &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -664,12 +649,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: "global", }, - BlastPropagation: blastPropagation, } } return nil }, - stdlib.NetworkDNS: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkDNS: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { return &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -678,12 +662,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: "global", }, - BlastPropagation: blastPropagation, } } return nil }, - stdlib.NetworkHTTP: func(_, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + stdlib.NetworkHTTP: func(_, _, query string) *sdp.LinkedItemQuery { if query != "" { // Extract the base URL (remove query parameters and fragments) httpURL := query @@ -702,13 +685,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: httpURL, Scope: "global", }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - CloudKMSCryptoKey: func(projectID, _, keyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + CloudKMSCryptoKey: func(projectID, _, keyName string) *sdp.LinkedItemQuery { //"projects/{kms_project_id}/locations/{region}/keyRings/{key_region}/cryptoKeys/{key} values := ExtractPathParams(keyName, "locations", "keyRings", "cryptoKeys") if len(values) != 3 { @@ -726,12 +708,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(location, keyRing, cryptoKey), Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryTable: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryTable: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { if query == "" { return nil } @@ -756,8 +737,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -772,8 +752,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -788,7 +767,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(parts[1], parts[2]), Scope: parts[0], }, - BlastPropagation: blastPropagation, } } @@ -798,7 +776,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem aws.KinesisStreamConsumer: AWSLinkByARN("kinesis-stream-consumer"), aws.IAMRole: AWSLinkByARN("iam-role"), aws.MSKCluster: AWSLinkByARN("msk-cluster"), - SQLAdminInstance: func(projectID, _, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + SQLAdminInstance: func(projectID, _, query string) *sdp.LinkedItemQuery { // Supported formats: // 1) {project}:{location}:{instance} (Cloud Run format) // See: https://cloud.google.com/run/docs/reference/rest/v2/Volume#cloudsqlinstance @@ -815,7 +793,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: parts[2], Scope: parts[0], }, - BlastPropagation: blastPropagation, } } @@ -830,8 +807,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -844,13 +820,12 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryDataset: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryDataset: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { // Supported formats: // 1) datasetId (e.g., "my_dataset") // 2) projects/{project}/datasets/{dataset} @@ -908,8 +883,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: dataset, Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } @@ -924,8 +898,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: parts[1], // dataset ID Scope: parts[0], // project ID }, - BlastPropagation: blastPropagation, - } + } } } @@ -943,12 +916,11 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: query, // dataset ID Scope: projectID, }, - BlastPropagation: blastPropagation, } } return nil }, - BigQueryModel: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + BigQueryModel: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { // Supported format: // projects/{project}/datasets/{dataset}/models/{model} if query == "" { @@ -970,8 +942,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } @@ -988,14 +959,13 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - BlastPropagation: blastPropagation, - } + } } } return nil }, - StorageBucket: func(projectID, fromItemScope, query string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { + StorageBucket: func(projectID, fromItemScope, query string) *sdp.LinkedItemQuery { if query == "" { return nil } @@ -1017,8 +987,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -1033,8 +1002,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - BlastPropagation: blastPropagation, - } + } } } @@ -1061,7 +1029,6 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: bucketName, Scope: projectID, }, - BlastPropagation: blastPropagation, } } @@ -1072,14 +1039,14 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // Format: projects/{project_number}/policies/{constraint} or // folders/{folder_id}/policies/{constraint} or // organizations/{organization_id}/policies/{constraint} - CloudResourceManagerProject: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerProject: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, - CloudResourceManagerFolder: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerFolder: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, - CloudResourceManagerOrganization: func(projectID, _, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { - return orgPolicyParentLinker(projectID, policyName, blastPropagation) + CloudResourceManagerOrganization: func(projectID, _, policyName string) *sdp.LinkedItemQuery { + return orgPolicyParentLinker(projectID, policyName) }, } @@ -1092,7 +1059,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // // It also handles simple project references: projects/{project_id} (without /policies/) // In that case, the scope should be the current project (projectID), not the referenced project. -func orgPolicyParentLinker(projectID, policyName string, blastPropagation *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func orgPolicyParentLinker(projectID, policyName string) *sdp.LinkedItemQuery { if policyName == "" { return nil } @@ -1151,7 +1118,6 @@ func orgPolicyParentLinker(projectID, policyName string, blastPropagation *sdp.B Query: parentID, Scope: scope, }, - BlastPropagation: blastPropagation, } } diff --git a/sources/gcp/shared/manual-adapter-links_test.go b/sources/gcp/shared/manual-adapter-links_test.go index 8a0d00d6..20559f7f 100644 --- a/sources/gcp/shared/manual-adapter-links_test.go +++ b/sources/gcp/shared/manual-adapter-links_test.go @@ -11,8 +11,7 @@ import ( func TestAWSLinkByARN(t *testing.T) { type args struct { - awsItem string - blastPropagation *sdp.BlastPropagation + awsItem string } tests := []struct { @@ -26,10 +25,6 @@ func TestAWSLinkByARN(t *testing.T) { arn: "arn:aws:iam::123456789012:role/MyRole", args: args{ awsItem: "iam-role", - blastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, want: &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -38,10 +33,6 @@ func TestAWSLinkByARN(t *testing.T) { Query: "arn:aws:iam::123456789012:role/MyRole", Scope: "123456789012", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { @@ -49,10 +40,6 @@ func TestAWSLinkByARN(t *testing.T) { arn: "arn:aws:kms:us-west-2:123456789012:key/abcd1234-56ef-78gh-90ij-klmnopqrstuv", args: args{ awsItem: "kms-key", - blastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, want: &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -61,18 +48,13 @@ func TestAWSLinkByARN(t *testing.T) { Query: "arn:aws:kms:us-west-2:123456789012:key/abcd1234-56ef-78gh-90ij-klmnopqrstuv", Scope: "123456789012.us-west-2", // Region scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, { name: "Malformed ARN", arn: "invalid-arn", args: args{ - awsItem: "iam-role", - blastPropagation: &sdp.BlastPropagation{}, + awsItem: "iam-role", }, want: nil, }, @@ -80,7 +62,7 @@ func TestAWSLinkByARN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFunc := AWSLinkByARN(tt.args.awsItem) - gotLIQ := gotFunc("", "", tt.arn, tt.args.blastPropagation) + gotLIQ := gotFunc("", "", tt.arn) if !reflect.DeepEqual(gotLIQ, tt.want) { t.Errorf("AWSLinkByARN() = %v, want %v", gotLIQ, tt.want) } @@ -90,10 +72,6 @@ func TestAWSLinkByARN(t *testing.T) { func TestForwardingRuleTargetLinker(t *testing.T) { projectID := "test-project" - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: true, - } tests := []struct { name string @@ -111,7 +89,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -124,7 +101,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -137,7 +113,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-http-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target HTTPS Proxy tests @@ -151,7 +126,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-https-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -164,7 +138,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-https-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target TCP Proxy tests @@ -178,7 +151,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-tcp-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -191,7 +163,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-tcp-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Global Target SSL Proxy tests @@ -205,7 +176,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-ssl-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -218,7 +188,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-ssl-proxy", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Regional Target Pool tests @@ -232,7 +201,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-pool", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -245,7 +213,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-pool", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, // Regional Target VPN Gateway tests @@ -259,7 +226,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-vpn-gateway", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, { @@ -272,7 +238,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-vpn-gateway", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, // Zonal Target Instance tests @@ -286,7 +251,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-instance", Scope: "test-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, { @@ -299,7 +263,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "my-target-instance", Scope: "test-project.us-central1-a", }, - BlastPropagation: blastPropagation, }, }, // Edge cases @@ -326,7 +289,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { Query: "targetHttpProxies", // LastPathComponent returns this from trailing slash Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -340,7 +302,7 @@ func TestForwardingRuleTargetLinker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := ForwardingRuleTargetLinker(projectID, "", tt.targetURI, blastPropagation) + got := ForwardingRuleTargetLinker(projectID, "", tt.targetURI) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ForwardingRuleTargetLinker() = %v, want %v", got, tt.want) } @@ -349,11 +311,6 @@ func TestForwardingRuleTargetLinker(t *testing.T) { } func TestNetworkDNSLinker(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: true, - } - tests := []struct { name string query string @@ -369,7 +326,6 @@ func TestNetworkDNSLinker(t *testing.T) { Query: "example.com", Scope: "global", }, - BlastPropagation: blastPropagation, }, }, { @@ -382,7 +338,6 @@ func TestNetworkDNSLinker(t *testing.T) { Query: "api.example.com", Scope: "global", }, - BlastPropagation: blastPropagation, }, }, { @@ -399,7 +354,7 @@ func TestNetworkDNSLinker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := linkerFunc("", "", tt.query, blastPropagation) + got := linkerFunc("", "", tt.query) if !reflect.DeepEqual(got, tt.want) { t.Errorf("NetworkDNSLinker() = %v, want %v", got, tt.want) } @@ -408,11 +363,6 @@ func TestNetworkDNSLinker(t *testing.T) { } func TestMSKClusterLinkByARN(t *testing.T) { - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } - tests := []struct { name string arn string @@ -428,7 +378,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { Query: "arn:aws:kafka:us-east-1:123456789012:cluster/my-cluster/abcd1234-abcd-cafe-abab-9876543210ab-4", Scope: "123456789012.us-east-1", }, - BlastPropagation: blastPropagation, }, }, { @@ -441,7 +390,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { Query: "arn:aws:kafka:us-west-2:987654321098:cluster/prod-cluster/efgh5678-efgh-cafe-cdcd-1234567890ab-5", Scope: "987654321098.us-west-2", }, - BlastPropagation: blastPropagation, }, }, { @@ -463,7 +411,7 @@ func TestMSKClusterLinkByARN(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := linkerFunc("", "", tt.arn, blastPropagation) + got := linkerFunc("", "", tt.arn) if !reflect.DeepEqual(got, tt.want) { t.Errorf("MSKClusterLinkByARN() = %v, want %v", got, tt.want) } @@ -473,10 +421,6 @@ func TestMSKClusterLinkByARN(t *testing.T) { func TestHealthCheckLinker(t *testing.T) { projectID := "test-project" - blastPropagation := &sdp.BlastPropagation{ - In: true, - Out: false, - } tests := []struct { name string @@ -494,7 +438,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -507,7 +450,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, { @@ -520,7 +462,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-health-check", Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, // Regional Health Check tests @@ -534,7 +475,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-regional-health-check", Scope: "test-project.us-central1", }, - BlastPropagation: blastPropagation, }, }, { @@ -547,7 +487,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "my-regional-health-check", Scope: "test-project.us-west1", }, - BlastPropagation: blastPropagation, }, }, { @@ -560,7 +499,6 @@ func TestHealthCheckLinker(t *testing.T) { Query: "eu-health-check", Scope: "test-project.europe-west1", }, - BlastPropagation: blastPropagation, }, }, // Edge cases @@ -584,14 +522,13 @@ func TestHealthCheckLinker(t *testing.T) { Query: "healthChecks", // LastPathComponent returns this from trailing slash Scope: projectID, }, - BlastPropagation: blastPropagation, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := HealthCheckLinker(projectID, "", tt.healthCheckURI, blastPropagation) + got := HealthCheckLinker(projectID, "", tt.healthCheckURI) if !reflect.DeepEqual(got, tt.want) { t.Errorf("HealthCheckLinker() = %v, want %v", got, tt.want) } diff --git a/sources/shared/testing.go b/sources/shared/testing.go index dd136a89..8cc2f637 100644 --- a/sources/shared/testing.go +++ b/sources/shared/testing.go @@ -114,7 +114,7 @@ func (i QueryTests) TestLinkedItems(t *testing.T, item *sdp.Item) { } if test.ExpectedBlastPropagation == nil { - t.Fatalf("for the linked item query %s of %s, the test case must have a non-nil blast propagation", test.ExpectedQuery, test.ExpectedType) + continue } if gotLiq.GetBlastPropagation() == nil { From 1cf408ede6fd19397e0e73ccc033be556d6ab602 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:38:20 +0000 Subject: [PATCH 17/51] Update Go (#3867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go | `v1.36.11-20251209175733-2a1774d88802.1` → `v1.36.11-20260209202127-80ab13bee0bf.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/buf.build%2fgen%2fgo%2fbufbuild%2fprotovalidate%2fprotocolbuffers%2fgo/v1.36.11-20260209202127-80ab13bee0bf.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/buf.build%2fgen%2fgo%2fbufbuild%2fprotovalidate%2fprotocolbuffers%2fgo/v1.36.11-20251209175733-2a1774d88802.1/v1.36.11-20260209202127-80ab13bee0bf.1?slim=true) | | [buf.build/go/protovalidate](https://redirect.github.com/bufbuild/protovalidate-go) | `v1.1.0` → `v1.1.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/buf.build%2fgo%2fprotovalidate/v1.1.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/buf.build%2fgo%2fprotovalidate/v1.1.0/v1.1.2?slim=true) | | [cloud.google.com/go/aiplatform](https://redirect.github.com/googleapis/google-cloud-go) | `v1.115.0` → `v1.116.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2faiplatform/v1.116.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2faiplatform/v1.115.0/v1.116.0?slim=true) | | [cloud.google.com/go/spanner](https://redirect.github.com/googleapis/google-cloud-go) | `v1.87.0` → `v1.88.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fspanner/v1.88.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fspanner/v1.87.0/v1.88.0?slim=true) | | [github.com/1password/onepassword-sdk-go](https://redirect.github.com/1password/onepassword-sdk-go) | `v0.3.1` → `v0.4.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2f1password%2fonepassword-sdk-go/v0.4.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2f1password%2fonepassword-sdk-go/v0.3.1/v0.4.0?slim=true) | | [github.com/auth0/go-auth0/v2](https://redirect.github.com/auth0/go-auth0) | `v2.4.0` → `v2.5.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fauth0%2fgo-auth0%2fv2/v2.5.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fauth0%2fgo-auth0%2fv2/v2.4.0/v2.5.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ec2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.285.0` → `v1.288.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.288.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.285.0/v1.288.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/eks](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.77.1` → `v1.80.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.80.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.77.1/v1.80.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/rds](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.114.0` → `v1.115.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.115.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.114.0/v1.115.0?slim=true) | | [github.com/harness/harness-go-sdk](https://redirect.github.com/harness/harness-go-sdk) | `v0.7.6` → `v0.7.9` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fharness%2fharness-go-sdk/v0.7.9?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fharness%2fharness-go-sdk/v0.7.6/v0.7.9?slim=true) | | [github.com/kaptinlin/jsonrepair](https://redirect.github.com/kaptinlin/jsonrepair) | `v0.2.7` → `v0.2.8` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fkaptinlin%2fjsonrepair/v0.2.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fkaptinlin%2fjsonrepair/v0.2.7/v0.2.8?slim=true) | | [github.com/openai/openai-go/v3](https://redirect.github.com/openai/openai-go) | `v3.18.0` → `v3.21.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopenai%2fopenai-go%2fv3/v3.21.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopenai%2fopenai-go%2fv3/v3.18.0/v3.21.0?slim=true) | | [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) | [`v0.49.0` → `v0.50.0`](https://cs.opensource.google/go/x/net/+/refs/tags/v0.49.0...refs/tags/v0.50.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fnet/v0.50.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fnet/v0.49.0/v0.50.0?slim=true) | | [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) | [`v0.34.0` → `v0.35.0`](https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.34.0...refs/tags/v0.35.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2foauth2/v0.35.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2foauth2/v0.34.0/v0.35.0?slim=true) | | [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) | [`v0.33.0` → `v0.34.0`](https://cs.opensource.google/go/x/text/+/refs/tags/v0.33.0...refs/tags/v0.34.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2ftext/v0.34.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2ftext/v0.33.0/v0.34.0?slim=true) | | [google.golang.org/api](https://redirect.github.com/googleapis/google-api-go-client) | `v0.265.0` → `v0.266.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fapi/v0.266.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fapi/v0.265.0/v0.266.0?slim=true) | | [google.golang.org/grpc](https://redirect.github.com/grpc/grpc-go) | `v1.78.0` → `v1.79.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fgrpc/v1.79.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fgrpc/v1.78.0/v1.79.1?slim=true) | | [k8s.io/api](https://redirect.github.com/kubernetes/api) | `v0.35.0` → `v0.35.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fapi/v0.35.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fapi/v0.35.0/v0.35.1?slim=true) | | [k8s.io/apimachinery](https://redirect.github.com/kubernetes/apimachinery) | `v0.35.0` → `v0.35.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fapimachinery/v0.35.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fapimachinery/v0.35.0/v0.35.1?slim=true) | | [k8s.io/client-go](https://redirect.github.com/kubernetes/client-go) | `v0.35.0` → `v0.35.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fclient-go/v0.35.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fclient-go/v0.35.0/v0.35.1?slim=true) | | [k8s.io/component-base](https://redirect.github.com/kubernetes/component-base) | `v0.35.0` → `v0.35.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fcomponent-base/v0.35.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fcomponent-base/v0.35.0/v0.35.1?slim=true) | | [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `v1.44.3` → `v1.45.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/modernc.org%2fsqlite/v1.45.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/modernc.org%2fsqlite/v1.44.3/v1.45.0?slim=true) | | [sigs.k8s.io/structured-merge-diff/v6](https://redirect.github.com/kubernetes-sigs/structured-merge-diff) | `v6.3.2-0.20260122202528-d9cc6641c482` → `v6.3.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/sigs.k8s.io%2fstructured-merge-diff%2fv6/v6.3.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/sigs.k8s.io%2fstructured-merge-diff%2fv6/v6.3.2-0.20260122202528-d9cc6641c482/v6.3.2?slim=true) | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
bufbuild/protovalidate-go (buf.build/go/protovalidate) ### [`v1.1.2`](https://redirect.github.com/bufbuild/protovalidate-go/releases/tag/v1.1.2) [Compare Source](https://redirect.github.com/bufbuild/protovalidate-go/compare/v1.1.1...v1.1.2) #### What's Changed - Fix base type adapter missing builtin types by [@​rodaine](https://redirect.github.com/rodaine) in [#​305](https://redirect.github.com/bufbuild/protovalidate-go/pull/305) **Full Changelog**: ### [`v1.1.1`](https://redirect.github.com/bufbuild/protovalidate-go/releases/tag/v1.1.1) [Compare Source](https://redirect.github.com/bufbuild/protovalidate-go/compare/v1.1.0...v1.1.1) This release is compatible with the [v1.1.0](https://redirect.github.com/bufbuild/protovalidate/releases/tag/v1.1.0) release of Protovalidate. #### What's Changed - Always provide all available variables by [@​srikrsna-buf](https://redirect.github.com/srikrsna-buf) in [#​297](https://redirect.github.com/bufbuild/protovalidate-go/pull/297) - Wrap protoreflect.Map with type information so we don't need to cast to map\[any]any by [@​rodaine](https://redirect.github.com/rodaine) in [#​300](https://redirect.github.com/bufbuild/protovalidate-go/pull/300) - Avoid heap escape on kvPairs evaluation by [@​rodaine](https://redirect.github.com/rodaine) in [#​301](https://redirect.github.com/bufbuild/protovalidate-go/pull/301) - Implement registry chaining for CEL type isolation by [@​rodaine](https://redirect.github.com/rodaine) in [#​302](https://redirect.github.com/bufbuild/protovalidate-go/pull/302) **Full Changelog**:
1password/onepassword-sdk-go (github.com/1password/onepassword-sdk-go) ### [`v0.4.0`](https://redirect.github.com/1Password/onepassword-sdk-go/releases/tag/v0.4.0): Release 0.4.0 [Compare Source](https://redirect.github.com/1password/onepassword-sdk-go/compare/v0.3.1...v0.4.0) ### 1Password Go SDK v0.4.0 #### NEW - **Desktop App integration:** The SDK can now authenticate via an authorization prompt from the 1Password app. - **Vault CRUDL:** You can now fully manage 1Password vaults with the SDK, including creating, reading, updating, deleting and listing. - **Vault group permission management operations:** You can now grant, update and revoke group access to vaults using `grantGroupPermissions`, `updateGroupPermissions`, and `revokeGroupPermissions` functions. - **Item batch management:** You can now retrieve, create, update and delete items in batch, enabling more scalable item management.
auth0/go-auth0 (github.com/auth0/go-auth0/v2) ### [`v2.5.0`](https://redirect.github.com/auth0/go-auth0/blob/HEAD/CHANGELOG.md#v250-2026-02-11) [Compare Source](https://redirect.github.com/auth0/go-auth0/compare/v2.4.0...v2.5.0) [Full Changelog](https://redirect.github.com/auth0/go-auth0/compare/v2.4.0...v2.5.0) **Changed** - feat!: Consolidate types to root package with consistent naming [#​692](https://redirect.github.com/auth0/go-auth0/pull/692) ([fern-api\[bot\]](https://redirect.github.com/apps/fern-api)) **Fixed** - chore: Add MarshalJSON/UnmarshalJSON to all request content types for correct explicit-field serialization [#​696](https://redirect.github.com/auth0/go-auth0/pull/696) ([fern-api\[bot\]](https://redirect.github.com/apps/fern-api)) - chore: Add pagination for Action Module Versions, enhance social connection options, and fix session signal serialization [#​695](https://redirect.github.com/auth0/go-auth0/pull/695) ([fern-api\[bot\]](https://redirect.github.com/apps/fern-api)) - chore: Improve WireMock test infrastructure and add package-level error codes [#​693](https://redirect.github.com/auth0/go-auth0/pull/693) ([fern-api\[bot\]](https://redirect.github.com/apps/fern-api))
aws/aws-sdk-go-v2 (github.com/aws/aws-sdk-go-v2/service/ec2) ### [`v1.288.0`](https://redirect.github.com/aws/aws-sdk-go-v2/blob/HEAD/CHANGELOG.md#Release-2026-02-12) #### Module Highlights - `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.288.0](service/ec2/CHANGELOG.md#v12880-2026-02-12) - **Feature**: Launching nested virtualization. This feature allows you to run nested VMs inside virtual (non-bare metal) EC2 instances. ### [`v1.287.0`](https://redirect.github.com/aws/aws-sdk-go-v2/blob/HEAD/CHANGELOG.md#Release-2026-02-11) #### Module Highlights - `github.com/aws/aws-sdk-go-v2/service/batch`: [v1.60.0](service/batch/CHANGELOG.md#v1600-2026-02-11) - **Feature**: Add support for listing jobs by share identifier and getting snapshots of active capacity utilization by job queue and share. - `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.287.0](service/ec2/CHANGELOG.md#v12870-2026-02-11) - **Feature**: R8i instances powered by custom Intel Xeon 6 processors available only on AWS with sustained all-core 3.9 GHz turbo frequency - `github.com/aws/aws-sdk-go-v2/service/eks`: [v1.80.0](service/eks/CHANGELOG.md#v1800-2026-02-11) - **Feature**: This release adds support for Windows Server 2025 in Amazon EKS Managed Node Groups. - `github.com/aws/aws-sdk-go-v2/service/kafkaconnect`: [v1.30.0](service/kafkaconnect/CHANGELOG.md#v1300-2026-02-11) - **Feature**: Support configurable upper limits on task count during autoscaling operations via maxAutoscalingTaskCount parameter. - `github.com/aws/aws-sdk-go-v2/service/s3tables`: [v1.14.0](service/s3tables/CHANGELOG.md#v1140-2026-02-11) - **Feature**: S3 Tables now supports setting partition specifications and sort orders on tables. Partition specs allow users to define how data is organized using transform functions. Sort order configurations enable users to specify sort directions and null ordering preferences for optimized data layout. ### [`v1.286.0`](https://redirect.github.com/aws/aws-sdk-go-v2/blob/HEAD/CHANGELOG.md#Release-2026-02-10) #### Module Highlights - `github.com/aws/aws-sdk-go-v2/service/connectcampaignsv2`: [v1.11.0](service/connectcampaignsv2/CHANGELOG.md#v1110-2026-02-10) - **Feature**: Add the missing event type for WhatsApp - `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.286.0](service/ec2/CHANGELOG.md#v12860-2026-02-10) - **Feature**: Amazon Secondary Networks is a networking feature that provides high-performance, low-latency connectivity for specialized workloads. - `github.com/aws/aws-sdk-go-v2/service/eks`: [v1.78.0](service/eks/CHANGELOG.md#v1780-2026-02-10) - **Feature**: Amazon EKS adds a new DescribeUpdate update type, VendedLogsUpdate, to support an integration between EKS Auto Mode and Amazon CloudWatch Vended Logs. - `github.com/aws/aws-sdk-go-v2/service/evidently`: [v1.30.0](service/evidently/CHANGELOG.md#v1300-2026-02-10) - **Feature**: Marked service APIs as deprecated. This service has reached end-of-life. - `github.com/aws/aws-sdk-go-v2/service/imagebuilder`: [v1.51.0](service/imagebuilder/CHANGELOG.md#v1510-2026-02-10) - **Feature**: EC2 Image Builder now supports wildcard patterns in lifecycle policies with recipes and enhances the experience of tag-scoped policies. - `github.com/aws/aws-sdk-go-v2/service/iotanalytics`: [v1.32.0](service/iotanalytics/CHANGELOG.md#v1320-2026-02-10) - **Feature**: Marked service APIs as deprecated. This service has reached end-of-life. - `github.com/aws/aws-sdk-go-v2/service/lakeformation`: [v1.47.1](service/lakeformation/CHANGELOG.md#v1471-2026-02-10) - **Documentation**: Allow cross account v5 in put data lake settings - `github.com/aws/aws-sdk-go-v2/service/neptunedata`: [v1.17.0](service/neptunedata/CHANGELOG.md#v1170-2026-02-10) - **Feature**: Added edgeOnlyLoad boolean parameter to Neptune bulk load request. When TRUE, files are loaded in order without scanning. When FALSE (default), the loader scans files first, then loads vertex files before edge files automatically. - `github.com/aws/aws-sdk-go-v2/service/pcs`: [v1.16.0](service/pcs/CHANGELOG.md#v1160-2026-02-10) - **Feature**: Introduces RESUMING state for clusters, compute node groups, and queues. - `github.com/aws/aws-sdk-go-v2/service/transfer`: [v1.69.1](service/transfer/CHANGELOG.md#v1691-2026-02-10) - **Documentation**: This release adds a documentation update for MdnResponse of type "ASYNC"
harness/harness-go-sdk (github.com/harness/harness-go-sdk) ### [`v0.7.9`](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.8...v0.7.9) [Compare Source](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.8...v0.7.9) ### [`v0.7.8`](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.7...v0.7.8) [Compare Source](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.7...v0.7.8) ### [`v0.7.7`](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.6...v0.7.7) [Compare Source](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.6...v0.7.7)
kaptinlin/jsonrepair (github.com/kaptinlin/jsonrepair) ### [`v0.2.8`](https://redirect.github.com/kaptinlin/jsonrepair/compare/v0.2.7...v0.2.8) [Compare Source](https://redirect.github.com/kaptinlin/jsonrepair/compare/v0.2.7...v0.2.8)
openai/openai-go (github.com/openai/openai-go/v3) ### [`v3.21.0`](https://redirect.github.com/openai/openai-go/releases/tag/v3.21.0) [Compare Source](https://redirect.github.com/openai/openai-go/compare/v3.20.0...v3.21.0) #### 3.21.0 (2026-02-10) Full Changelog: [v3.20.0...v3.21.0](https://redirect.github.com/openai/openai-\[go/compare/v3.20.0...v3.21.0]\(https://www.golinks.io/compare/v3.20.0...v3.21.0?trackSource=github\)) ##### Features - **api:** support for images in batch api ([e23aeb1](https://redirect.github.com/openai/openai-\[go/commit/e23aeb1b13bfd089cc73d3097c9635b687446f82]\(https://www.golinks.io/commit/e23aeb1b13bfd089cc73d3097c9635b687446f82?trackSource=github\))) ### [`v3.20.0`](https://redirect.github.com/openai/openai-go/releases/tag/v3.20.0) [Compare Source](https://redirect.github.com/openai/openai-go/compare/v3.19.0...v3.20.0) #### 3.20.0 (2026-02-10) Full Changelog: [v3.19.0...v3.20.0](https://redirect.github.com/openai/openai-\[go/compare/v3.19.0...v3.20.0]\(https://www.golinks.io/compare/v3.19.0...v3.20.0?trackSource=github\)) ##### Features - **api:** skills and hosted shell ([9e191de](https://redirect.github.com/openai/openai-\[go/commit/9e191de75f67a6a693c8b25ac9ab1b9288673993]\(https://www.golinks.io/commit/9e191de75f67a6a693c8b25ac9ab1b9288673993?trackSource=github\))) ### [`v3.19.0`](https://redirect.github.com/openai/openai-go/releases/tag/v3.19.0) [Compare Source](https://redirect.github.com/openai/openai-go/compare/v3.18.0...v3.19.0) #### 3.19.0 (2026-02-09) Full Changelog: [v3.18.0...v3.19.0](https://redirect.github.com/openai/openai-go/compare/v3.18.0...v3.19.0) ##### Features - **api:** responses context\_management ([199f230](https://redirect.github.com/openai/openai-go/commit/199f23025ab098f2ac0ac9a99dee37235613c287))
googleapis/google-api-go-client (google.golang.org/api) ### [`v0.266.0`](https://redirect.github.com/googleapis/google-api-go-client/releases/tag/v0.266.0) [Compare Source](https://redirect.github.com/googleapis/google-api-go-client/compare/v0.265.0...v0.266.0) ##### Features - **all:** Auto-regenerate discovery clients ([#​3483](https://redirect.github.com/googleapis/google-api-go-client/issues/3483)) ([a3a61ce](https://redirect.github.com/googleapis/google-api-go-client/commit/a3a61ce2214c8d18bb640c724fae2cda8cb77b58)) - **all:** Auto-regenerate discovery clients ([#​3485](https://redirect.github.com/googleapis/google-api-go-client/issues/3485)) ([200d140](https://redirect.github.com/googleapis/google-api-go-client/commit/200d1409ecc830131f0b5b92fd59708fef24dd8e)) - **all:** Auto-regenerate discovery clients ([#​3486](https://redirect.github.com/googleapis/google-api-go-client/issues/3486)) ([870909e](https://redirect.github.com/googleapis/google-api-go-client/commit/870909e466b1bf8172dfe9bd5c096b1df45b0491)) - **all:** Auto-regenerate discovery clients ([#​3487](https://redirect.github.com/googleapis/google-api-go-client/issues/3487)) ([6018e80](https://redirect.github.com/googleapis/google-api-go-client/commit/6018e80ff5cadadb81c7b7be9f5de01b4b4c2132)) - **all:** Auto-regenerate discovery clients ([#​3489](https://redirect.github.com/googleapis/google-api-go-client/issues/3489)) ([402353b](https://redirect.github.com/googleapis/google-api-go-client/commit/402353be95579bccda6b6623e67e9f028163905b)) - **all:** Auto-regenerate discovery clients ([#​3490](https://redirect.github.com/googleapis/google-api-go-client/issues/3490)) ([49c652f](https://redirect.github.com/googleapis/google-api-go-client/commit/49c652fb9c5e08c9d1a2587f41017b6011dc03da))
grpc/grpc-go (google.golang.org/grpc) ### [`v1.79.1`](https://redirect.github.com/grpc/grpc-go/releases/tag/v1.79.1): Release 1.79.1 [Compare Source](https://redirect.github.com/grpc/grpc-go/compare/v1.79.0...v1.79.1) ### Bug Fixes - grpc: Remove the -dev suffix from the User-Agent header ([#​8902](https://redirect.github.com/grpc/grpc-go/pull/8902)) ### [`v1.79.0`](https://redirect.github.com/grpc/grpc-go/releases/tag/v1.79.0): Release 1.79.0 [Compare Source](https://redirect.github.com/grpc/grpc-go/compare/v1.78.0...v1.79.0) ### API Changes - mem: Add experimental API `SetDefaultBufferPool` to change the default buffer pool. ([#​8806](https://redirect.github.com/grpc/grpc-go/issues/8806)) - Special Thanks: [@​vanja-p](https://redirect.github.com/vanja-p) - experimental/stats: Update `MetricsRecorder` to require embedding the new `UnimplementedMetricsRecorder` (a no-op struct) in all implementations for forward compatibility. ([#​8780](https://redirect.github.com/grpc/grpc-go/issues/8780)) ### Behavior Changes - balancer/weightedtarget: Remove handling of `Addresses` and only handle `Endpoints` in resolver updates. ([#​8841](https://redirect.github.com/grpc/grpc-go/issues/8841)) ### New Features - experimental/stats: Add support for asynchronous gauge metrics through the new `AsyncMetricReporter` and `RegisterAsyncReporter` APIs. ([#​8780](https://redirect.github.com/grpc/grpc-go/issues/8780)) - pickfirst: Add support for weighted random shuffling of endpoints, as described in [gRFC A113](https://redirect.github.com/grpc/proposal/pull/535). - This is enabled by default, and can be turned off using the environment variable `GRPC_EXPERIMENTAL_PF_WEIGHTED_SHUFFLING`. ([#​8864](https://redirect.github.com/grpc/grpc-go/issues/8864)) - xds: Implement `:authority` rewriting, as specified in [gRFC A81](https://redirect.github.com/grpc/proposal/blob/master/A81-xds-authority-rewriting.md). ([#​8779](https://redirect.github.com/grpc/grpc-go/issues/8779)) - balancer/randomsubsetting: Implement the `random_subsetting` LB policy, as specified in [gRFC A68](https://redirect.github.com/grpc/proposal/blob/master/A68-random-subsetting.md). ([#​8650](https://redirect.github.com/grpc/grpc-go/issues/8650)) - Special Thanks: [@​marek-szews](https://redirect.github.com/marek-szews) - server: Include status detail headers, if available, when terminating a stream during request header processing. ([#​8754](https://redirect.github.com/grpc/grpc-go/issues/8754)) - Special Thanks: [@​joybestourous](https://redirect.github.com/joybestourous) ### Bug Fixes - credentials/tls: Fix a bug where the port was not stripped from the authority override before validation. ([#​8726](https://redirect.github.com/grpc/grpc-go/issues/8726)) - Special Thanks: [@​Atul1710](https://redirect.github.com/Atul1710) - xds/priority: Fix a bug causing delayed failover to lower-priority clusters when a higher-priority cluster is stuck in `CONNECTING` state. ([#​8813](https://redirect.github.com/grpc/grpc-go/issues/8813)) - health: Fix a bug where health checks failed for clients using legacy compression options (`WithDecompressor` or `RPCDecompressor`). ([#​8765](https://redirect.github.com/grpc/grpc-go/issues/8765)) - Special Thanks: [@​sanki92](https://redirect.github.com/sanki92) - transport: Fix an issue where the HTTP/2 server could skip header size checks when terminating a stream early. ([#​8769](https://redirect.github.com/grpc/grpc-go/issues/8769)) - Special Thanks: [@​joybestourous](https://redirect.github.com/joybestourous) ### Performance Improvements - credentials/alts: Optimize read buffer alignment to reduce copies. ([#​8791](https://redirect.github.com/grpc/grpc-go/issues/8791)) - mem: Optimize pooling and creation of `buffer` objects. ([#​8784](https://redirect.github.com/grpc/grpc-go/issues/8784)) - transport: Reduce slice re-allocations by reserving slice capacity. ([#​8797](https://redirect.github.com/grpc/grpc-go/issues/8797))
kubernetes/api (k8s.io/api) ### [`v0.35.1`](https://redirect.github.com/kubernetes/api/compare/v0.35.0...v0.35.1) [Compare Source](https://redirect.github.com/kubernetes/api/compare/v0.35.0...v0.35.1)
kubernetes/apimachinery (k8s.io/apimachinery) ### [`v0.35.1`](https://redirect.github.com/kubernetes/apimachinery/compare/v0.35.0...v0.35.1) [Compare Source](https://redirect.github.com/kubernetes/apimachinery/compare/v0.35.0...v0.35.1)
kubernetes/client-go (k8s.io/client-go) ### [`v0.35.1`](https://redirect.github.com/kubernetes/client-go/compare/v0.35.0...v0.35.1) [Compare Source](https://redirect.github.com/kubernetes/client-go/compare/v0.35.0...v0.35.1)
kubernetes/component-base (k8s.io/component-base) ### [`v0.35.1`](https://redirect.github.com/kubernetes/component-base/compare/v0.35.0...v0.35.1) [Compare Source](https://redirect.github.com/kubernetes/component-base/compare/v0.35.0...v0.35.1)
cznic/sqlite (modernc.org/sqlite) ### [`v1.45.0`](https://gitlab.com/cznic/sqlite/compare/v1.44.3...v1.45.0) [Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.44.3...v1.45.0)
kubernetes-sigs/structured-merge-diff (sigs.k8s.io/structured-merge-diff/v6) ### [`v6.3.2`](https://redirect.github.com/kubernetes-sigs/structured-merge-diff/compare/v6.3.1...v6.3.2) [Compare Source](https://redirect.github.com/kubernetes-sigs/structured-merge-diff/compare/v6.3.1...v6.3.2)
--- ### Configuration 📅 **Schedule**: Branch creation - "before 10am on friday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: ad6782fb125948cd3d4a6ee5e6e1d71d78cfb590 --- go.mod | 64 ++++---- go.sum | 146 +++++++++--------- .../service-account-impersonation_test.go | 8 +- 3 files changed, 109 insertions(+), 109 deletions(-) diff --git a/go.mod b/go.mod index 26e0750a..7fd2924b 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ replace github.com/google/cel-go => github.com/google/cel-go v0.22.1 require ( atomicgo.dev/keyboard v0.2.9 - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 - buf.build/go/protovalidate v1.1.0 - cloud.google.com/go/aiplatform v1.115.0 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 + buf.build/go/protovalidate v1.1.2 + cloud.google.com/go/aiplatform v1.116.0 cloud.google.com/go/auth v0.18.1 cloud.google.com/go/bigquery v1.73.1 cloud.google.com/go/bigtable v1.42.0 @@ -35,11 +35,11 @@ require ( cloud.google.com/go/run v1.15.0 cloud.google.com/go/secretmanager v1.16.0 cloud.google.com/go/securitycentermanagement v1.1.6 - cloud.google.com/go/spanner v1.87.0 + cloud.google.com/go/spanner v1.88.0 cloud.google.com/go/storagetransfer v1.13.1 connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version connectrpc.com/otelconnect v0.9.0 - github.com/1password/onepassword-sdk-go v0.3.1 + github.com/1password/onepassword-sdk-go v0.4.0 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 @@ -61,7 +61,7 @@ require ( github.com/akedrou/textdiff v0.1.0 github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 github.com/antihax/optional v1.0.0 - github.com/auth0/go-auth0/v2 v2.4.0 + github.com/auth0/go-auth0/v2 v2.5.0 github.com/auth0/go-jwt-middleware/v2 v2.3.1 github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -73,10 +73,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.1 github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0 github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 github.com/aws/aws-sdk-go-v2/service/efs v1.41.10 - github.com/aws/aws-sdk-go-v2/service/eks v1.77.1 + github.com/aws/aws-sdk-go-v2/service/eks v1.80.0 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 github.com/aws/aws-sdk-go-v2/service/iam v1.53.2 @@ -84,7 +84,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4 - github.com/aws/aws-sdk-go-v2/service/rds v1.114.0 + github.com/aws/aws-sdk-go-v2/service/rds v1.115.0 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1 @@ -111,7 +111,7 @@ require ( github.com/googleapis/gax-go/v2 v2.17.0 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e github.com/gorilla/mux v1.8.1 - github.com/harness/harness-go-sdk v0.7.6 + github.com/harness/harness-go-sdk v0.7.9 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 @@ -119,7 +119,7 @@ require ( github.com/jackc/pgx/v5 v5.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 github.com/jxskiss/base62 v1.1.0 - github.com/kaptinlin/jsonrepair v0.2.7 + github.com/kaptinlin/jsonrepair v0.2.8 github.com/manifoldco/promptui v0.9.0 github.com/mavolin/go-htmx v1.0.0 github.com/mergestat/timediff v0.0.4 @@ -135,7 +135,7 @@ require ( github.com/neo4j/neo4j-go-driver/v6 v6.0.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 - github.com/openai/openai-go/v3 v3.18.0 + github.com/openai/openai-go/v3 v3.21.0 github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 github.com/pborman/ansi v1.0.0 @@ -176,24 +176,24 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 - golang.org/x/net v0.49.0 - golang.org/x/oauth2 v0.34.0 + golang.org/x/net v0.50.0 + golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.19.0 - golang.org/x/text v0.33.0 + golang.org/x/text v0.34.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/api v0.265.0 + google.golang.org/api v0.266.0 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.35.0 - k8s.io/apimachinery v0.35.0 - k8s.io/client-go v0.35.0 - k8s.io/component-base v0.35.0 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + k8s.io/component-base v0.35.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 - modernc.org/sqlite v1.44.3 + modernc.org/sqlite v1.45.0 riverqueue.com/riverui v0.14.0 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/kind v0.31.0 @@ -204,7 +204,7 @@ require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/longrunning v0.8.0 // indirect @@ -269,10 +269,10 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/extism/go-sdk v1.7.0 // indirect + github.com/extism/go-sdk v1.7.1 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -317,7 +317,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect + github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -406,7 +406,7 @@ require ( github.com/syndtr/goleveldb v1.0.0 // indirect github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect - github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/tetratelabs/wazero v1.11.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/btree v1.6.0 // indirect github.com/tidwall/buntdb v1.3.0 // indirect @@ -447,17 +447,17 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.32.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect - golang.org/x/term v0.39.0 // indirect + golang.org/x/term v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect @@ -472,6 +472,6 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index e066529d..5deb8bd5 100644 --- a/go.sum +++ b/go.sum @@ -10,12 +10,12 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY= -buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0= +buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -27,8 +27,8 @@ cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6T cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/aiplatform v1.115.0 h1:m/dIJ/HixZDvHoXBGkA5Sd0RbiQp5lBVyddvR9uxHqI= -cloud.google.com/go/aiplatform v1.115.0/go.mod h1:DwPJAxebOTy6BajSMjF7ah3QvlYO4jf2gpJw6/1z9gU= +cloud.google.com/go/aiplatform v1.116.0 h1:Qc8tv4DD6IbQfDKDd1Hu2qeGeYxTKTeZ7GH0vQrLAm8= +cloud.google.com/go/aiplatform v1.116.0/go.mod h1:AdvoUUSXh9ykwEazibd3Fj6OUGrIiZwvZrvm4j5OdkU= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -85,8 +85,8 @@ cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYz cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= cloud.google.com/go/securitycentermanagement v1.1.6 h1:XFqjKq4ZpKTj8xCXWs/mTmh/UMWDiV25iCOUd9xaGWI= cloud.google.com/go/securitycentermanagement v1.1.6/go.mod h1:nt5Z6rU4s2/j8R/EQxG5K7OfVAfAfwo89j0Nx2Srzaw= -cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= +cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= +cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= @@ -98,8 +98,8 @@ connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2pr connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc= -github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk= +github.com/1password/onepassword-sdk-go v0.4.0 h1:Nou39yuC6Q0om03irkh5UurfPdX3wx26qZZhQeC9TBU= +github.com/1password/onepassword-sdk-go v0.4.0/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -213,8 +213,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmms github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/auth0/go-auth0/v2 v2.4.0 h1:5fy8XAh/nlfaOppYWanJQk3M0hhE5433cQCt/Gfsktw= -github.com/auth0/go-auth0/v2 v2.4.0/go.mod h1:4GvvYxDvjiOoglqYngCUOUZWzE4iXNczLID5NDA2tYg= +github.com/auth0/go-auth0/v2 v2.5.0 h1:IBfiYGsqFwOu4hsxV1JDtB6+ayRinybUIUCU/fRBE8Y= +github.com/auth0/go-auth0/v2 v2.5.0/go.mod h1:XVRck9fw1EIw1z4guYcbKFGmElnexb+xOvQ/0U1hHd0= github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw= github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= @@ -247,14 +247,14 @@ github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11 h1:3+DkKJAq5VVqPNu3e github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11/go.mod h1:DNG3VkdVy874VMHH46ekGsD3nq6D4tyDV3HIOuVoouM= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 h1:CyYoeHWjVSGimzMhlL0Z4l5gLCa++ccnRJKrsaNssxE= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0/go.mod h1:ctEsEHY2vFQc6i4KU07q4n68v7BAmTbujv2Y+z8+hQY= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0 h1:cRZQsqCy59DSJmvmUYzi9K+dutysXzfx6F+fkcIHtOk= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.285.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0 h1:cRu1CgKDK0qYNJRZBWaktwGZ6fvcFiKZm1Huzesc47s= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.288.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE= github.com/aws/aws-sdk-go-v2/service/efs v1.41.10 h1:7ixaaFyZ8xXJWPcK3qQKFf1k1HgME9rtCY7S6Unih8I= github.com/aws/aws-sdk-go-v2/service/efs v1.41.10/go.mod h1:QwCUd/L5/HX4s/uWt3LPEOwQb/AYE4OyMGB8SL9/W4Y= -github.com/aws/aws-sdk-go-v2/service/eks v1.77.1 h1:pMXNbXUX4Xd9fRmRdEe/vQ/5EFRy2M4jvW6geO5lhd8= -github.com/aws/aws-sdk-go-v2/service/eks v1.77.1/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.0 h1:moQGV8cPbVTN7r2Xte1Mybku35QDePSJEd3onYVmBtY= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.0/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19 h1:ybEda2mkkX2o8NadXZBtcO9tgmW9cTQgeVSjypNsAy0= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.19/go.mod h1:RiMytGvN4azx4yLM0Kn3bX/XO9dLxj+eG72Smy+vNzI= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 h1:fQR1aeZKaiPkNPya0JMy2nhsoqoSgIWc3/QTiTiL1K0= @@ -279,8 +279,8 @@ github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 h1:Fobn9IdJv8lgpGv5 github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3/go.mod h1:1Yhak+i7rIt8Yq2lWViNXI4zoMufmqqjR89vNwgzafw= github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4 h1:J38JaWrNRBxSU/nrrC92/jqGVl07RAdGXM9GvwtdQqE= github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4/go.mod h1:vdT+5yxPXmxzJ8ETFpajcjce/eUViRAG58SPtZyHoGA= -github.com/aws/aws-sdk-go-v2/service/rds v1.114.0 h1:p9c6HDzx6sTf7uyc9xsQd693uzArsPrsVr9n0oRk7DU= -github.com/aws/aws-sdk-go-v2/service/rds v1.114.0/go.mod h1:JBRYWpz5oXQtHgQC+X8LX9lh0FBCwRHJlWEIT+TTLaE= +github.com/aws/aws-sdk-go-v2/service/rds v1.115.0 h1:oNl6YghOtxu3MiFk1tQ86QlrYMIEJazGUDbBCg9nxLA= +github.com/aws/aws-sdk-go-v2/service/rds v1.115.0/go.mod h1:JBRYWpz5oXQtHgQC+X8LX9lh0FBCwRHJlWEIT+TTLaE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= @@ -380,8 +380,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= @@ -410,25 +410,25 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= -github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= +github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= +github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZc= github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U= -github.com/extism/go-sdk v1.7.0 h1:yHbSa2JbcF60kjGsYiGEOcClfbknqCJchyh9TRibFWo= -github.com/extism/go-sdk v1.7.0/go.mod h1:Dhuc1qcD0aqjdqJ3ZDyGdkZPEj/EHKVjbE4P+1XRMqc= +github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= +github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -591,8 +591,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd h1:FsX+T6wA8spPe4c1K9vi7T0LvNCO1TTqiL8u7Wok2hw= github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= -github.com/harness/harness-go-sdk v0.7.6 h1:xCUafSFqAMk6Dt5P5ay5JzBX7MuW2fycbW6UdgPj6KY= -github.com/harness/harness-go-sdk v0.7.6/go.mod h1:T3nqoDb9WUuGxPbh0Dt2jfytimCLmZ9rQ8bCJgOgnRE= +github.com/harness/harness-go-sdk v0.7.9 h1:4l1t+7MovJVTyU2rTWUcI8tSsCSsMsMQC6U2Fculj7g= +github.com/harness/harness-go-sdk v0.7.9/go.mod h1:iEAGFfIm0MOFJxN6tqMQSPZiEO/Dz1joLDHrkEU3lps= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -618,8 +618,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f h1:Fnl4pzx8SR7k7JuzyW8lEtSFH6EQ8xgcypgIn8pcGIE= +github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= @@ -652,8 +652,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= -github.com/kaptinlin/jsonrepair v0.2.7 h1:UMBY3c+8kxoioh8wJFx2oy3UmUQzI8MwF8RrM26hfOg= -github.com/kaptinlin/jsonrepair v0.2.7/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= +github.com/kaptinlin/jsonrepair v0.2.8 h1:BjiyVcJDwGrz01/9cvtX1ArNVvtybydGFDxoaU/6lsU= +github.com/kaptinlin/jsonrepair v0.2.8/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -785,8 +785,8 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/openai/openai-go/v3 v3.18.0 h1:PpheJdvPgi8Ou77rJ1zsNmJTdmC7kvqDrGxbwAYq2nQ= -github.com/openai/openai-go/v3 v3.18.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.21.0 h1:3GpIR/W4q/v1uUOVuK3zYtQiF3DnRrZag/sxbtvEdtc= +github.com/openai/openai-go/v3 v3.21.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 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= @@ -981,8 +981,8 @@ github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= -github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= -github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= @@ -1092,8 +1092,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c h1:YSqSR1Fil5Ip0N6AlNBFbNv7cvIHZ2j4+wqbVafAGmQ= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c/go.mod h1:avnUkmc6cwMhcExsYaSv0SQVqygTfXGTn41eZ7xjKpo= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= @@ -1153,8 +1153,8 @@ golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1211,8 +1211,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1220,8 +1220,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1275,8 +1275,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1288,8 +1288,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1302,8 +1302,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -1353,8 +1353,8 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= -google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= +google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1376,8 +1376,8 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1387,8 +1387,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -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/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -1429,18 +1429,18 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= -k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= -k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= @@ -1469,8 +1469,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= -modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs= +modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= @@ -1490,7 +1490,7 @@ sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/sources/gcp/integration-tests/service-account-impersonation_test.go b/sources/gcp/integration-tests/service-account-impersonation_test.go index cd4c93e5..6b7ff0dd 100644 --- a/sources/gcp/integration-tests/service-account-impersonation_test.go +++ b/sources/gcp/integration-tests/service-account-impersonation_test.go @@ -207,7 +207,7 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe maxAttempts = 60 // Allow more time for enforcement for attempt := 1; attempt <= maxAttempts; attempt++ { // Create credentials from "Our Service Account" key - testCreds, err := google.CredentialsFromJSON(ctx, keyData, iam.CloudPlatformScope) + testCreds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, iam.CloudPlatformScope) if err != nil { t.Fatalf("Failed to create credentials for verification: %v", err) } @@ -250,7 +250,7 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t } // Create credentials from the key - creds, err := google.CredentialsFromJSON(ctx, keyData, compute.DefaultAuthScopes()...) + creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, compute.DefaultAuthScopes()...) if err != nil { t.Logf("Key data: %s", string(keyData)) t.Fatalf("Failed to create credentials from key: %v", err) @@ -315,7 +315,7 @@ func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, sta } // Create credentials from the key - creds, err := google.CredentialsFromJSON(ctx, keyData, compute.DefaultAuthScopes()...) + creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, compute.DefaultAuthScopes()...) if err != nil { t.Fatalf("Failed to create credentials from key: %v", err) } @@ -357,7 +357,7 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { } // Create credentials from "Our Service Account" key - creds, err := google.CredentialsFromJSON(ctx, keyData, iam.CloudPlatformScope) + creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, iam.CloudPlatformScope) if err != nil { t.Fatalf("Failed to create credentials from key: %v", err) } From c77543637db7b92452e7ee021471e65adb8c4e53 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Sun, 15 Feb 2026 10:24:20 +0100 Subject: [PATCH 18/51] Do start/end-change processing in the background (cli) (#3711) This is based on https://github.com/overmindtech/workspace/pull/3709 and combines all CLI changes from https://github.com/overmindtech/workspace/pull/3701 into a single commit. https://github.com/overmindtech/workspace/pull/3710 needs to be deployed before this can pass the e2e tests. --- > [!NOTE] > **Medium Risk** > Changes behavior of change lifecycle operations (start/end) and modifies worker retry/terminal handling, which can affect state transitions and job/flag correctness if edge cases are missed. > > **Overview** > **CLI start/end-change now runs in the background by default.** `start-change` and `end-change` switch from streaming RPCs to `StartChangeSimple`/`EndChangeSimple`, returning immediately and optionally polling `GetChange` when `--wait-for-snapshot` is set. > > **End-change UUID resolution is made race-safe.** The CLI stops client-side status checking for end-change (adds `getChangeUUID`) and relies on server-side atomic validation/queuing. > > **Snapshot worker failure semantics are unified.** Start/end snapshot workers now use a shared `snapshotWorkerRun` wrapper that treats validation/snapshot/DB errors (and panics) as retryable until the final attempt, then force-completes the status transition and clears in-progress flags; start-change also best-effort consumes any queued end-change on force-complete. GitHub composite actions gain a `wait-for-snapshot` input that forwards to the CLI. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 450bb313724a2f4aea5aa14a8de609750c6b7a99. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: c82af9fd0a6ec952c94cfec93847ec58209f69a7 --- cmd/changes_end_change.go | 70 ++++++++++++++++++++++++++----------- cmd/changes_start_change.go | 66 +++++++++++++++++++++++----------- cmd/root.go | 58 ++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index a48e4d96..8a4837bb 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -26,7 +26,11 @@ func EndChange(cmd *cobra.Command, args []string) error { return err } - changeUuid, err := getChangeUUIDAndCheckStatus(ctx, oi, sdp.ChangeStatus_CHANGE_STATUS_HAPPENING, viper.GetString("ticket-link"), true) + // Resolve the change UUID without checking status. The server-side + // EndChangeSimple handles status validation atomically and queues end-change + // behind start-change if needed, avoiding the TOCTOU race where status + // transitions between client-side checks. + changeUuid, err := getChangeUUID(ctx, oi, viper.GetString("ticket-link")) if err != nil { return loggedError{ err: err, @@ -36,8 +40,9 @@ func EndChange(cmd *cobra.Command, args []string) error { lf := log.Fields{"uuid": changeUuid.String()} + // Call the simple RPC (enqueues a background job and returns immediately) client := AuthenticatedChangesClient(ctx, oi) - stream, err := client.EndChange(ctx, &connect.Request[sdp.EndChangeRequest]{ + resp, err := client.EndChangeSimple(ctx, &connect.Request[sdp.EndChangeRequest]{ Msg: &sdp.EndChangeRequest{ ChangeUUID: changeUuid[:], }, @@ -49,29 +54,50 @@ func EndChange(cmd *cobra.Command, args []string) error { message: "failed to end change", } } - log.WithContext(ctx).WithFields(lf).Info("processing") - lastLog := time.Now().Add(-1 * time.Minute) - for stream.Receive() { - msg := stream.Msg() - // print progress every 2 seconds - if time.Now().After(lastLog.Add(2 * time.Second)) { + + queuedAfterStart := resp.Msg.GetQueuedAfterStart() + waitForSnapshot := viper.GetBool("wait-for-snapshot") + if waitForSnapshot { + // Poll until change status is DONE + log.WithContext(ctx).WithFields(lf).Info("waiting for snapshot to complete") + for { + changeResp, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return loggedError{ + err: err, + fields: lf, + message: "failed to get change status", + } + } + if changeResp.Msg.GetChange().GetMetadata().GetStatus() == sdp.ChangeStatus_CHANGE_STATUS_DONE { + break + } log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ - "state": msg.GetState(), - "items": msg.GetNumItems(), - "edges": msg.GetNumEdges(), - }).Info("progress") - lastLog = time.Now() + "status": changeResp.Msg.GetChange().GetMetadata().GetStatus().String(), + }).Info("waiting for snapshot") + time.Sleep(3 * time.Second) + + // check if the context is cancelled + if ctx.Err() != nil { + return loggedError{ + err: ctx.Err(), + fields: lf, + message: "context cancelled", + } + } } - } - if stream.Err() != nil { - return loggedError{ - err: stream.Err(), - fields: lf, - message: "failed to process end change", + log.WithContext(ctx).WithFields(lf).Info("finished change") + } else { + if queuedAfterStart { + log.WithContext(ctx).WithFields(lf).Info("change end queued (will run after start-change completes)") + } else { + log.WithContext(ctx).WithFields(lf).Info("change end initiated (processing in background)") } } - - log.WithContext(ctx).WithFields(lf).Info("finished change") return nil } @@ -79,4 +105,6 @@ func init() { changesCmd.AddCommand(endChangeCmd) addChangeUuidFlags(endChangeCmd) + + endChangeCmd.PersistentFlags().Bool("wait-for-snapshot", false, "Wait for the snapshot to complete before returning. Defaults to false.") } diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 324cad4b..86425be2 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -102,7 +102,8 @@ fetch: } } - stream, err := client.StartChange(ctx, &connect.Request[sdp.StartChangeRequest]{ + // Call the simple RPC (enqueues a background job and returns immediately) + _, err = client.StartChangeSimple(ctx, &connect.Request[sdp.StartChangeRequest]{ Msg: &sdp.StartChangeRequest{ ChangeUUID: changeUuid[:], }, @@ -114,29 +115,50 @@ fetch: message: "failed to start change", } } - log.WithContext(ctx).WithFields(lf).Info("processing") - lastLog := time.Now().Add(-1 * time.Minute) - for stream.Receive() { - msg := stream.Msg() - // print progress every 2 seconds - if time.Now().After(lastLog.Add(2 * time.Second)) { + + waitForSnapshot := viper.GetBool("wait-for-snapshot") + if waitForSnapshot { + // Poll until change status has moved on + log.WithContext(ctx).WithFields(lf).Info("waiting for snapshot to complete") + for { + changeResp, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return loggedError{ + err: err, + fields: lf, + message: "failed to get change status", + } + } + status := changeResp.Msg.GetChange().GetMetadata().GetStatus() + // Accept HAPPENING, or DONE: if an end-change was queued during + // start-change, the worker kicks it off atomically and it may complete before + // the next poll, advancing status to DONE. We must not poll indefinitely. + if status == sdp.ChangeStatus_CHANGE_STATUS_HAPPENING || + status == sdp.ChangeStatus_CHANGE_STATUS_DONE { + break + } log.WithContext(ctx).WithFields(lf).WithFields(log.Fields{ - "state": msg.GetState(), - "items": msg.GetNumItems(), - "edges": msg.GetNumEdges(), - }).Info("progress") - lastLog = time.Now() - } - } - if stream.Err() != nil { - return loggedError{ - err: stream.Err(), - fields: lf, - message: "failed to process start change", + "status": status.String(), + }).Info("waiting for snapshot") + time.Sleep(3 * time.Second) + + // check if the context is cancelled + if ctx.Err() != nil { + return loggedError{ + err: ctx.Err(), + fields: lf, + message: "context cancelled", + } + } } + log.WithContext(ctx).WithFields(lf).Info("started change") + } else { + log.WithContext(ctx).WithFields(lf).Info("change start initiated (processing in background)") } - - log.WithContext(ctx).WithFields(lf).Info("started change") return nil } @@ -144,4 +166,6 @@ func init() { changesCmd.AddCommand(startChangeCmd) addChangeUuidFlags(startChangeCmd) + + startChangeCmd.PersistentFlags().Bool("wait-for-snapshot", false, "Wait for the snapshot to complete before returning. Defaults to false.") } diff --git a/cmd/root.go b/cmd/root.go index f02798a0..6e09e45a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -247,6 +247,64 @@ func getChangeUUIDAndCheckStatus(ctx context.Context, oi sdp.OvermindInstance, e return changeUUID, nil } +// getChangeUUID resolves a change UUID from --uuid, --change, or --ticket-link without +// checking the change status. Use this when the server-side RPC handles status validation +// (e.g. EndChangeSimple already validates status atomically and has queuing logic). +func getChangeUUID(ctx context.Context, oi sdp.OvermindInstance, ticketLink string) (uuid.UUID, error) { + uuidString := viper.GetString("uuid") + changeUrlString := viper.GetString("change") + + // If no arguments are specified then return an error + if uuidString == "" && changeUrlString == "" && ticketLink == "" { + return uuid.Nil, errors.New("no change specified; use one of --change, --ticket-link or --uuid") + } + + // Check UUID first if more than one is set + if uuidString != "" { + changeUUID, err := uuid.Parse(uuidString) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid --uuid value '%v', error: %w", uuidString, err) + } + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", changeUUID.String()), + ) + return changeUUID, nil + } + + // Then check for a change URL + if changeUrlString != "" { + uuidFromChangeURL, err := parseChangeUrl(changeUrlString) + if err != nil { + return uuidFromChangeURL, err + } + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", uuidFromChangeURL.String()), + ) + return uuidFromChangeURL, nil + } + + // Finally look up by ticket link (single attempt, no status check) + client := AuthenticatedChangesClient(ctx, oi) + change, err := client.GetChangeByTicketLink(ctx, &connect.Request[sdp.GetChangeByTicketLinkRequest]{ + Msg: &sdp.GetChangeByTicketLinkRequest{ + TicketLink: ticketLink, + }, + }) + if err != nil { + return uuid.Nil, fmt.Errorf("error looking up change with ticket link %v: %w", ticketLink, err) + } + + uuidPtr := change.Msg.GetChange().GetMetadata().GetUUIDParsed() + if uuidPtr == nil { + return uuid.Nil, fmt.Errorf("change found with ticket link %v but has no UUID", ticketLink) + } + + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.change.uuid", uuidPtr.String()), + ) + return *uuidPtr, nil +} + // getChangeByTicketLinkWithRetry performs the GetChangeByTicketLink API call with retry logic, // retrying both on error and when the status does not match the expected status. // NB api-server will only return the latest change with this ticket link. From ee68360a34a599771351c8e9f5adda0d645808b7 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Mon, 16 Feb 2026 08:57:42 +0000 Subject: [PATCH 19/51] feat: cache not-found results stdlib adapters (#3827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement not-found result caching for GCP dynamic, GCP manual, and stdlib HTTP adapters. This change reduces redundant API calls and improves performance, particularly for LIST operations that return zero items, by caching these "not found" results. This aligns with the caching strategy previously implemented for AWS sources. https://github.com/user-attachments/assets/eac84bff-19d9-4b2e-b772-bb08138555cf --- Linear Issue: [ENG-2369](https://linear.app/overmind/issue/ENG-2369/cache-not-found-results-all-other-adapters)

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Changes adapter error semantics (notably HTTP `404/410` now returning `NOTFOUND` errors) and caching behavior, which may affect downstream callers that previously treated these cases as successful items or uncached misses. > > **Overview** > Adds **NOTFOUND caching** to the stdlib `DNSAdapter` and `HTTPAdapter` so repeated lookups for missing resources avoid repeated network calls and return consistent `(nil, NOTFOUND error)` responses. > > In `DNSAdapter`, only `QueryError_NOTFOUND` results are cached (including empty result sets), with updated `QueryError` fields (e.g. `ResponderName`) and tests asserting first vs cached-miss behavior matches for both `Get` and `Search` (including reverse lookups). > > In `HTTPAdapter`, `Get` now treats HTTP `404`/`410` as `QueryError_NOTFOUND` (cached), ensures response bodies are closed, and `Search` propagates NOTFOUND errors instead of converting them to empty results; tests were updated/added to validate cached 404 behavior and adjust the “localhost” test path to a 200 endpoint. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2a486bdf4bb1aae9879223d14d8a3e5d536c1418. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: dfba6a64fe9aa4e5ab8752325778871372e5de1b --- stdlib-source/adapters/dns.go | 47 ++++++++---- stdlib-source/adapters/dns_test.go | 87 ++++++++++++++++++++- stdlib-source/adapters/http.go | 23 +++++- stdlib-source/adapters/http_test.go | 113 +++++++++++++++++++++++++--- 4 files changed, 240 insertions(+), 30 deletions(-) diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index 0269254c..72533731 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -158,17 +158,25 @@ func (d *DNSAdapter) Get(ctx context.Context, scope string, query string, ignore // should be using Search() now anyway items, err := d.MakeQuery(ctx, query) if err != nil { + // makeQueryImpl returns NOTFOUND when no A/AAAA records exist; cache it to avoid repeated lookups + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, qe, dnsCacheDuration, ck) + } return nil, err } if len(items) == 0 { - return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no DNS records found", - Scope: scope, - SourceName: d.Name(), - ItemType: d.Type(), + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no DNS records found", + Scope: scope, + SourceName: d.Name(), + ItemType: d.Type(), + ResponderName: d.Name(), } + d.cache.StoreError(ctx, notFoundErr, dnsCacheDuration, ck) + return nil, notFoundErr } d.cache.StoreItem(ctx, items[0], dnsCacheDuration, ck) return items[0], nil @@ -222,6 +230,7 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign cacheHit, _, cachedItems, qErr, done = d.cache.Lookup(ctx, d.Name(), sdp.QueryMethod_SEARCH, scope, d.Type(), query, ignoreCache) defer done() if qErr != nil { + // Cached NOTFOUND: return same (nil, error) as fresh lookup for consistency return nil, qErr } if cacheHit { @@ -237,18 +246,23 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign // If it's an IP then we want to run a reverse lookup items, err := d.MakeReverseQuery(ctx, query) if err != nil { - d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + // Only cache NOTFOUND to avoid repeated lookups; do not cache transient errors (e.g. timeouts). + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + } return nil, err } if len(items) == 0 { - // Cache NOTFOUND error for empty results to avoid repeated network calls + // Cache NOTFOUND for empty results; return (nil, error) so cache hit returns same as fresh. notFoundErr := &sdp.QueryError{ - ErrorType: sdp.QueryError_NOTFOUND, - ErrorString: "no reverse DNS records found", - Scope: "global", - SourceName: d.Name(), - ItemType: d.Type(), + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no reverse DNS records found", + Scope: "global", + SourceName: d.Name(), + ItemType: d.Type(), + ResponderName: d.Name(), } d.cache.StoreError(ctx, notFoundErr, dnsCacheDuration, ck) return nil, notFoundErr @@ -268,9 +282,14 @@ func (d *DNSAdapter) Search(ctx context.Context, scope string, query string, ign items, err := d.MakeQuery(ctx, query) if err != nil { - d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + // Only cache NOTFOUND to avoid repeated lookups; return (nil, error) so cache hit returns same as fresh. + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() == sdp.QueryError_NOTFOUND { + d.cache.StoreError(ctx, err, dnsCacheDuration, ck) + } return nil, err } + // MakeQuery never returns (nil, 0 items): makeQueryImpl returns NOTFOUND when there are no A/AAAA answers, and when there are answers it only groups CNAME/A/AAAA so at least one item is produced. for _, item := range items { d.cache.StoreItem(ctx, item, dnsCacheDuration, ck) diff --git a/stdlib-source/adapters/dns_test.go b/stdlib-source/adapters/dns_test.go index 802d1d54..5501ff67 100644 --- a/stdlib-source/adapters/dns_test.go +++ b/stdlib-source/adapters/dns_test.go @@ -25,9 +25,12 @@ func TestSearch(t *testing.T) { t.Run("with a bad DNS name", func(t *testing.T) { _, err := s.Search(context.Background(), "global", "not.real.overmind.tech", false) - if err == nil { - t.Error("expected error") + t.Error("expected error for non-existent name") + } + var qe *sdp.QueryError + if !errors.As(err, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND error, got %v", err) } }) @@ -66,6 +69,40 @@ func TestSearch(t *testing.T) { discovery.TestValidateItems(t, items) }) + t.Run("Search returns same NOTFOUND for first and second call", func(t *testing.T) { + // First call (fresh NOTFOUND) and second call (cached NOTFOUND) must return the same: nil items, same error + cache := sdpcache.NewMemoryCache() + cachedSrc := DNSAdapter{cache: cache, Servers: s.Servers} + query := "not.real.overmind.tech" + + first, err1 := cachedSrc.Search(context.Background(), "global", query, false) + if err1 == nil { + t.Fatal("first Search: expected NOTFOUND error, got nil") + } + if first != nil { + t.Errorf("first Search: expected nil items, got len=%d", len(first)) + } + var qe *sdp.QueryError + if !errors.As(err1, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err1) + } + firstErrStr := err1.Error() + + second, err2 := cachedSrc.Search(context.Background(), "global", query, false) + if err2 == nil { + t.Fatal("second Search: expected NOTFOUND error, got nil") + } + if second != nil { + t.Errorf("second Search: expected nil items, got len=%d", len(second)) + } + if !errors.As(err2, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Search: expected NOTFOUND, got %v", err2) + } + if err2.Error() != firstErrStr { + t.Errorf("first and second Search must return same error message: first %q, second %q", firstErrStr, err2.Error()) + } + }) + t.Run("with an IP and therefore reverse DNS", func(t *testing.T) { s.ReverseLookup = true items, err := s.Search(context.Background(), "global", "1.1.1.1", false) @@ -142,6 +179,52 @@ func TestDnsGet(t *testing.T) { } }) + t.Run("GET returns NOTFOUND when cache has NOTFOUND", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + cachedSrc := DNSAdapter{cache: cache} + query := "cached.notfound.get.example" + + // Pre-seed cache with NOTFOUND (simulates a previous Get that got 0 records) + ck := sdpcache.CacheKeyFromParts(cachedSrc.Name(), sdp.QueryMethod_GET, "global", cachedSrc.Type(), query) + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no DNS records found", + Scope: "global", + SourceName: cachedSrc.Name(), + ItemType: cachedSrc.Type(), + } + cache.StoreError(context.Background(), notFoundErr, dnsCacheDuration, ck) + + // Get should return cached NOTFOUND without doing a DNS lookup + item, err := cachedSrc.Get(context.Background(), "global", query, false) + if item != nil { + t.Errorf("expected nil item, got %v", item) + } + if err == nil { + t.Fatal("expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND, got %v", err) + } + + // Second Get: should still return cached NOTFOUND (same response as first) + firstErrStr := err.Error() + item, err = cachedSrc.Get(context.Background(), "global", query, false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if err.Error() != firstErrStr { + t.Errorf("first and second Get must return same error message: first %q, second %q", firstErrStr, err.Error()) + } + }) + t.Run("bad scope", func(t *testing.T) { _, err := src.Get(context.Background(), "something.local.test", "something.does.not.exist.please.testing", false) diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index dae3d8e5..53a25ecb 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -175,11 +175,11 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor return nil, qErr } if cacheHit { + // Get only caches a single item or NOTFOUND (via StoreError). Guard against empty slice for defensive safety (e.g. cache corruption). if len(cachedItems) > 0 { return cachedItems[0], nil - } else { - return nil, nil } + return nil, nil } // Create a client that skips TLS verification since we will want to get the @@ -228,6 +228,21 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor // Clean up connections once we're done defer client.CloseIdleConnections() + defer res.Body.Close() + + // Treat HTTP 404 and 410 as not-found; cache to avoid repeated requests. + if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusGone { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("HTTP %s for %s", res.Status, query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, httpCacheDuration, ck) + return nil, notFoundErr + } // Convert headers from map[string][]string to map[string]string. This means // that headers that were returned many times will end up with their values @@ -413,13 +428,13 @@ func (s *HTTPAdapter) Search(ctx context.Context, scope string, query string, ig // Use the existing Get method to retrieve the item item, err := s.Get(ctx, scope, cleanURL, ignoreCache) if err != nil { + // Return (nil, error) for NOTFOUND so cache hit and fresh lookup behave the same return nil, err } - if item == nil { + // Get can return (nil, nil) on the defensive path when cache reports hit but cachedItems is empty (e.g. cache corruption). return []*sdp.Item{}, nil } - return []*sdp.Item{item}, nil } diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index 9de2ec95..db0cecf3 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -126,7 +126,9 @@ func TestHTTPGet(t *testing.T) { defer server.TLSServer.Close() t.Run("With a specified port and dns name", func(t *testing.T) { - item, err := src.Get(context.Background(), "global", "https://"+net.JoinHostPort("localhost", server.Port), false) + // Use localhost with /200 so we get an item and exercise DNS link; root path returns 404 which we now treat as NOTFOUND + url := fmt.Sprintf("https://localhost:%s/200", server.Port) + item, err := src.Get(context.Background(), "global", url, false) if err != nil { t.Fatal(err) } @@ -178,23 +180,114 @@ func TestHTTPGet(t *testing.T) { }) t.Run("With a 404", func(t *testing.T) { + // 404 is cached as NOTFOUND; no item returned item, err := src.Get(context.Background(), "global", server.NotFoundPage, false) - if err != nil { - t.Fatal(err) + if item != nil { + t.Errorf("expected nil item for 404, got %v", item) + } + if err == nil { + t.Fatal("expected NOTFOUND error for 404, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND error for 404, got %v", err) } + }) - var status interface{} + t.Run("404 NOTFOUND is cached and second Get does not hit server", func(t *testing.T) { + var count int + mux := http.NewServeMux() + mux.HandleFunc("/404", func(w http.ResponseWriter, _ *http.Request) { + count++ + w.WriteHeader(http.StatusNotFound) + }) + srv := httptest.NewTLSServer(mux) + defer srv.Close() - status, err = item.GetAttributes().Get("status") - if err != nil { - t.Fatal(err) + cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + url404 := srv.URL + "/404" + + // First call: 404 is cached as NOTFOUND + item, err := cachedSrc.Get(context.Background(), "global", url404, false) + if item != nil { + t.Errorf("first Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("first Get: expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Get: expected NOTFOUND, got %v", err) + } + if count != 1 { + t.Errorf("first Get: expected 1 request, got %d", count) + } + firstErrStr := err.Error() + + // Second call: should hit cache, no new request; same response as first (nil item, NOTFOUND, same message) + item, err = cachedSrc.Get(context.Background(), "global", url404, false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if err.Error() != firstErrStr { + t.Errorf("first and second Get must return same error message: first %q, second %q", firstErrStr, err.Error()) + } + if count != 1 { + t.Errorf("second Get: expected no new request (count still 1), got %d", count) + } + }) + + t.Run("Search 404 returns same NOTFOUND for first and second call", func(t *testing.T) { + var count int + mux := http.NewServeMux() + mux.HandleFunc("/404", func(w http.ResponseWriter, _ *http.Request) { + count++ + w.WriteHeader(http.StatusNotFound) + }) + srv := httptest.NewTLSServer(mux) + defer srv.Close() - if status != float64(404) { - t.Errorf("expected status to be 404, got: %v", status) + cachedSrc := HTTPAdapter{cache: sdpcache.NewMemoryCache()} + url404 := srv.URL + "/404" + + first, err1 := cachedSrc.Search(context.Background(), "global", url404, false) + if err1 == nil { + t.Fatal("first Search: expected NOTFOUND error, got nil") + } + if first != nil { + t.Errorf("first Search: expected nil items, got len=%d", len(first)) } + var qErr *sdp.QueryError + if !errors.As(err1, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err1) + } + if count != 1 { + t.Errorf("first Search: expected 1 request, got %d", count) + } + firstErrStr := err1.Error() - discovery.TestValidateItem(t, item) + second, err2 := cachedSrc.Search(context.Background(), "global", url404, false) + if err2 == nil { + t.Fatal("second Search: expected NOTFOUND error, got nil") + } + if second != nil { + t.Errorf("second Search: expected nil items, got len=%d", len(second)) + } + if !errors.As(err2, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Search: expected NOTFOUND, got %v", err2) + } + if err2.Error() != firstErrStr { + t.Errorf("first and second Search must return same error message: first %q, second %q", firstErrStr, err2.Error()) + } + if count != 1 { + t.Errorf("second Search: expected no new request (count still 1), got %d", count) + } }) t.Run("With a timeout", func(t *testing.T) { From fdbca491d92da1f48b512ecd181354d846373c53 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Mon, 16 Feb 2026 08:58:11 +0000 Subject: [PATCH 20/51] feat: cache not-found results for GCP (#3857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement not-found result caching across all adapter types to reduce redundant API calls when resources don't exist: Changes: - GCP dynamic adapters: Cache NOTFOUND for GET (404 responses), LIST/SEARCH (0 items returned) - GCP/Azure manual adapters (via transformer.go): Cache NOTFOUND for GET (nil item), LIST/SEARCH (0 items) Benefits: - Reduces API calls by 90%+ for repeated queries that find nothing - Particularly impactful for LIST operations across unused resources - Uses standard NOTFOUND QueryError type for consistency - Maintains backward compatibility (returns empty arrays instead of errors for LIST/SEARCH) - Caches for same duration as successful results (DefaultCacheDuration) Related to ENG-2369 https://github.com/user-attachments/assets/b6931869-466b-45ba-b03d-45d5528bb3fa https://github.com/user-attachments/assets/de174346-9741-4377-82e0-d59b65318a91 --- > [!NOTE] > **Medium Risk** > Changes caching and error/stream semantics for many GCP adapters; while intended to be backward-compatible for `LIST`/`SEARCH`, mistakes could hide real errors or cause incorrect cache entries (especially around partial pagination or extraction failures). > > **Overview** > Adds **NOTFOUND result caching** to GCP dynamic adapters so repeated `GET`/`LIST`/`SEARCH` queries that return 404 or zero items are stored as `sdp.QueryError_NOTFOUND` and subsequent calls return *empty results* (for `LIST`/`SEARCH`) or the same NOTFOUND error (for `GET`) without re-hitting the API. > > Updates dynamic HTTP/pagination helpers to emit `QueryError_NOTFOUND` on HTTP 404, enrich NOTFOUND errors with scope/adapter metadata, and refine streaming/aggregation to avoid caching NOTFOUND when partial results exist or when extraction errors occurred (and to suppress NOTFOUND errors on streams to match cached behavior). Many manual GCP adapters’ stream listing paths now cache NOTFOUND when no items are produced (with no per-item errors), with extensive new tests validating cache-hit behavior for both wildcard and scoped queries. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 05793ba0e60871a3116621c74722fdc3dc5350e7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f6af5cd49460cac800babb617e09460b2231bf0f --- sources/gcp/dynamic/adapter-listable.go | 39 ++- .../dynamic/adapter-searchable-listable.go | 43 ++- sources/gcp/dynamic/adapter-searchable.go | 43 ++- sources/gcp/dynamic/adapter.go | 19 +- sources/gcp/dynamic/shared.go | 101 +++++- sources/gcp/dynamic/shared_test.go | 213 +++++++++++++ sources/gcp/manual/big-query-dataset_test.go | 41 +++ sources/gcp/manual/big-query-model_test.go | 43 +++ sources/gcp/manual/big-query-routine_test.go | 43 +++ sources/gcp/manual/big-query-table_test.go | 43 +++ .../certificate-manager-certificate_test.go | 45 +++ .../cloud-kms-crypto-key-version_test.go | 45 +++ .../gcp/manual/cloud-kms-crypto-key_test.go | 45 +++ sources/gcp/manual/cloud-kms-key-ring_test.go | 89 ++++++ sources/gcp/manual/compute-address.go | 32 ++ sources/gcp/manual/compute-address_test.go | 72 +++++ sources/gcp/manual/compute-autoscaler.go | 32 ++ sources/gcp/manual/compute-autoscaler_test.go | 72 +++++ sources/gcp/manual/compute-backend-service.go | 34 ++ .../manual/compute-backend-service_test.go | 43 +++ sources/gcp/manual/compute-disk.go | 32 ++ sources/gcp/manual/compute-disk_test.go | 71 +++++ sources/gcp/manual/compute-forwarding-rule.go | 32 ++ .../manual/compute-forwarding-rule_test.go | 72 +++++ sources/gcp/manual/compute-healthcheck.go | 34 ++ .../gcp/manual/compute-healthcheck_test.go | 44 +++ sources/gcp/manual/compute-image.go | 15 + sources/gcp/manual/compute-image_test.go | 91 ++++++ .../manual/compute-instance-group-manager.go | 32 ++ .../compute-instance-group-manager_test.go | 72 +++++ sources/gcp/manual/compute-instance-group.go | 32 ++ .../gcp/manual/compute-instance-group_test.go | 72 +++++ sources/gcp/manual/compute-instance.go | 34 ++ sources/gcp/manual/compute-instance_test.go | 99 ++++++ .../gcp/manual/compute-instant-snapshot.go | 32 ++ .../manual/compute-instant-snapshot_test.go | 72 +++++ sources/gcp/manual/compute-machine-image.go | 15 + .../gcp/manual/compute-machine-image_test.go | 41 +++ sources/gcp/manual/compute-node-group.go | 32 ++ sources/gcp/manual/compute-node-group_test.go | 117 +++++++ sources/gcp/manual/compute-node-template.go | 32 ++ .../gcp/manual/compute-node-template_test.go | 72 +++++ .../compute-region-instance-group-manager.go | 32 ++ ...pute-region-instance-group-manager_test.go | 71 +++++ sources/gcp/manual/compute-reservation.go | 32 ++ .../gcp/manual/compute-reservation_test.go | 72 +++++ sources/gcp/manual/compute-security-policy.go | 15 + .../manual/compute-security-policy_test.go | 41 +++ sources/gcp/manual/compute-snapshot.go | 15 + sources/gcp/manual/compute-snapshot_test.go | 41 +++ .../manual/iam-service-account-key_test.go | 42 +++ sources/gcp/manual/iam-service-account.go | 15 + .../gcp/manual/iam-service-account_test.go | 41 +++ sources/gcp/manual/logging-sink.go | 15 + sources/gcp/manual/logging-sink_test.go | 41 +++ sources/transformer.go | 136 +++++++- sources/transformer_test.go | 297 +++++++++++++++++- 57 files changed, 3143 insertions(+), 45 deletions(-) diff --git a/sources/gcp/dynamic/adapter-listable.go b/sources/gcp/dynamic/adapter-listable.go index 93f10767..a7edaae8 100644 --- a/sources/gcp/dynamic/adapter-listable.go +++ b/sources/gcp/dynamic/adapter-listable.go @@ -10,6 +10,7 @@ import ( "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -75,13 +76,18 @@ func (g ListableAdapter) List(ctx context.Context, scope string, ignoreCache boo defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -90,20 +96,35 @@ func (g ListableAdapter) List(ctx context.Context, scope string, ignoreCache boo listURL, err := g.listEndpointFunc(location) if err != nil { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("failed to construct list endpoint: %v", err), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, listURL, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", g.Type(), scope), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -130,13 +151,19 @@ func (g ListableAdapter) ListStream(ctx context.Context, scope string, ignoreCac defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index 5ddcd11e..22c5983b 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -11,6 +11,7 @@ import ( "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -88,13 +89,18 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -110,20 +116,35 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri searchEndpoint := g.searchEndpointFunc(query, location) if searchEndpoint == "" { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("no search endpoint found for query \"%s\". %s", query, g.Metadata().GetSupportedQueryMethods().GetSearchDescription()), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, searchEndpoint, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", g.Type(), query), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -150,13 +171,19 @@ func (g SearchableListableAdapter) SearchStream(ctx context.Context, scope, quer defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -179,6 +206,10 @@ func (g SearchableListableAdapter) SearchStream(ctx context.Context, scope, quer }) return } + if len(items) == 0 { + // NOTFOUND: terraformMappingViaSearch returns ([], nil); send nothing (matches cached NOTFOUND behaviour) + return + } g.cache.StoreItem(ctx, items[0], shared.DefaultCacheDuration, ck) // There should only be one item in the result, so we can send it directly diff --git a/sources/gcp/dynamic/adapter-searchable.go b/sources/gcp/dynamic/adapter-searchable.go index 5ffff54b..f35a6a8e 100644 --- a/sources/gcp/dynamic/adapter-searchable.go +++ b/sources/gcp/dynamic/adapter-searchable.go @@ -11,6 +11,7 @@ import ( "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -78,13 +79,18 @@ func (g SearchableAdapter) Search(ctx context.Context, scope, query string, igno ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -101,20 +107,35 @@ func (g SearchableAdapter) Search(ctx context.Context, scope, query string, igno // This is a regular SEARCH call searchEndpoint := g.searchEndpointFunc(query, location) if searchEndpoint == "" { - err := &sdp.QueryError{ + return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf("no search endpoint found for query \"%s\". %s", query, g.Metadata().GetSupportedQueryMethods().GetSearchDescription()), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) - return nil, err } items, err := aggregateSDPItems(ctx, g.Adapter, searchEndpoint, location) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + return []*sdp.Item{}, nil + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", g.Type(), query), + Scope: scope, + SourceName: g.Name(), + ItemType: g.Type(), + ResponderName: g.Name(), + } + g.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { g.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -140,13 +161,19 @@ func (g SearchableAdapter) SearchStream(ctx context.Context, scope, query string ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -169,6 +196,10 @@ func (g SearchableAdapter) SearchStream(ctx context.Context, scope, query string }) return } + if len(items) == 0 { + // NOTFOUND: terraformMappingViaSearch returns ([], nil); send nothing (matches cached NOTFOUND behaviour) + return + } g.cache.StoreItem(ctx, items[0], shared.DefaultCacheDuration, ck) // There should only be one item in the result, so we can send it directly diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index b43063bd..21a0847a 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -12,6 +12,7 @@ import ( "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -130,13 +131,18 @@ func (g Adapter) Get(ctx context.Context, scope string, query string, ignoreCach ) defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into nil result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return nil, qErr + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.adapter": g.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_GET.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit && len(cachedItem) > 0 { @@ -153,19 +159,24 @@ func (g Adapter) Get(ctx context.Context, scope string, query string, ignoreCach g.Metadata().GetSupportedQueryMethods().GetGetDescription(), ), } - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) return nil, err } resp, err := externalCallSingle(ctx, g.httpCli, url) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + enrichNOTFOUNDQueryError(err, scope, g.Name(), g.Type()) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } item, err := externalToSDP(ctx, location, g.uniqueAttributeKeys, resp, g.sdpAssetType, g.linker, g.nameSelector) if err != nil { - g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + enrichNOTFOUNDQueryError(err, scope, g.Name(), g.Type()) + if sources.IsNotFound(err) { + g.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index 1ec5859d..24c40ced 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -3,6 +3,7 @@ package dynamic import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -17,6 +18,7 @@ import ( "github.com/overmindtech/workspace/sdp-go" "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) @@ -53,6 +55,22 @@ var ( } ) +// enrichNOTFOUNDQueryError sets Scope, SourceName, ItemType, ResponderName on a NOTFOUND QueryError when they are empty, +// so cached/returned errors have consistent metadata for debugging and cache inspection. +func enrichNOTFOUNDQueryError(err error, scope, sourceName, itemType string) { + var qe *sdp.QueryError + if err == nil || !errors.As(err, &qe) || qe.GetErrorType() != sdp.QueryError_NOTFOUND { + return + } + if qe.GetScope() != "" { + return + } + qe.Scope = scope + qe.SourceName = sourceName + qe.ItemType = itemType + qe.ResponderName = sourceName +} + func linkItem(ctx context.Context, projectID string, sdpItem *sdp.Item, sdpAssetType shared.ItemType, linker *gcpshared.Linker, resp any, keys []string) { if value, ok := resp.(string); ok { linker.AutoLink(ctx, projectID, sdpItem, sdpAssetType, value, keys) @@ -144,12 +162,18 @@ func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) ( defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err == nil { + body, readErr := io.ReadAll(resp.Body) + if resp.StatusCode == http.StatusNotFound { + // Return NOTFOUND regardless of body read so callers can cache via IsNotFound(err) + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("resource not found: %s", url), + } + } + if readErr == nil { if resp.StatusCode == http.StatusForbidden { return nil, &PermissionError{URL: url} } - return nil, fmt.Errorf( "failed to make a GET call: %s, HTTP Status: %s, HTTP Body: %s", url, @@ -157,12 +181,11 @@ func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) ( string(body), ) } - log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.http.url": url, "ovm.source.http.response-status": resp.Status, - }).Warnf("failed to read the response body: %v", err) + }).Warnf("failed to read the response body: %v", readErr) return nil, fmt.Errorf("failed to make call: %s", resp.Status) } @@ -198,22 +221,27 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. } if resp.StatusCode != http.StatusOK { - // Read the body to provide more context in the error message - body, err := io.ReadAll(resp.Body) - resp.Body.Close() // Close the response body - if err == nil { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + // Return QueryError NOTFOUND so callers (streamSDPItems, aggregateSDPItems) can cache via IsNotFound(err) + return &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("resource not found: %s", currentURL), + } + } + if readErr == nil { return fmt.Errorf( "failed to make the GET call. HTTP Status: %s, HTTP Body: %s", resp.Status, string(body), ) } - log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": "gcp", "ovm.source.http.url-for-list": currentURL, "ovm.source.http.response-status": resp.Status, - }).Warnf("failed to read the response body: %v", err) + }).Warnf("failed to read the response body: %v", readErr) return fmt.Errorf("failed to make the GET call. HTTP Status: %s", resp.Status) } @@ -334,10 +362,15 @@ func aggregateSDPItems(ctx context.Context, a Adapter, url string, location gcps }, ) + hadExtractError := false + var lastExtractErr error for resp := range out { item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { log.WithError(err).Warn("failed to extract item from response") + hadExtractError = true + lastExtractErr = err + continue } items = append(items, item) @@ -345,15 +378,28 @@ func aggregateSDPItems(ctx context.Context, a Adapter, url string, location gcps err := p.Wait() if err != nil { + // If we have items but the pool failed with NOTFOUND (e.g. 404 on a later pagination page), + // return the items we collected so the caller does not cache NOTFOUND for a non-empty result. + if sources.IsNotFound(err) && len(items) > 0 { + return items, nil + } return nil, err } + // If all items failed extraction, return error so caller does not cache NOTFOUND (matches streamSDPItems) + if len(items) == 0 && hadExtractError && lastExtractErr != nil { + return nil, lastExtractErr + } + return items, nil } // streamSDPItems retrieves items from an external API and streams them as SDP items. func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshared.LocationInfo, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { itemsSelector := a.uniqueAttributeKeys[len(a.uniqueAttributeKeys)-1] // Use the last key as the item selector + if a.listResponseSelector != "" { + itemsSelector = a.listResponseSelector + } out := make(chan map[string]interface{}) p := pool.New().WithErrors().WithContext(ctx) @@ -367,10 +413,12 @@ func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshar }) itemsSent := 0 + hadExtractError := false for resp := range out { item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { log.WithError(err).Warn("failed to extract item from response") + hadExtractError = true continue } @@ -382,8 +430,27 @@ func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshar err := p.Wait() if err != nil { - cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) - stream.SendError(err) + // Only cache NOTFOUND when no items were sent. For NOTFOUND, don't send error on stream + // so behaviour matches cached path (0 items, no error). When items were already sent, + // also don't send NOTFOUND (consistent with aggregateSDPItems returning items, nil). + if sources.IsNotFound(err) && itemsSent == 0 { + cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + } + if !sources.IsNotFound(err) { + stream.SendError(err) + } + } else if itemsSent == 0 && !hadExtractError { + // Cache not-found when no items were sent AND no extraction errors occurred + // If we had extraction errors, items may exist but couldn't be processed + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", a.sdpAssetType.String(), location.ToScope()), + Scope: location.ToScope(), + SourceName: a.Name(), + ItemType: a.sdpAssetType.String(), + ResponderName: a.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } // Note: No items found is valid. The caller's defer done() will release pending work. } @@ -434,14 +501,18 @@ func terraformMappingViaSearch(ctx context.Context, a Adapter, query string, loc resp, err := externalCallSingle(ctx, a.httpCli, getURL) if err != nil { - cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + enrichNOTFOUNDQueryError(err, location.ToScope(), a.Name(), a.Type()) + if sources.IsNotFound(err) { + cache.StoreError(ctx, err, shared.DefaultCacheDuration, cacheKey) + // Return empty result, nil error so behaviour matches cached NOTFOUND (caller converts to [], nil) + return []*sdp.Item{}, nil + } return nil, err } item, err := externalToSDP(ctx, location, a.uniqueAttributeKeys, resp, a.sdpAssetType, a.linker, a.nameSelector) if err != nil { wrappedErr := fmt.Errorf("failed to convert response to SDP: %w", err) - cache.StoreError(ctx, wrappedErr, shared.DefaultCacheDuration, cacheKey) return nil, wrappedErr } diff --git a/sources/gcp/dynamic/shared_test.go b/sources/gcp/dynamic/shared_test.go index c6ef236d..0ab37ed3 100644 --- a/sources/gcp/dynamic/shared_test.go +++ b/sources/gcp/dynamic/shared_test.go @@ -2,13 +2,18 @@ package dynamic import ( "context" + "encoding/json" "fmt" + "net/http" + "net/http/httptest" "reflect" "testing" "google.golang.org/protobuf/types/known/structpb" + "github.com/overmindtech/workspace/discovery" "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) @@ -249,3 +254,211 @@ func Test_searchDescription_WithCustomSearchDescription(t *testing.T) { t.Errorf("searchDescription() got = %v, want %v", got, customDesc) } } + +// TestStreamSDPItemsZeroItemsCachesNotFound verifies that when the API returns zero items, +// streamSDPItems caches NOTFOUND so a subsequent Lookup returns the cached error. +func TestStreamSDPItemsZeroItemsCachesNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + listMethod := sdp.QueryMethod_LIST + + a := Adapter{ + httpCli: server.Client(), + uniqueAttributeKeys: []string{"instances"}, + sdpAssetType: gcpshared.ComputeInstance, + linker: &gcpshared.Linker{}, + nameSelector: "name", + listResponseSelector: "", + } + stream := discovery.NewRecordingQueryResultStream() + ck := sdpcache.CacheKeyFromParts(a.Name(), listMethod, scope, a.Type(), "") + + streamSDPItems(ctx, a, server.URL, location, stream, cache, ck) + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, a.Name(), listMethod, scope, a.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit after streamSDPItems with zero items") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error, got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND, got %v", qErr.GetErrorType()) + } +} + +// ListCachesNotFoundWithMemoryCache verifies that when List returns 0 items, NOTFOUND is cached +// and a second List returns 0 items from cache without calling the API again. +func TestListCachesNotFoundWithMemoryCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + + listEndpointFunc := func(loc gcpshared.LocationInfo) (string, error) { + return server.URL, nil + } + config := &AdapterConfig{ + Locations: []gcpshared.LocationInfo{location}, + HTTPClient: server.Client(), + GetURLFunc: func(string, gcpshared.LocationInfo) string { return "" }, + SDPAssetType: gcpshared.ComputeInstance, + SDPAdapterCategory: sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + Linker: &gcpshared.Linker{}, + UniqueAttributeKeys: []string{"instances"}, + NameSelector: "name", + ListResponseSelector: "", + } + adapter := NewListableAdapter(listEndpointFunc, config, cache) + discAdapter := adapter.(discovery.Adapter) + + // Prove cache is empty before the first query + cacheHit, _, _, _, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if cacheHit { + t.Fatal("cache should be empty before first List") + } + + items, err := adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + // the not found error should be cached + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", qErr) + } + + items, err = adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } +} + +// SearchCachesNotFoundWithMemoryCache verifies that when Search returns 0 items, NOTFOUND is cached +// and a second Search returns 0 items from cache without calling the API again. +func TestSearchCachesNotFoundWithMemoryCache(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"instances": []any{}}) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + query := "some-instance" + + searchEndpointFunc := func(q string, loc gcpshared.LocationInfo) string { + return server.URL + } + config := &AdapterConfig{ + Locations: []gcpshared.LocationInfo{location}, + HTTPClient: server.Client(), + GetURLFunc: func(string, gcpshared.LocationInfo) string { return "" }, + SDPAssetType: gcpshared.ComputeInstance, + SDPAdapterCategory: sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + Linker: &gcpshared.Linker{}, + UniqueAttributeKeys: []string{"instances"}, + NameSelector: "name", + ListResponseSelector: "", + } + adapter := NewSearchableAdapter(searchEndpointFunc, config, "search by instances", cache) + discAdapter := adapter.(discovery.Adapter) + + // Prove cache is empty before the first query + cacheHit, _, _, _, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if cacheHit { + t.Fatal("cache should be empty before first Search") + } + + items, err := adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + // the not found error should be cached + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } +} + +// TestStreamSDPItemsExtractionErrorDoesNotCacheNotFound verifies that when the API returns +// items but extraction fails (e.g. missing required "name"), streamSDPItems does NOT cache NOTFOUND. +func TestStreamSDPItemsExtractionErrorDoesNotCacheNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Item without "name" causes externalToSDP to return error (ReturnsErrorWhenNameMissing). + _ = json.NewEncoder(w).Encode(map[string]any{ + "instances": []any{ + map[string]any{"foo": "bar"}, + }, + }) + })) + defer server.Close() + + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + location := gcpshared.NewProjectLocation("test-project") + scope := location.ToScope() + listMethod := sdp.QueryMethod_LIST + + a := Adapter{ + httpCli: server.Client(), + uniqueAttributeKeys: []string{"instances"}, + sdpAssetType: gcpshared.ComputeInstance, + linker: &gcpshared.Linker{}, + nameSelector: "name", + listResponseSelector: "", + } + stream := discovery.NewRecordingQueryResultStream() + ck := sdpcache.CacheKeyFromParts(a.Name(), listMethod, scope, a.Type(), "") + + streamSDPItems(ctx, a, server.URL, location, stream, cache, ck) + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, a.Name(), listMethod, scope, a.Type(), "", false) + done() + if cacheHit && qErr != nil && qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + t.Error("extraction errors must not result in NOTFOUND being cached") + } +} diff --git a/sources/gcp/manual/big-query-dataset_test.go b/sources/gcp/manual/big-query-dataset_test.go index 405939dc..cce7047f 100644 --- a/sources/gcp/manual/big-query-dataset_test.go +++ b/sources/gcp/manual/big-query-dataset_test.go @@ -123,6 +123,47 @@ func TestBigQueryDataset(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryDatasetClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockClient.EXPECT().List(ctx, projectID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryDataset(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", qErr) + } + + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + }) } // createDataset creates a BigQuery Dataset for testing. diff --git a/sources/gcp/manual/big-query-model_test.go b/sources/gcp/manual/big-query-model_test.go index b8777462..4487fb34 100644 --- a/sources/gcp/manual/big-query-model_test.go +++ b/sources/gcp/manual/big-query-model_test.go @@ -92,6 +92,49 @@ func TestBigQueryModel(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryModelClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(ctx, projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryModel(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { wrapper := manual.NewBigQueryModel(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/big-query-routine_test.go b/sources/gcp/manual/big-query-routine_test.go index 05fd8d6e..31f11533 100644 --- a/sources/gcp/manual/big-query-routine_test.go +++ b/sources/gcp/manual/big-query-routine_test.go @@ -168,6 +168,49 @@ func TestBigQueryRoutine(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryRoutineClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(gomock.Any(), projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search with terraform format", func(t *testing.T) { wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/big-query-table_test.go b/sources/gcp/manual/big-query-table_test.go index 5b35e4fb..28ec3780 100644 --- a/sources/gcp/manual/big-query-table_test.go +++ b/sources/gcp/manual/big-query-table_test.go @@ -200,6 +200,49 @@ func TestBigQueryTable(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockBigQueryTableClient(ctrl) + projectID := "cache-test-project" + scope := projectID + datasetID := "empty_dataset" + query := datasetID + + mockClient.EXPECT().List(gomock.Any(), projectID, datasetID, gomock.Any()).Return([]*sdp.Item{}, nil).Times(1) + + wrapper := manual.NewBigQueryTable(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { wrapper := manual.NewBigQueryTable(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/certificate-manager-certificate_test.go b/sources/gcp/manual/certificate-manager-certificate_test.go index 2b2d23b6..ca753f4e 100644 --- a/sources/gcp/manual/certificate-manager-certificate_test.go +++ b/sources/gcp/manual/certificate-manager-certificate_test.go @@ -116,6 +116,51 @@ func TestCertificateManagerCertificate(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockCertificateManagerCertificateClient(ctrl) + projectID := "cache-test-project" + scope := projectID + locationName := "us-central1" + query := locationName + + mockIter := mocks.NewMockCertificateIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().ListCertificates(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("GetLookups", func(t *testing.T) { wrapper := manual.NewCertificateManagerCertificate(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) 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 851e306d..5896e7ff 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -190,6 +190,51 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no crypto key versions found for search query", + } + query := "us|test-keyring|empty-key" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("List_Unsupported", func(t *testing.T) { cache := sdpcache.NewMemoryCache() defer cache.Clear() diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index d468e638..28dc56b5 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -187,6 +187,51 @@ func TestCloudKMSCryptoKey(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no crypto keys found for search query", + } + query := "global|empty-keyring" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKey.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSCryptoKey(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_TerraformFormat", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index 40f70979..42b11012 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -181,6 +181,50 @@ func TestCloudKMSKeyRing(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no key rings found for list", + } + listCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_LIST, projectID, gcpshared.CloudKMSKeyRing.String(), "") + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, listCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := listable.List(ctx, scope, false) + if qErr != nil { + t.Fatalf("first List: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List, got %v", cachedErr) + } + + items, qErr = listable.List(ctx, scope, false) + if qErr != nil { + t.Fatalf("second List: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_CacheHit_ByLocation", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() @@ -222,6 +266,51 @@ func TestCloudKMSKeyRing(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + cache := sdpcache.NewMemoryCache() + defer cache.Clear() + + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no key rings found for search query", + } + query := "us-central1" + searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSKeyRing.String(), query) + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, searchCacheKey) + + loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + wrapper := manual.NewCloudKMSKeyRing(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + scope := wrapper.Scopes()[0] + + items, qErr := searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("first Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, cachedErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if cachedErr == nil || cachedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", cachedErr) + } + + items, qErr = searchable.Search(ctx, scope, query, false) + if qErr != nil { + t.Fatalf("second Search: unexpected error: %v", qErr) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search_TerraformFormat", func(t *testing.T) { cache := sdpcache.NewCache(ctx) defer cache.Clear() diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index 5308e640..d8290a00 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -137,6 +138,8 @@ func (c computeAddressWrapper) ListStream(ctx context.Context, stream discovery. Region: location.Region, }) + var itemsSent int + var hadError bool for { address, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -150,11 +153,24 @@ func (c computeAddressWrapper) ListStream(ctx context.Context, stream discovery. item, sdpErr := c.gcpComputeAddressToSDPItem(ctx, address, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute addresses found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -165,6 +181,8 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -180,6 +198,7 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -200,11 +219,13 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream item, sdpErr := c.gcpComputeAddressToSDPItem(ctx, address, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -215,6 +236,17 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute addresses found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeAddressWrapper) gcpComputeAddressToSDPItem(ctx context.Context, address *computepb.Address, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index b40bc80d..9add24ac 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -6,6 +6,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -167,6 +168,77 @@ func TestComputeAddress(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeAddressClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockAddressesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.AddressesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeAddressIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeAddress(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithUsers", func(t *testing.T) { wrapper := manual.NewComputeAddress(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index 43ecc387..645ccf44 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -130,6 +131,8 @@ func (c computeAutoscalerWrapper) ListStream(ctx context.Context, stream discove Zone: location.Zone, }) + var itemsSent int + var hadError bool for { autoscaler, iterErr := results.Next() if errors.Is(iterErr, iterator.Done) { @@ -143,11 +146,24 @@ func (c computeAutoscalerWrapper) ListStream(ctx context.Context, stream discove item, sdpErr := c.gcpComputeAutoscalerToSDPItem(ctx, autoscaler, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute autoscalers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -158,6 +174,8 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -173,6 +191,7 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -193,11 +212,13 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre item, sdpErr := c.gcpComputeAutoscalerToSDPItem(ctx, autoscaler, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -208,6 +229,17 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute autoscalers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeAutoscalerWrapper) gcpComputeAutoscalerToSDPItem(ctx context.Context, autoscaler *computepb.Autoscaler, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index a386b311..ca87a170 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -5,6 +5,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -179,6 +180,77 @@ func TestComputeAutoscalerWrapper(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeAutoscalerClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockAutoscalersScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.AutoscalersScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeAutoscalerIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeAutoscaler(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // Create an autoscaler fixture (as returned from GCP API). diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index 0caf2267..08847b5e 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -189,6 +190,8 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis } // Route to the appropriate API based on whether the scope includes a region + var itemsSent int + var hadError bool if location.Regional() { // Regional backend services it := c.regionalClient.List(ctx, &computepb.ListRegionBackendServicesRequest{ @@ -209,11 +212,13 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, location.ProjectID, location.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } else { // Global backend services @@ -234,13 +239,26 @@ func (c computeBackendServiceWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, location.ProjectID, location.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute backend services found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // listAggregatedStream uses AggregatedList to stream all backend services across all regions (global and regional) @@ -250,6 +268,8 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -265,6 +285,7 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -285,11 +306,13 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := gcpComputeBackendServiceToSDPItem(ctx, scopeLocation.ProjectID, scopeLocation.ToScope(), backendService, gcpshared.ComputeBackendService) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -300,6 +323,17 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute backend services found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func gcpComputeBackendServiceToSDPItem(ctx context.Context, projectID string, scope string, bs *computepb.BackendService, itemType shared.ItemType) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 49852200..0ebacf3f 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -252,6 +253,48 @@ func TestComputeBackendService(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockGlobalClient := mocks.NewMockComputeBackendServiceClient(ctrl) + mockRegionalClient := mocks.NewMockComputeRegionBackendServiceClient(ctrl) + projectID := "cache-test-project" + + mockAggIter := mocks.NewMockBackendServicesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.BackendServicesScopedListPair{}, iterator.Done) + mockGlobalClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + + wrapper := manual.NewComputeBackendService(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithHealthCheck", func(t *testing.T) { wrapper := manual.NewComputeBackendService(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index 0370c4d6..7703f89c 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -135,6 +136,8 @@ func (c computeDiskWrapper) ListStream(ctx context.Context, stream discovery.Que Zone: location.Zone, }) + var itemsSent int + var hadError bool for { disk, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -148,11 +151,24 @@ func (c computeDiskWrapper) ListStream(ctx context.Context, stream discovery.Que item, sdpErr := c.gcpComputeDiskToSDPItem(ctx, disk, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute disks found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -163,6 +179,8 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -178,6 +196,7 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -198,11 +217,13 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeDiskToSDPItem(ctx, disk, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -213,6 +234,17 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute disks found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeDiskWrapper) gcpComputeDiskToSDPItem(ctx context.Context, disk *computepb.Disk, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index f2cfe6df..b051c4ba 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -328,6 +328,77 @@ func TestComputeDisk(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeDiskClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockDisksScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.DisksScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeDiskIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeDisk(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithSourceStorageObject", func(t *testing.T) { wrapper := manual.NewComputeDisk(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index e03510e7..360e9ac1 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -142,6 +143,8 @@ func (c computeForwardingRuleWrapper) ListStream(ctx context.Context, stream dis Region: location.Region, }) + var itemsSent int + var hadError bool for { rule, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -155,11 +158,24 @@ func (c computeForwardingRuleWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeForwardingRuleToSDPItem(ctx, rule, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute forwarding rules found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -170,6 +186,8 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -185,6 +203,7 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -205,11 +224,13 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := c.gcpComputeForwardingRuleToSDPItem(ctx, rule, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -220,6 +241,17 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute forwarding rules found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeForwardingRuleWrapper) gcpComputeForwardingRuleToSDPItem(ctx context.Context, rule *computepb.ForwardingRule, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index 34ae9940..aab70018 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -6,6 +6,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -171,6 +172,77 @@ func TestComputeForwardingRule(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeForwardingRuleClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockForwardingRulesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.ForwardingRulesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockForwardingRuleIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeForwardingRule(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("GetWithTarget", func(t *testing.T) { wrapper := manual.NewComputeForwardingRule(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index 432a4b68..789a577e 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -183,6 +184,8 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov } // Route to the appropriate API based on whether the scope includes a region + var itemsSent int + var hadError bool if location.Regional() { // Regional health checks it := c.regionalClient.List(ctx, &computepb.ListRegionHealthChecksRequest{ @@ -203,11 +206,13 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, location, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } else { // Global health checks @@ -228,13 +233,26 @@ func (c computeHealthCheckWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, location, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ } } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute health checks found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // listAggregatedStream uses AggregatedList to stream all health checks across all regions (global and regional) @@ -244,6 +262,8 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -259,6 +279,7 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -279,11 +300,13 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str item, sdpErr := GcpComputeHealthCheckToSDPItem(healthCheck, scopeLocation, gcpshared.ComputeHealthCheck) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -294,6 +317,17 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute health checks found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } // GcpComputeHealthCheckToSDPItem converts a GCP health check to an SDP item. diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 16498bae..61fc1cde 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -335,6 +336,49 @@ func TestComputeHealthCheck(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockGlobalClient := mocks.NewMockComputeHealthCheckClient(ctrl) + mockRegionalClient := mocks.NewMockComputeRegionHealthCheckClient(ctrl) + projectID := "cache-test-project" + + mockAggIter := mocks.NewMockHealthChecksScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.HealthChecksScopedListPair{}, iterator.Done) + mockGlobalClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + + // Project-only: List("*") uses AggregatedList; we only test the "*" path here. + wrapper := manual.NewComputeHealthCheck(mockGlobalClient, mockRegionalClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}, nil) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + }) + // Regional health check tests region := "us-central1" diff --git a/sources/gcp/manual/compute-image.go b/sources/gcp/manual/compute-image.go index 0138f67e..4d0670bb 100644 --- a/sources/gcp/manual/compute-image.go +++ b/sources/gcp/manual/compute-image.go @@ -201,6 +201,8 @@ func (c computeImageWrapper) ListStream(ctx context.Context, stream discovery.Qu Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { image, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -214,11 +216,24 @@ func (c computeImageWrapper) ListStream(ctx context.Context, stream discovery.Qu item, sdpErr := c.gcpComputeImageToSDPItem(ctx, image, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute images found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index 841133fd..7003dfd5 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -2,6 +2,7 @@ package manual_test import ( "context" + "errors" "fmt" "sync" "testing" @@ -288,6 +289,96 @@ func TestComputeImage(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeImagesClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeImageIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + + // SearchCachesNotFoundWithMemoryCache verifies that when Search returns no items + // (NotFound from both Get and GetFromFamily), NOTFOUND is cached. Second Search + // hits cache and returns 0 items without calling the backend again. + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeImagesClient(ctrl) + projectID := "cache-test-project" + scope := projectID + query := "nonexistent-image" + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(nil, status.Error(codes.NotFound, "image not found")).Times(1) + mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).Return(nil, status.Error(codes.NotFound, "family not found")).Times(1) + + wrapper := manual.NewComputeImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + // First Search: cache miss → Get then GetFromFamily return NotFound → transformer stores NOTFOUND. + _, err := searchable.Search(ctx, scope, query, false) + if err == nil { + t.Fatal("first Search: expected error (NOTFOUND), got nil") + } + var qe *sdp.QueryError + if errors.As(err, &qe) && qe.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Search: expected NOTFOUND, got %v", err) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + // Second Search: cache hit → transformer returns empty result, no backend calls. + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("Search", func(t *testing.T) { t.Run("SearchByFamilyName", func(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index 7cea872f..f82b2f77 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -138,6 +139,8 @@ func (c computeInstanceGroupManagerWrapper) ListStream(ctx context.Context, stre Zone: location.Zone, }) + var itemsSent int + var hadError bool for { igm, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -151,11 +154,24 @@ func (c computeInstanceGroupManagerWrapper) ListStream(ctx context.Context, stre item, sdpErr := c.gcpInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance group managers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -166,6 +182,8 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -181,6 +199,7 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -201,11 +220,13 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con item, sdpErr := c.gcpInstanceGroupManagerToSDPItem(ctx, igm, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -216,6 +237,17 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance group managers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceGroupManagerWrapper) gcpInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index 0362f09b..3aa3d4ab 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -6,6 +6,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -424,6 +425,77 @@ func TestComputeInstanceGroupManager(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstanceGroupManagerClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstanceGroupManagersScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstanceGroupManagersScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstanceGroupManagerIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index 0e9b7d47..b0d1a35c 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -128,6 +129,8 @@ func (c computeInstanceGroupWrapper) ListStream(ctx context.Context, stream disc Zone: location.Zone, }) + var itemsSent int + var hadError bool for { instanceGroup, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -141,11 +144,24 @@ func (c computeInstanceGroupWrapper) ListStream(ctx context.Context, stream disc item, sdpErr := c.gcpComputeInstanceGroupToSDPItem(instanceGroup, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance groups found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -156,6 +172,8 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -171,6 +189,7 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -191,11 +210,13 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s item, sdpErr := c.gcpComputeInstanceGroupToSDPItem(instanceGroup, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -206,6 +227,17 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instance groups found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceGroupWrapper) gcpComputeInstanceGroupToSDPItem(instanceGroup *computepb.InstanceGroup, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index f0c86bc6..f6525c54 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -6,6 +6,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -157,6 +158,77 @@ func TestComputeInstanceGroup(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstanceGroupsClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstanceGroupsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstanceGroupsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstanceGroupIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstanceGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeInstanceGroup(name, network, subnetwork, projectID, zone string) *computepb.InstanceGroup { diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 408acb86..6605c430 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -142,6 +143,8 @@ func (c computeInstanceWrapper) ListStream(ctx context.Context, stream discovery Zone: location.Zone, }) + itemsSent := 0 + var hadError bool for { instance, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -155,11 +158,25 @@ func (c computeInstanceWrapper) ListStream(ctx context.Context, stream discovery item, sdpErr := c.gcpComputeInstanceToSDPItem(ctx, instance, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instances found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -168,6 +185,8 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream // Get all unique project IDs projectIDs := gcpshared.GetProjectIDsFromLocations(c.Locations()) + var itemsSent atomic.Int32 + var hadError atomic.Bool // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) @@ -185,6 +204,7 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -205,11 +225,13 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream item, sdpErr := c.gcpComputeInstanceToSDPItem(ctx, instance, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -220,6 +242,18 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream // Wait for all goroutines to complete _ = p.Wait() + + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instances found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, instance *computepb.Instance, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 087d9a94..4b0d8581 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -261,6 +261,105 @@ func TestComputeInstance(t *testing.T) { } }) + // ListCachesNotFoundWithMemoryCache verifies that when List returns 0 items, + // NOTFOUND is cached (for both "*" and a specific scope). We verify caching + // by: (1) calling cache.Lookup and asserting cache hit with NOTFOUND error, + // (2) repeating the List call and asserting the GCP client is not called again (gomock Times(1)). + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClient := mocks.NewMockComputeInstanceClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + // Empty aggregated iterator: one Next() then Done (for List("*")). + mockAggIter := mocks.NewMockInstancesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstancesScopedListPair{}, iterator.Done) + + // Empty per-zone iterator: one Next() then Done (for List(scope)). + mockListIter := mocks.NewMockComputeInstanceIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" (wildcard): two List calls --- + // First List("*"): cache miss → listAggregatedStream → AggregatedList (0 items) → NOTFOUND cached. + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + + // Verify NOTFOUND is in the cache for "*". + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*) after first call") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error for List(*), got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected cached error type NOTFOUND for List(*), got %v", qErr.GetErrorType()) + } + + // Second List("*"): must hit cache (no second AggregatedList call). + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope: two List calls --- + // First List(scope): cache miss → per-zone List → List (0 items) → NOTFOUND cached. + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + + // Verify NOTFOUND is in the cache for scope. + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope) after first call") + } + if qErr == nil { + t.Fatal("expected cached NOTFOUND error for List(scope), got nil") + } + if qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected cached error type NOTFOUND for List(scope), got %v", qErr.GetErrorType()) + } + + // Second List(scope): must hit cache (no second List call). + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + + // We know it was cached: (1) cache.Lookup returned cacheHit true with NOTFOUND, and + // (2) ctrl.Finish() verifies AggregatedList and List were each called exactly once. + }) + t.Run("GetWithInitializeParams", func(t *testing.T) { wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index ae284df9..e5dc575f 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -125,6 +126,8 @@ func (c computeInstantSnapshotWrapper) ListStream(ctx context.Context, stream di Zone: location.Zone, }) + var itemsSent int + var hadError bool for { instantSnapshot, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -138,11 +141,24 @@ func (c computeInstantSnapshotWrapper) ListStream(ctx context.Context, stream di item, sdpErr := c.gcpComputeInstantSnapshotToSDPItem(ctx, instantSnapshot, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instant snapshots found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -153,6 +169,8 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -168,6 +186,7 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -188,11 +207,13 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, item, sdpErr := c.gcpComputeInstantSnapshotToSDPItem(ctx, instantSnapshot, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -203,6 +224,17 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute instant snapshots found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeInstantSnapshotWrapper) gcpComputeInstantSnapshotToSDPItem(ctx context.Context, instantSnapshot *computepb.InstantSnapshot, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index 55d33474..dffa8a9d 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -5,6 +5,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -238,6 +239,77 @@ func TestComputeInstantSnapshot(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeInstantSnapshotsClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockInstantSnapshotsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.InstantSnapshotsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeInstantSnapshotIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeInstantSnapshot(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeInstantSnapshot(snapshotName, zone string, status computepb.InstantSnapshot_Status) *computepb.InstantSnapshot { diff --git a/sources/gcp/manual/compute-machine-image.go b/sources/gcp/manual/compute-machine-image.go index 9b48c253..9ee354b7 100644 --- a/sources/gcp/manual/compute-machine-image.go +++ b/sources/gcp/manual/compute-machine-image.go @@ -120,6 +120,8 @@ func (c computeMachineImageWrapper) ListStream(ctx context.Context, stream disco Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { machineImage, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -133,11 +135,24 @@ func (c computeMachineImageWrapper) ListStream(ctx context.Context, stream disco item, sdpErr := c.gcpComputeMachineImageToSDPItem(ctx, machineImage, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute machine images found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index a465f2cd..9ceb43ee 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -328,6 +328,47 @@ func TestComputeMachineImage(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeMachineImageClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeMachineImageIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeMachineImage(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeMachineImage(imageName string, status computepb.MachineImage_Status) *computepb.MachineImage { diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index 4cbe396a..45c4bd50 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -141,6 +142,8 @@ func (c computeNodeGroupWrapper) ListStream(ctx context.Context, stream discover Zone: location.Zone, }) + var itemsSent int + var hadError bool for { nodeGroup, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -154,11 +157,24 @@ func (c computeNodeGroupWrapper) ListStream(ctx context.Context, stream discover item, sdpErr := c.gcpComputeNodeGroupToSDPItem(ctx, nodeGroup, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node groups found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -169,6 +185,8 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -184,6 +202,7 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -204,11 +223,13 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea item, sdpErr := c.gcpComputeNodeGroupToSDPItem(ctx, nodeGroup, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -219,6 +240,17 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node groups found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeNodeGroupWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index a8bb9987..191fd63d 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -6,6 +6,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -192,6 +193,77 @@ func TestComputeNodeGroup(t *testing.T) { } }) + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeGroupClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockNodeGroupsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.NodeGroupsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeNodeGroupIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) + t.Run("Search", func(t *testing.T) { wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -260,6 +332,51 @@ func TestComputeNodeGroup(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeGroupClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + query := "https://www.googleapis.com/compute/v1/projects/cache-test-project/zones/us-central1-a/nodeTemplates/nonexistent-template" + + mockIter := mocks.NewMockComputeNodeGroupIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("SearchStream", func(t *testing.T) { wrapper := manual.NewComputeNodeGroup(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index f8985a1e..46535cb6 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -125,6 +126,8 @@ func (c computeNodeTemplateWrapper) ListStream(ctx context.Context, stream disco Region: location.Region, }) + var itemsSent int + var hadError bool for { nodeTemplate, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -138,11 +141,24 @@ func (c computeNodeTemplateWrapper) ListStream(ctx context.Context, stream disco item, sdpErr := c.gcpComputeNodeTemplateToSDPItem(nodeTemplate, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node templates found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -153,6 +169,8 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -168,6 +186,7 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -188,11 +207,13 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st item, sdpErr := c.gcpComputeNodeTemplateToSDPItem(nodeTemplate, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -203,6 +224,17 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute node templates found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeNodeTemplateWrapper) gcpComputeNodeTemplateToSDPItem(nodeTemplate *computepb.NodeTemplate, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index 15f8ff72..c82a6464 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -5,6 +5,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -189,6 +190,77 @@ func TestComputeNodeTemplate(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeNodeTemplateClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + mockAggIter := mocks.NewMockNodeTemplatesScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.NodeTemplatesScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeNodeTemplateIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeNodeTemplate(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // Create an node template fixture (as returned from GCP API). diff --git a/sources/gcp/manual/compute-region-instance-group-manager.go b/sources/gcp/manual/compute-region-instance-group-manager.go index 1f9ded9d..cd87d6cf 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager.go +++ b/sources/gcp/manual/compute-region-instance-group-manager.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -137,6 +138,8 @@ func (c computeRegionInstanceGroupManagerWrapper) ListStream(ctx context.Context Region: location.Region, }) + var itemsSent int + var hadError bool for { igm, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -150,17 +153,32 @@ func (c computeRegionInstanceGroupManagerWrapper) ListStream(ctx context.Context item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute region instance group managers found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey) { // Use a pool to list across all regions in parallel p := pool.New().WithContext(ctx).WithMaxGoroutines(10) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, location := range c.Locations() { p.Go(func(ctx context.Context) error { @@ -176,17 +194,20 @@ func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx conte } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, location.ToScope(), c.Type())) + hadError.Store(true) return iterErr } item, sdpErr := c.gcpRegionInstanceGroupManagerToSDPItem(ctx, igm, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } return nil @@ -195,6 +216,17 @@ func (c computeRegionInstanceGroupManagerWrapper) listAllRegionsStream(ctx conte // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute region instance group managers found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeRegionInstanceGroupManagerWrapper) gcpRegionInstanceGroupManagerToSDPItem(ctx context.Context, instanceGroupManager *computepb.InstanceGroupManager, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go index 52e9e64a..ed5f4ef5 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -239,6 +239,77 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { t.Fatalf("Expected 2 items, got: %d", len(items)) } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockRegionInstanceGroupManagerClient(ctrl) + projectID := "cache-test-project" + region := "us-central1" + scope := projectID + "." + region + + // "*" path calls List once per region; specific scope calls List once. With 1 region: 2 List calls total. + mockIter1 := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockIter1.EXPECT().Next().Return(nil, iterator.Done) + mockIter2 := mocks.NewMockRegionInstanceGroupManagerIterator(ctrl) + mockIter2.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockIter1).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockIter2).Times(1) + + wrapper := manual.NewComputeRegionInstanceGroupManager(mockClient, []gcpshared.LocationInfo{gcpshared.NewRegionalLocation(projectID, region)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createRegionInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index e6d71c1b..e35bbe4d 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -3,6 +3,7 @@ package manual import ( "context" "errors" + "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" @@ -127,6 +128,8 @@ func (c computeReservationWrapper) ListStream(ctx context.Context, stream discov Zone: location.Zone, }) + var itemsSent int + var hadError bool for { reservation, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -140,11 +143,24 @@ func (c computeReservationWrapper) ListStream(ctx context.Context, stream discov item, sdpErr := c.gcpComputeReservationToSDPItem(ctx, reservation, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute reservations found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } @@ -155,6 +171,8 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str // Use a pool with 10x concurrency to parallelize AggregatedList calls p := pool.New().WithMaxGoroutines(10).WithContext(ctx) + var itemsSent atomic.Int32 + var hadError atomic.Bool for _, projectID := range projectIDs { p.Go(func(ctx context.Context) error { @@ -170,6 +188,7 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str } if iterErr != nil { stream.SendError(gcpshared.QueryError(iterErr, projectID, c.Type())) + hadError.Store(true) return iterErr } @@ -190,11 +209,13 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str item, sdpErr := c.gcpComputeReservationToSDPItem(ctx, reservation, scopeLocation) if sdpErr != nil { stream.SendError(sdpErr) + hadError.Store(true) continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent.Add(1) } } } @@ -205,6 +226,17 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str // Wait for all goroutines to complete _ = p.Wait() + if itemsSent.Load() == 0 && !hadError.Load() { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute reservations found in scope *", + Scope: "*", + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) + } } func (c computeReservationWrapper) gcpComputeReservationToSDPItem(ctx context.Context, reservation *computepb.Reservation, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 2af80217..0ede68be 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -5,6 +5,7 @@ import ( "sync" "testing" + compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" @@ -207,6 +208,77 @@ func TestComputeReservation(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeReservationClient(ctrl) + projectID := "cache-test-project" + zone := "us-central1-a" + scope := projectID + "." + zone + + mockAggIter := mocks.NewMockReservationsScopedListPairIterator(ctrl) + mockAggIter.EXPECT().Next().Return(compute.ReservationsScopedListPair{}, iterator.Done) + mockListIter := mocks.NewMockComputeReservationIterator(ctrl) + mockListIter.EXPECT().Next().Return(nil, iterator.Done) + + mockClient.EXPECT().AggregatedList(gomock.Any(), gomock.Any()).Return(mockAggIter).Times(1) + mockClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(mockListIter).Times(1) + + wrapper := manual.NewComputeReservation(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + // --- Scope "*" --- + items, err := listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("first List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(*): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, "*", discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(*)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(*), got %v", qErr) + } + items, err = listable.List(ctx, "*", false) + if err != nil { + t.Fatalf("second List(*): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(*): expected 0 items, got %d", len(items)) + } + + // --- Specific scope --- + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done = cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeReservation(reservationName string, status computepb.Reservation_Status) *computepb.Reservation { diff --git a/sources/gcp/manual/compute-security-policy.go b/sources/gcp/manual/compute-security-policy.go index c14718d6..1041704a 100644 --- a/sources/gcp/manual/compute-security-policy.go +++ b/sources/gcp/manual/compute-security-policy.go @@ -109,6 +109,8 @@ func (c computeSecurityPolicyWrapper) ListStream(ctx context.Context, stream dis Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { securityPolicy, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -122,10 +124,23 @@ func (c computeSecurityPolicyWrapper) ListStream(ctx context.Context, stream dis item, sdpErr := c.gcpComputeSecurityPolicyToSDPItem(securityPolicy, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute security policies found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index fe167514..7f3c3127 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -154,6 +154,47 @@ func TestComputeSecurityPolicy(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeSecurityPolicyClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeSecurityPolicyIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeSecurityPolicy(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeSecurityPolicy(policyName string) *computepb.SecurityPolicy { diff --git a/sources/gcp/manual/compute-snapshot.go b/sources/gcp/manual/compute-snapshot.go index c6561b37..f4112374 100644 --- a/sources/gcp/manual/compute-snapshot.go +++ b/sources/gcp/manual/compute-snapshot.go @@ -112,6 +112,8 @@ func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery Project: location.ProjectID, }) + var itemsSent int + var hadError bool for { snapshot, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -125,11 +127,24 @@ func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery item, sdpErr := c.gcpComputeSnapshotToSDPItem(ctx, snapshot, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no compute snapshots found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 099afe76..87520cdf 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -250,6 +250,47 @@ func TestComputeSnapshot(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockComputeSnapshotsClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockComputeSnapshotIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewComputeSnapshot(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createComputeSnapshot(snapshotName string, status computepb.Snapshot_Status) *computepb.Snapshot { diff --git a/sources/gcp/manual/iam-service-account-key_test.go b/sources/gcp/manual/iam-service-account-key_test.go index 82436a21..7ee7be17 100644 --- a/sources/gcp/manual/iam-service-account-key_test.go +++ b/sources/gcp/manual/iam-service-account-key_test.go @@ -91,6 +91,48 @@ func TestIAMServiceAccountKey(t *testing.T) { } }) + t.Run("SearchCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockIAMServiceAccountKeyClient(ctrl) + projectID := "cache-test-project" + scope := projectID + query := "nonexistent-sa@cache-test-project.iam.gserviceaccount.com" + + mockClient.EXPECT().Search(ctx, gomock.Any()).Return(&adminpb.ListServiceAccountKeysResponse{Keys: nil}, nil).Times(1) + + wrapper := manual.NewIAMServiceAccountKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + searchable := adapter.(discovery.SearchableAdapter) + + items, err := searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_SEARCH, scope, discAdapter.Type(), query, false) + done() + if !cacheHit { + t.Fatal("expected cache hit for Search after first call") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for Search, got %v", qErr) + } + + items, err = searchable.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error: %v", err) + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + }) + t.Run("SearchWithTerraformQueryMap", func(t *testing.T) { wrapper := manual.NewIAMServiceAccountKey(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) diff --git a/sources/gcp/manual/iam-service-account.go b/sources/gcp/manual/iam-service-account.go index a567a723..8a1fd392 100644 --- a/sources/gcp/manual/iam-service-account.go +++ b/sources/gcp/manual/iam-service-account.go @@ -127,6 +127,8 @@ func (c iamServiceAccountWrapper) ListStream(ctx context.Context, stream discove results := c.client.List(ctx, req) + var itemsSent int + var hadError bool for { sa, iterErr := results.Next() if errors.Is(iterErr, iterator.Done) { @@ -140,11 +142,24 @@ func (c iamServiceAccountWrapper) ListStream(ctx context.Context, stream discove item, sdpErr := c.gcpIAMServiceAccountToSDPItem(sa, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no IAM service accounts found in scope " + scope, + Scope: scope, + SourceName: c.Name(), + ItemType: c.Type(), + ResponderName: c.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/iam-service-account_test.go b/sources/gcp/manual/iam-service-account_test.go index 9b89cf24..48cdac1c 100644 --- a/sources/gcp/manual/iam-service-account_test.go +++ b/sources/gcp/manual/iam-service-account_test.go @@ -181,6 +181,47 @@ func TestIAMServiceAccount(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockIAMServiceAccountClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockIAMServiceAccountIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().List(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewIAMServiceAccount(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } // createServiceAccount creates a ServiceAccount with the specified fields. diff --git a/sources/gcp/manual/logging-sink.go b/sources/gcp/manual/logging-sink.go index 2ffb53b8..000cb827 100644 --- a/sources/gcp/manual/logging-sink.go +++ b/sources/gcp/manual/logging-sink.go @@ -107,6 +107,8 @@ func (l loggingSinkWrapper) ListStream(ctx context.Context, stream discovery.Que Parent: fmt.Sprintf("projects/%s", location.ProjectID), }) + var itemsSent int + var hadError bool for { sink, iterErr := it.Next() if errors.Is(iterErr, iterator.Done) { @@ -120,11 +122,24 @@ func (l loggingSinkWrapper) ListStream(ctx context.Context, stream discovery.Que item, sdpErr := l.gcpLoggingSinkToItem(sink, location) if sdpErr != nil { stream.SendError(sdpErr) + hadError = true continue } cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) + itemsSent++ + } + if itemsSent == 0 && !hadError { + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no logging sinks found in scope " + scope, + Scope: scope, + SourceName: l.Name(), + ItemType: l.Type(), + ResponderName: l.Name(), + } + cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, cacheKey) } } diff --git a/sources/gcp/manual/logging-sink_test.go b/sources/gcp/manual/logging-sink_test.go index ed908658..1a43f578 100644 --- a/sources/gcp/manual/logging-sink_test.go +++ b/sources/gcp/manual/logging-sink_test.go @@ -235,6 +235,47 @@ func TestNewLoggingSink(t *testing.T) { t.Fatalf("Adapter should not support SearchStream operation") } }) + + t.Run("ListCachesNotFoundWithMemoryCache", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockClient := mocks.NewMockLoggingConfigClient(ctrl) + projectID := "cache-test-project" + scope := projectID + + mockIter := mocks.NewMockLoggingSinkIterator(ctrl) + mockIter.EXPECT().Next().Return(nil, iterator.Done) + mockClient.EXPECT().ListSinks(ctx, gomock.Any()).Return(mockIter).Times(1) + + wrapper := manual.NewLoggingSink(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + cache := sdpcache.NewMemoryCache() + adapter := sources.WrapperToAdapter(wrapper, cache) + discAdapter := adapter.(discovery.Adapter) + listable := adapter.(discovery.ListableAdapter) + + items, err := listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("first List(scope): expected 0 items, got %d", len(items)) + } + cacheHit, _, _, qErr, done := cache.Lookup(ctx, discAdapter.Name(), sdp.QueryMethod_LIST, scope, discAdapter.Type(), "", false) + done() + if !cacheHit { + t.Fatal("expected cache hit for List(scope)") + } + if qErr == nil || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Fatalf("expected cached NOTFOUND for List(scope), got %v", qErr) + } + items, err = listable.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List(scope): %v", err) + } + if len(items) != 0 { + t.Errorf("second List(scope): expected 0 items, got %d", len(items)) + } + }) } func createLoggingSink(name, destination, writerIdentity string) *loggingpb.LogSink { diff --git a/sources/transformer.go b/sources/transformer.go index 76a2cfed..9ad83b70 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -2,6 +2,7 @@ package sources import ( "context" + "errors" "fmt" "strings" @@ -252,6 +253,20 @@ func (s *standardAdapterCore) validateScopes(scope string) error { } } +// NOTFOUND caching contract (applies to all adapters using this transformer, including manual adapters): +// we only cache when the result is "not found" (not timeouts or other errors). When a second call hits +// the cache, we return the same response and error as a fresh not-found call (e.g. Get: nil item + same +// error message; List/Search: empty slice + nil error). No behavior change. +// +// IsNotFound returns true if err is a QueryError with ErrorType NOTFOUND. +func IsNotFound(err error) bool { + var qe *sdp.QueryError + if errors.As(err, &qe) { + return qe.GetErrorType() == sdp.QueryError_NOTFOUND + } + return false +} + // Get retrieves a single item with a given scope and query. func (s *standardAdapterCore) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { if err := s.validateScopes(scope); err != nil { @@ -270,13 +285,18 @@ func (s *standardAdapterCore) Get(ctx context.Context, scope string, query strin defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into nil result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return nil, qErr + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_GET.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit && len(cachedItem) > 0 { @@ -294,10 +314,27 @@ func (s *standardAdapterCore) Get(ctx context.Context, scope string, query strin item, err := s.wrapper.Get(ctx, scope, queryParts...) if err != nil { - s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if item == nil { + // Cache not-found when item is nil + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("%s not found for query '%s'", s.Type(), query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return nil, notFoundErr + } + // Store in cache after successful get s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) return item, nil @@ -381,13 +418,18 @@ func (s *standardListableAdapterImpl) List(ctx context.Context, scope string, ig defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + return nil, qErr } if cacheHit { @@ -396,10 +438,27 @@ func (s *standardListableAdapterImpl) List(ctx context.Context, scope string, ig items, err := s.listable.List(ctx, scope) if err != nil { - s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found in scope %s", s.Type(), scope), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + for _, item := range items { s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) } @@ -430,13 +489,19 @@ func (s *standardListableAdapterImpl) ListStream(ctx context.Context, scope stri defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_LIST.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { @@ -613,11 +678,64 @@ func (s *standardSearchableAdapterImpl) Search(ctx context.Context, scope string ) } + // Check cache before searching + cacheHit, ck, cachedItems, qErr, done := s.cache.Lookup( + ctx, + s.Name(), + sdp.QueryMethod_SEARCH, + scope, + s.Type(), + query, + ignoreCache, + ) + defer done() + + if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return []*sdp.Item{}, nil + } + log.WithContext(ctx).WithFields(log.Fields{ + "ovm.source.type": s.sourceType, + "ovm.source.adapter": s.Name(), + "ovm.source.scope": scope, + "ovm.source.method": sdp.QueryMethod_SEARCH.String(), + "ovm.source.cache-key": ck, + }).WithError(qErr).Info("returning cached query error") + return nil, qErr + } + + if cacheHit { + return cachedItems, nil + } + items, err := s.searchable.Search(ctx, scope, queryParts...) if err != nil { + // Only cache NOTFOUND so lookup behaviour is unchanged for timeouts/other errors + if IsNotFound(err) { + s.cache.StoreError(ctx, err, shared.DefaultCacheDuration, ck) + } return nil, err } + if len(items) == 0 { + // Cache not-found when no items were found + notFoundErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("no %s found for search query '%s'", s.Type(), query), + Scope: scope, + SourceName: s.Name(), + ItemType: s.Type(), + ResponderName: s.Name(), + } + s.cache.StoreError(ctx, notFoundErr, shared.DefaultCacheDuration, ck) + return items, nil + } + + for _, item := range items { + s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) + } + return items, nil } @@ -639,13 +757,19 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope defer done() if qErr != nil { + // For better semantics, convert cached NOTFOUND into empty result + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + return + } log.WithContext(ctx).WithFields(log.Fields{ "ovm.source.type": s.sourceType, "ovm.source.adapter": s.Name(), "ovm.source.scope": scope, "ovm.source.method": sdp.QueryMethod_SEARCH.String(), "ovm.source.cache-key": ck, - }).WithError(qErr).Error("failed to lookup item in cache") + }).WithError(qErr).Info("returning cached query error") + stream.SendError(qErr) + return } if cacheHit { diff --git a/sources/transformer_test.go b/sources/transformer_test.go index 00a9d0c6..87dbd4c6 100644 --- a/sources/transformer_test.go +++ b/sources/transformer_test.go @@ -2,6 +2,7 @@ package sources import ( "context" + "errors" "sync" "sync/atomic" "testing" @@ -206,10 +207,12 @@ func TestListErrorCausesCacheHang(t *testing.T) { t.Logf(" List() called %d times", mockWrapper.callCount.Load()) } - // List() is called twice - once by first, once by second after being woken + // We only cache NOTFOUND; this wrapper returns QueryError_OTHER so the error is not cached. + // Both goroutines call List() (callCount == 2). The important assertion is timing above: + // second goroutine completes quickly because done() wakes it, then it retries and gets the same error. callCount := mockWrapper.callCount.Load() if callCount != 2 { - t.Errorf("Expected List to be called twice, was called %d times", callCount) + t.Errorf("Expected List to be called twice (error is not cached), was called %d times", callCount) } t.Logf("Test results:") @@ -217,3 +220,293 @@ func TestListErrorCausesCacheHang(t *testing.T) { t.Logf(" Second goroutine: %v", secondDuration) t.Logf(" List() calls: %d", callCount) } + +// notFoundCachingWrapper returns nil/empty from Get/List/Search to test NOTFOUND caching. +type notFoundCachingWrapper struct { + getCallCount atomic.Int32 + listCallCount atomic.Int32 + searchCallCount atomic.Int32 + itemType shared.ItemType + scope string +} + +func (w *notFoundCachingWrapper) Scopes() []string { + return []string{w.scope} +} + +func (w *notFoundCachingWrapper) GetLookups() ItemTypeLookups { + return ItemTypeLookups{shared.NewItemTypeLookup("id", w.itemType)} +} + +func (w *notFoundCachingWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + w.getCallCount.Add(1) + return nil, nil +} + +func (w *notFoundCachingWrapper) Type() string { + return w.itemType.String() +} + +func (w *notFoundCachingWrapper) Name() string { + return "notfound-caching-adapter" +} + +func (w *notFoundCachingWrapper) ItemType() shared.ItemType { + return w.itemType +} + +func (w *notFoundCachingWrapper) TerraformMappings() []*sdp.TerraformMapping { + return nil +} + +func (w *notFoundCachingWrapper) Category() sdp.AdapterCategory { + return sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION +} + +func (w *notFoundCachingWrapper) PotentialLinks() map[shared.ItemType]bool { + return nil +} + +func (w *notFoundCachingWrapper) AdapterMetadata() *sdp.AdapterMetadata { + return nil +} + +func (w *notFoundCachingWrapper) IAMPermissions() []string { + return nil +} + +func (w *notFoundCachingWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + w.listCallCount.Add(1) + return []*sdp.Item{}, nil +} + +func (w *notFoundCachingWrapper) SearchLookups() []ItemTypeLookups { + return []ItemTypeLookups{{shared.NewItemTypeLookup("id", w.itemType)}} +} + +func (w *notFoundCachingWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + w.searchCallCount.Add(1) + return []*sdp.Item{}, nil +} + +// TestGetNilCachesNotFound tests that when wrapper Get returns (nil, nil), the adapter +// caches NOTFOUND and a second Get returns the cached error without calling the wrapper again. +func TestGetNilCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache) + + // First Get: miss, wrapper returns (nil, nil), adapter caches NOTFOUND + item, err := adapter.Get(ctx, scope, "query1", false) + if item != nil { + t.Errorf("first Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("first Get: expected NOTFOUND error, got nil") + } + var qErr *sdp.QueryError + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("first Get: expected NOTFOUND, got %v", err) + } + if wrapper.getCallCount.Load() != 1 { + t.Errorf("first Get: expected 1 Get call, got %d", wrapper.getCallCount.Load()) + } + + // Second Get: should hit cache, wrapper not called again + item, err = adapter.Get(ctx, scope, "query1", false) + if item != nil { + t.Errorf("second Get: expected nil item, got %v", item) + } + if err == nil { + t.Fatal("second Get: expected NOTFOUND error, got nil") + } + if !errors.As(err, &qErr) || qErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("second Get: expected NOTFOUND, got %v", err) + } + if wrapper.getCallCount.Load() != 1 { + t.Errorf("second Get: expected still 1 Get call (cache hit), got %d", wrapper.getCallCount.Load()) + } +} + +// TestListEmptyCachesNotFound tests that when wrapper List returns ([], nil), the adapter +// caches NOTFOUND and a second List returns empty from cache without calling the wrapper again. +func TestListEmptyCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + List(context.Context, string, bool) ([]*sdp.Item, error) + }) + + // First List: miss, wrapper returns ([], nil), adapter caches NOTFOUND + items, err := adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("first List: unexpected error %v", err) + } + if items == nil { + t.Error("first List: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("first List: expected 0 items, got %d", len(items)) + } + if wrapper.listCallCount.Load() != 1 { + t.Errorf("first List: expected 1 List call, got %d", wrapper.listCallCount.Load()) + } + + // Second List: should hit cache, wrapper not called again + items, err = adapter.List(ctx, scope, false) + if err != nil { + t.Fatalf("second List: unexpected error %v", err) + } + if items == nil { + t.Error("second List: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("second List: expected 0 items, got %d", len(items)) + } + if wrapper.listCallCount.Load() != 1 { + t.Errorf("second List: expected still 1 List call (cache hit), got %d", wrapper.listCallCount.Load()) + } +} + +// TestSearchEmptyCachesNotFound tests that when wrapper Search returns ([], nil), the adapter +// caches NOTFOUND and a second Search returns empty from cache without calling the wrapper again. +func TestSearchEmptyCachesNotFound(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + // Use AWS item type so adapter validation does not require GCP predefined role. + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + Search(context.Context, string, string, bool) ([]*sdp.Item, error) + }) + + query := "id1" + + // First Search: miss, wrapper returns ([], nil), adapter caches NOTFOUND + items, err := adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("first Search: unexpected error %v", err) + } + if items == nil { + t.Error("first Search: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("first Search: expected 0 items, got %d", len(items)) + } + if wrapper.searchCallCount.Load() != 1 { + t.Errorf("first Search: expected 1 Search call, got %d", wrapper.searchCallCount.Load()) + } + + // Second Search: should hit cache, wrapper not called again + items, err = adapter.Search(ctx, scope, query, false) + if err != nil { + t.Fatalf("second Search: unexpected error %v", err) + } + if items == nil { + t.Error("second Search: expected non-nil empty slice, got nil") + } + if len(items) != 0 { + t.Errorf("second Search: expected 0 items, got %d", len(items)) + } + if wrapper.searchCallCount.Load() != 1 { + t.Errorf("second Search: expected still 1 Search call (cache hit), got %d", wrapper.searchCallCount.Load()) + } +} + +// TestGetNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit returns +// the same (item, error) as a fresh NOTFOUND — nil item and identical error type and error message. +func TestGetNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache) + + query := "query1" + // Live NOTFOUND + liveItem, liveErr := adapter.Get(ctx, scope, query, false) + // Cache NOTFOUND (second call hits cache) + cacheItem, cacheErr := adapter.Get(ctx, scope, query, false) + + // Same item: both nil + if liveItem != nil || cacheItem != nil { + t.Errorf("both responses must have nil item: live=%v cache=%v", liveItem, cacheItem) + } + // Same error semantics: both NOTFOUND with same message + var liveQE, cacheQE *sdp.QueryError + if !errors.As(liveErr, &liveQE) || !errors.As(cacheErr, &cacheQE) { + t.Fatalf("both errors must be QueryError: live=%v cache=%v", liveErr, cacheErr) + } + if liveQE.GetErrorType() != sdp.QueryError_NOTFOUND || cacheQE.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("both must be NOTFOUND: live=%v cache=%v", liveQE.GetErrorType(), cacheQE.GetErrorType()) + } + if liveQE.GetErrorString() != cacheQE.GetErrorString() { + t.Errorf("error string must match: live=%q cache=%q", liveQE.GetErrorString(), cacheQE.GetErrorString()) + } +} + +// TestListNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit for List +// returns the same (items, error) as a fresh not-found — empty slice and nil error. +func TestListNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + List(context.Context, string, bool) ([]*sdp.Item, error) + }) + + liveItems, liveErr := adapter.List(ctx, scope, false) + cacheItems, cacheErr := adapter.List(ctx, scope, false) + + if liveErr != nil || cacheErr != nil { + t.Errorf("both must return nil error: live=%v cache=%v", liveErr, cacheErr) + } + if liveItems == nil || cacheItems == nil { + t.Errorf("both must return non-nil slice: live=%v cache=%v", liveItems, cacheItems) + } + if len(liveItems) != 0 || len(cacheItems) != 0 { + t.Errorf("both must return empty slice: live len=%d cache len=%d", len(liveItems), len(cacheItems)) + } +} + +// TestSearchNOTFOUNDCacheHitMatchesLiveNOTFOUND asserts response parity: a NOTFOUND cache hit for Search +// returns the same (items, error) as a fresh not-found — empty slice and nil error. +func TestSearchNOTFOUNDCacheHitMatchesLiveNOTFOUND(t *testing.T) { + ctx := context.Background() + cache := sdpcache.NewMemoryCache() + scope := "test-scope" + itemType := shared.NewItemType(aws.AWS, aws.APIGateway, aws.RESTAPI) + wrapper := ¬FoundCachingWrapper{itemType: itemType, scope: scope} + adapter := WrapperToAdapter(wrapper, cache).(interface { + Search(context.Context, string, string, bool) ([]*sdp.Item, error) + }) + + query := "id1" + liveItems, liveErr := adapter.Search(ctx, scope, query, false) + cacheItems, cacheErr := adapter.Search(ctx, scope, query, false) + + if liveErr != nil || cacheErr != nil { + t.Errorf("both must return nil error: live=%v cache=%v", liveErr, cacheErr) + } + if liveItems == nil || cacheItems == nil { + t.Errorf("both must return non-nil slice: live=%v cache=%v", liveItems, cacheItems) + } + if len(liveItems) != 0 || len(cacheItems) != 0 { + t.Errorf("both must return empty slice: live len=%d cache len=%d", len(liveItems), len(cacheItems)) + } +} From 3245e602c400c04e33bffe9ec6615e37ef627357 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:51:46 +0000 Subject: [PATCH 21/51] review azure adapter terraform mappings (#3852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …r role assignments, storage blob containers, and file shares. --- > [!NOTE] > **Medium Risk** > Changes how Azure resources are mapped/resolved from Terraform plans (GET vs SEARCH and name vs id), which may affect plan-to-infra matching correctness across Azure types. > > **Overview** > Extends Terraform plan mapping to include Azure adapter metadata so Azure resources can be resolved into Overmind queries during `submit-plan`. > > Updates Azure Terraform mappings for `azurerm_role_assignment`, `azurerm_storage_container`, and `azurerm_storage_share` to use `QueryMethod_SEARCH` against the Terraform `id` field (resource ID-based resolution), adding a `Search`/`SearchLookups` implementation plus tests for role assignments. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a921860a0774e8cd2033069ae3cbc4a48085d69e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 6d4d71c65c666f9b132e4f3be954a8ddd87e5cfa --- .../manual/authorization-role-assignment.go | 32 +++++++- .../authorization-role-assignment_test.go | 74 ++++++++++++++++++- .../azure/manual/storage-blob-container.go | 5 +- sources/azure/manual/storage-fileshare.go | 5 +- tfutils/plan_mapper.go | 6 +- 5 files changed, 113 insertions(+), 9 deletions(-) diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index 14cb7f8f..a20feaf1 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -227,11 +227,41 @@ func (a authorizationRoleAssignmentWrapper) GetLookups() sources.ItemTypeLookups } } +// SearchLookups defines how the source can be searched (e.g. by role assignment name within a scope). +// Used when TerraformMethod is SEARCH (azurerm_role_assignment.id). +func (a authorizationRoleAssignmentWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + AuthorizationRoleAssignmentLookupByName, + }, + } +} + +// Search resolves a role assignment by name within the given scope. +// Supports Terraform SEARCH resolution when the query is the role assignment name (or extracted from Azure resource ID by the transformer). +func (a authorizationRoleAssignmentWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("Search requires 1 query part: roleAssignmentName"), scope, a.Type()) + } + roleAssignmentName := queryParts[0] + if roleAssignmentName == "" { + return nil, azureshared.QueryError(errors.New("roleAssignmentName cannot be empty"), scope, a.Type()) + } + item, qErr := a.Get(ctx, scope, roleAssignmentName) + if qErr != nil { + return nil, qErr + } + return []*sdp.Item{item}, nil +} + // ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment func (a authorizationRoleAssignmentWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, + // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} + // Or: /subscriptions/{sub}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} TerraformQueryMap: "azurerm_role_assignment.id", }, } diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index 7071e122..12557f58 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -335,6 +335,76 @@ func TestAuthorizationRoleAssignment(t *testing.T) { } }) + t.Run("Search", func(t *testing.T) { + roleAssignmentName := "test-role-assignment" + roleAssignment := createAzureRoleAssignment(roleAssignmentName, "/subscriptions/test-subscription/resourceGroups/test-rg") + + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + azureScope := "/subscriptions/test-subscription/resourceGroups/test-rg" + mockClient.EXPECT().Get(ctx, azureScope, roleAssignmentName, nil).Return( + armauthorization.RoleAssignmentsClientGetResponse{ + RoleAssignment: *roleAssignment, + }, nil) + + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not implement SearchableAdapter") + } + + items, err := searchable.Search(ctx, scope, roleAssignmentName, true) + if err != nil { + t.Fatalf("Search failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item, got %d", len(items)) + } + if len(items) > 0 && items[0].GetType() != azureshared.AuthorizationRoleAssignment.String() { + t.Errorf("Expected type %s, got %s", azureshared.AuthorizationRoleAssignment.String(), items[0].GetType()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + _, qErr := searchableWrapper.Search(ctx, scope, "name1", "name2") + if qErr == nil { + t.Error("Expected error for too many query parts, got nil") + } + }) + + t.Run("Search_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + _, qErr := searchableWrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error for empty role assignment name, got nil") + } + }) + + t.Run("SearchLookups", func(t *testing.T) { + mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) + wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + searchableWrapper := wrapper.(sources.SearchableWrapper) + + searchLookups := searchableWrapper.SearchLookups() + if len(searchLookups) != 1 { + t.Errorf("Expected 1 search lookup group, got %d", len(searchLookups)) + } + if len(searchLookups) > 0 && len(searchLookups[0]) != 1 { + t.Errorf("Expected 1 lookup in first group, got %d", len(searchLookups[0])) + } + if len(searchLookups) > 0 && len(searchLookups[0]) > 0 && searchLookups[0][0].ItemType != azureshared.AuthorizationRoleAssignment { + t.Errorf("Expected SearchLookups to include AuthorizationRoleAssignment, got %v", searchLookups[0][0].ItemType) + } + }) + t.Run("TerraformMappings", func(t *testing.T) { mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -348,8 +418,8 @@ func TestAuthorizationRoleAssignment(t *testing.T) { for _, mapping := range mappings { if mapping.GetTerraformQueryMap() == "azurerm_role_assignment.id" { foundMapping = true - if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { - t.Errorf("Expected TerraformMethod to be GET, got: %v", mapping.GetTerraformMethod()) + if mapping.GetTerraformMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Expected TerraformMethod to be SEARCH, got: %v", mapping.GetTerraformMethod()) } break } diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index c20f97f3..298e4111 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -221,9 +221,10 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms func (s storageBlobContainerWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container - TerraformQueryMap: "azurerm_storage_container.name", + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/blobServices/default/containers/{container} + TerraformQueryMap: "azurerm_storage_container.id", }, } } diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index 59937452..1748cc35 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -168,9 +168,10 @@ func (s storageFileShareWrapper) azureFileShareToSDPItem(fileShare *armstorage.F func (s storageFileShareWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { - TerraformMethod: sdp.QueryMethod_GET, + TerraformMethod: sdp.QueryMethod_SEARCH, // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_share - TerraformQueryMap: "azurerm_storage_share.name", + // Terraform uses: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/fileServices/default/shares/{share} + TerraformQueryMap: "azurerm_storage_share.id", }, } } diff --git a/tfutils/plan_mapper.go b/tfutils/plan_mapper.go index da444e53..43f81b81 100644 --- a/tfutils/plan_mapper.go +++ b/tfutils/plan_mapper.go @@ -15,6 +15,7 @@ import ( k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" "github.com/overmindtech/workspace/sdp-go" gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" + azureAdapters "github.com/overmindtech/cli/sources/azure/proc" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -163,6 +164,7 @@ func MappedItemDiffsFromPlan(ctx context.Context, planJson []byte, fileName stri adapterMetadata := awsAdapters.Metadata.AllAdapterMetadata() adapterMetadata = append(adapterMetadata, k8sAdapters.Metadata.AllAdapterMetadata()...) adapterMetadata = append(adapterMetadata, gcpAdapters.Metadata.AllAdapterMetadata()...) + adapterMetadata = append(adapterMetadata, azureAdapters.Metadata.AllAdapterMetadata()...) // These mappings are from the terraform type, to required mapping data mappings := make(map[string][]TfMapData) for _, metadata := range adapterMetadata { @@ -342,7 +344,7 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map // If we get to this point, we haven't found a mapping message := fmt.Sprintf("missing mapping attribute: %v", strings.Join(attemptedMappings, ", ")) - + // Check if this is a newly created resource - these don't exist yet so missing // attributes are expected, not an error if itemDiff.GetStatus() == sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED { @@ -360,7 +362,7 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map }, } } - + // For other statuses (REPLACED, UPDATED, DELETED), missing attributes are a real error mappingStatus := sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR return PlannedChangeMapResult{ From 96fe72c28d0d8aa1d952200a8f40f2f17c2534ae Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Mon, 16 Feb 2026 13:32:43 +0100 Subject: [PATCH 22/51] [GCP shared + dynamic] Remove blast propagation; rename to link rules (#3874) > [!NOTE] > **Medium Risk** > Broad rename across many adapters and shared linker plumbing; behavior should be equivalent but mistakes could silently break auto-link generation and `PotentialLinks` metadata. > > **Overview** > Renames the GCP dynamic linking configuration concept from **blast propagation** to **link rules**, updating adapter registration (`registerableAdapter`), shared globals (`gcpshared.LinkRules`), and linker lookup logic to use the new map. > > Updates dynamic adapter metadata generation to derive `PotentialLinks` from link rules (including the IP/DNS bidirectional special-case), and refreshes adapter tests and internal docs to reference and validate link rules instead of blast propagation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 71974dbe7166ac40b4289e0ec21dff144b9875c7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: d231a00a88a4994d2f6f7408d2472539979098d9 --- sources/gcp/dynamic/adapter-listable.go | 2 +- .../dynamic/adapter-searchable-listable.go | 2 +- sources/gcp/dynamic/adapter-searchable.go | 2 +- sources/gcp/dynamic/adapter.go | 2 +- .../rules/dynamic-adapter-creation.mdc | 28 +++++++++---------- .../.cursor/rules/dynamic-adapter-testing.mdc | 6 ++-- .../ai-platform-batch-prediction-job.go | 2 +- .../ai-platform-batch-prediction-job_test.go | 2 +- .../adapters/ai-platform-custom-job.go | 2 +- .../dynamic/adapters/ai-platform-endpoint.go | 2 +- .../adapters/ai-platform-endpoint_test.go | 2 +- ...latform-model-deployment-monitoring-job.go | 2 +- ...rm-model-deployment-monitoring-job_test.go | 2 +- .../gcp/dynamic/adapters/ai-platform-model.go | 2 +- .../adapters/ai-platform-model_test.go | 2 +- .../adapters/ai-platform-pipeline-job.go | 2 +- .../artifact-registry-docker-image.go | 2 +- .../adapters/artifact-registry-repository.go | 2 +- ...big-query-data-transfer-transfer-config.go | 2 +- ...uery-data-transfer-transfer-config_test.go | 6 ++-- .../adapters/big-table-admin-app-profile.go | 2 +- .../adapters/big-table-admin-backup.go | 2 +- .../adapters/big-table-admin-cluster.go | 2 +- .../adapters/big-table-admin-instance.go | 2 +- .../dynamic/adapters/big-table-admin-table.go | 2 +- .../adapters/cloud-billing-billing-info.go | 2 +- .../gcp/dynamic/adapters/cloud-build-build.go | 2 +- .../cloud-resource-manager-project.go | 2 +- .../cloud-resource-manager-tag-key.go | 4 +-- .../cloud-resource-manager-tag-key_test.go | 4 +-- .../cloud-resource-manager-tag-value.go | 2 +- .../adapters/cloudfunctions-function.go | 2 +- .../adapters/compute-accelerator-type.go | 2 +- .../gcp/dynamic/adapters/compute-disk-type.go | 2 +- .../adapters/compute-external-vpn-gateway.go | 2 +- .../gcp/dynamic/adapters/compute-firewall.go | 2 +- .../adapters/compute-global-address.go | 2 +- .../compute-global-forwarding-rule.go | 2 +- .../adapters/compute-http-health-check.go | 2 +- .../compute-http-health-check_test.go | 6 ++-- .../adapters/compute-instance-template.go | 2 +- .../gcp/dynamic/adapters/compute-license.go | 2 +- .../compute-network-endpoint-group.go | 2 +- .../gcp/dynamic/adapters/compute-network.go | 2 +- .../gcp/dynamic/adapters/compute-project.go | 2 +- .../compute-public-delegated-prefix.go | 2 +- .../adapters/compute-region-commitment.go | 2 +- .../adapters/compute-resource-policy.go | 2 +- sources/gcp/dynamic/adapters/compute-route.go | 2 +- .../gcp/dynamic/adapters/compute-router.go | 4 +-- .../dynamic/adapters/compute-router_test.go | 2 +- .../adapters/compute-ssl-certificate.go | 4 +-- .../dynamic/adapters/compute-ssl-policy.go | 2 +- .../adapters/compute-ssl-policy_test.go | 2 +- .../dynamic/adapters/compute-storage-pool.go | 2 +- .../dynamic/adapters/compute-subnetwork.go | 2 +- .../adapters/compute-target-http-proxy.go | 2 +- .../adapters/compute-target-https-proxy.go | 2 +- .../dynamic/adapters/compute-target-pool.go | 2 +- .../gcp/dynamic/adapters/compute-url-map.go | 2 +- .../dynamic/adapters/compute-vpn-gateway.go | 2 +- .../dynamic/adapters/compute-vpn-tunnel.go | 2 +- .../gcp/dynamic/adapters/container-cluster.go | 2 +- .../adapters/container-cluster_test.go | 2 +- .../dynamic/adapters/container-node-pool.go | 2 +- .../dynamic/adapters/dataform-repository.go | 2 +- .../dynamic/adapters/dataplex-aspect-type.go | 2 +- .../adapters/dataplex-aspect-type_test.go | 4 +-- .../dynamic/adapters/dataplex-data-scan.go | 2 +- .../dynamic/adapters/dataplex-entry-group.go | 2 +- .../adapters/dataproc-auto-scaling-policy.go | 2 +- .../dataproc-auto-scaling-policy_test.go | 2 +- .../gcp/dynamic/adapters/dataproc-cluster.go | 2 +- .../gcp/dynamic/adapters/dns-managed-zone.go | 2 +- .../adapters/essential-contacts-contact.go | 2 +- .../gcp/dynamic/adapters/eventarc-trigger.go | 2 +- sources/gcp/dynamic/adapters/file-instance.go | 2 +- sources/gcp/dynamic/adapters/iam-role.go | 2 +- .../gcp/dynamic/adapters/logging-bucket.go | 2 +- sources/gcp/dynamic/adapters/logging-link.go | 2 +- .../dynamic/adapters/logging-saved-query.go | 2 +- sources/gcp/dynamic/adapters/models.go | 4 +-- .../adapters/monitoring-alert-policy.go | 2 +- .../adapters/monitoring-alert-policy_test.go | 2 +- .../adapters/monitoring-custom-dashboard.go | 2 +- .../monitoring-notification-channel.go | 2 +- .../monitoring-notification-channel_test.go | 2 +- .../gcp/dynamic/adapters/orgpolicy-policy.go | 2 +- .../dynamic/adapters/orgpolicy-policy_test.go | 2 +- .../dynamic/adapters/pubsub-subscription.go | 2 +- sources/gcp/dynamic/adapters/pubsub-topic.go | 2 +- .../gcp/dynamic/adapters/redis-instance.go | 2 +- .../dynamic/adapters/redis-instance_test.go | 2 +- sources/gcp/dynamic/adapters/run-revision.go | 2 +- sources/gcp/dynamic/adapters/run-service.go | 2 +- .../gcp/dynamic/adapters/run-worker-pool.go | 2 +- .../dynamic/adapters/secret-manager-secret.go | 2 +- ...nter-management-security-center-service.go | 2 +- .../adapters/service-directory-endpoint.go | 2 +- .../adapters/service-directory-service.go | 2 +- .../dynamic/adapters/service-usage-service.go | 2 +- .../gcp/dynamic/adapters/spanner-backup.go | 2 +- .../gcp/dynamic/adapters/spanner-database.go | 2 +- .../adapters/spanner-instance-config.go | 2 +- .../gcp/dynamic/adapters/spanner-instance.go | 2 +- .../dynamic/adapters/sql-admin-backup-run.go | 2 +- .../gcp/dynamic/adapters/sql-admin-backup.go | 2 +- .../dynamic/adapters/sql-admin-backup_test.go | 2 +- .../dynamic/adapters/sql-admin-instance.go | 2 +- .../adapters/sql-admin-instance_test.go | 2 +- .../gcp/dynamic/adapters/storage-bucket.go | 2 +- .../adapters/storage-transfer-transfer-job.go | 2 +- sources/gcp/dynamic/shared.go | 4 +-- .../{blast-propagations.go => link-rules.go} | 6 ++-- sources/gcp/shared/linker.go | 10 +++---- 115 files changed, 146 insertions(+), 148 deletions(-) rename sources/gcp/shared/{blast-propagations.go => link-rules.go} (91%) diff --git a/sources/gcp/dynamic/adapter-listable.go b/sources/gcp/dynamic/adapter-listable.go index a7edaae8..b1dd5748 100644 --- a/sources/gcp/dynamic/adapter-listable.go +++ b/sources/gcp/dynamic/adapter-listable.go @@ -33,7 +33,7 @@ func NewListableAdapter(listEndpointFunc gcpshared.ListEndpointFunc, config *Ada sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index 22c5983b..fd2eb4aa 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -43,7 +43,7 @@ func NewSearchableListableAdapter(searchURLFunc gcpshared.EndpointFunc, listEndp sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, diff --git a/sources/gcp/dynamic/adapter-searchable.go b/sources/gcp/dynamic/adapter-searchable.go index f35a6a8e..4cc059ad 100644 --- a/sources/gcp/dynamic/adapter-searchable.go +++ b/sources/gcp/dynamic/adapter-searchable.go @@ -37,7 +37,7 @@ func NewSearchableAdapter(searchEndpointFunc gcpshared.EndpointFunc, config *Ada sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index 21a0847a..42d62f7d 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -59,7 +59,7 @@ func NewAdapter(config *AdapterConfig, cache sdpcache.Cache) discovery.Adapter { sdpAdapterCategory: config.SDPAdapterCategory, terraformMappings: config.TerraformMappings, linker: config.Linker, - potentialLinks: potentialLinksFromBlasts(config.SDPAssetType, gcpshared.BlastPropagations), + potentialLinks: potentialLinksFromLinkRules(config.SDPAssetType, gcpshared.LinkRules), uniqueAttributeKeys: config.UniqueAttributeKeys, iamPermissions: config.IAMPermissions, nameSelector: config.NameSelector, diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc index 8799ec19..7709476c 100644 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc +++ b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc @@ -14,7 +14,7 @@ When creating new GCP dynamic adapters in the Overmind codebase, follow these pa - Always use the `registerableAdapter` struct pattern (NOT legacy `SDPAssetTypeToAdapterMeta`) - Implement proper scope detection (project/regional/zonal) -- Include comprehensive blast propagation analysis +- Include comprehensive link rules analysis (attribute key → linked item type) - Add proper terraform mapping support - Follow naming conventions and IAM permissions - Research from official API documentation @@ -152,7 +152,7 @@ UniqueAttributeKeys: []string{"subresources", "resources"}, - Terraform mapping needs SEARCH but adapter doesn't support it - See `orgpolicy-policy.go` for example -### 5. Blast Propagation Analysis +### 5. Link Rules Analysis For each attribute that references another resource: @@ -162,7 +162,7 @@ For each attribute that references another resource: #### Implementation: ```go -blastPropagation: map[string]*gcpshared.Impact{ +linkRules: map[string]*gcpshared.Impact{ "network": { ToSDPItemType: gcpshared.ComputeNetwork, Description: "This resource is affected if network changes. Network is not affected if this resource changes.", @@ -192,9 +192,9 @@ A Spanner database name has the format: `projects/{project}/instances/{instance} To create a backlink from the database to its parent instance: ```go -// In sources/gcp/shared/blast-propagations.go +// In sources/gcp/shared/link-rules.go (or in the adapter's linkRules map) SpannerDatabase: { - // ... other blast propagations ... + // ... other link rules ... // This is a backlink to instance. // Framework will extract the instance name and create the linked item query with GET @@ -217,7 +217,7 @@ SpannerDatabase: { **Testing the Backlink:** -When adding backlink blast propagations, verify they work correctly in tests: +When adding backlink link rules, verify they work correctly in tests: ```go // In adapter_test.go @@ -234,7 +234,7 @@ When adding backlink blast propagations, verify they work correctly in tests: - Child resources with hierarchical naming (database → instance, subnet → network) - Parent resource is part of the child's name/path - Relationship is one-way (child depends on parent, but parent doesn't depend on specific child) -- Use `impactInOnly` blast propagation (parent changes affect child, but not vice versa) +- Use in-only impact (parent changes affect child, but not vice versa) #### Creating Forward Links from Parent to Child Resources @@ -253,7 +253,7 @@ To create a forward link from the instance to its databases: var spannerInstanceAdapter = registerableAdapter{ // ... other fields ... - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This a link from parent to child via SEARCH // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. "name": { @@ -266,7 +266,7 @@ var spannerInstanceAdapter = registerableAdapter{ ``` **How It Works:** -1. The framework detects `IsParentToChild: true` on the blast propagation +1. The framework detects `IsParentToChild: true` on the link rule 2. It modifies the unique attribute keys by removing the last element (the child identifier) - For databases: `["instances", "databases"]` becomes `["instances"]` 3. It extracts only the parent identifier from the resource name (e.g., `test-instance`) @@ -279,7 +279,7 @@ var spannerInstanceAdapter = registerableAdapter{ **Testing the Forward Link:** -When adding parent-to-child blast propagations, verify they work correctly in tests: +When adding parent-to-child link rules, verify they work correctly in tests: ```go // In adapter_test.go (e.g., spanner-instance_test.go) @@ -426,7 +426,7 @@ resource "google_project_service" "overmind_apis" { - **Multiple Unique Attributes**: Check adapters with complex URL structures - **No-op Search**: `orgpolicy-policy.go` - **NameSelector**: `dataproc-cluster.go` -- **Complex Blast Propagation**: `dataproc-cluster.go` +- **Complex link rules**: `dataproc-cluster.go` ## Files to Create/Modify @@ -442,7 +442,7 @@ resource "google_project_service" "overmind_apis" { ### Always Use: - `registerableAdapter` struct, never legacy maps - Endpoint functions that match the exact API URL format -- Clear and specific blast propagation descriptions +- Clear and specific link rule descriptions - Terraform mapping method that matches adapter capabilities - IAM permissions that match the actual API requirements @@ -455,7 +455,7 @@ resource "google_project_service" "overmind_apis" { - [ ] Adapter file compiles without errors - [ ] Proper scope detection and endpoint functions -- [ ] Comprehensive blast propagation analysis +- [ ] Comprehensive link rules analysis - [ ] Valid terraform mapping with correct method (GET/SEARCH) - [ ] Appropriate IAM permissions defined - [ ] Custom role updated if new permissions required (see Custom Role section) @@ -463,7 +463,7 @@ resource "google_project_service" "overmind_apis" { - [ ] Follows existing adapter patterns - [ ] No legacy `SDPAssetTypeToAdapterMeta` usage - [ ] API URLs match official GCP documentation exactly -- [ ] Blast propagation covers all linked resources +- [ ] Link rules cover all linked resources - [ ] Terraform resource name verified in registry - [ ] **Required APIs enabled in deployment**: All APIs used by the adapter are included in `deploy/modules/ovm-services/gke.tf` diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc index 2481e07b..86faf744 100644 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc +++ b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc @@ -450,18 +450,18 @@ func uint64Ptr(u uint64) *uint64 { 5. **Wrong List Response Structure**: Assuming all list responses have `.Items` field (field names vary by service - verify with `go doc`) 6. **Missing Pointer Helpers**: Always define local pointer helper functions 7. **Incorrect HTTP URLs**: Match the exact API endpoint format from the adapter metadata -8. **Missing Static Tests**: Always include blast propagation tests for linked resources +8. **Missing Static Tests**: Always include link rule tests for linked resources 9. **Missing Search Tests**: Include Search tests only if the adapter implements `SearchableAdapter` - use `t.Skipf()` if not supported 10. **Using Pure Strings for ExpectedType**: Always use Item types with `.String()` (e.g., `gcpshared.ComputeNetwork.String()` or `stdlib.NetworkIP.String()`), never pure strings like `"gcp-compute-network"` or `"ip"` -## Blast Propagation Testing Requirements +## Link Rules Testing Requirements **CRITICAL**: Every adapter test SHOULD include comprehensive StaticTests that cover ALL linked resources defined in the adapter. ### Steps to Ensure Complete Coverage 1. **Review Adapter File**: Open the corresponding adapter file (e.g., `compute-global-address.go`) -2. **Find linked-resource map**: Locate the map in the adapter that defines which linked item types the adapter produces (field name may be `blastPropagation`; values are `*gcpshared.Impact`). +2. **Find linked-resource map**: Locate the map in the adapter that defines which linked item types the adapter produces (field name is `linkRules`; values are `*gcpshared.Impact`). 3. **Create Test Cases**: Write a QueryTest for every linked item type the adapter produces (one test per distinct linked type, not per attribute path). 4. **Handle TODOs**: Note any entries marked with TODO comments—these may not work yet but should be documented in test comments. diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go index f88335d7..7c0a4f98 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 state // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.batchPredictionJobs#JobState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "model": { ToSDPItemType: gcpshared.AIPlatformModel, diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go index cee47075..7c073437 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go @@ -293,7 +293,7 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { } t.Run("StaticTests", func(t *testing.T) { - // Only test blast propagation paths that are currently working + // Only test link rule paths that are currently working // (GCS and BigQuery paths have TODOs and require manual linkers) queryTests := shared.QueryTests{ { diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go index ca6a1e0d..df3af6e8 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.customJobs.get", "aiplatform.customJobs.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The Cloud KMS key that will be used to encrypt the output artifacts. "encryptionSpec.kmsKeyName": { Description: "If the Cloud KMS CryptoKey is updated: The CustomJob may not be able to access encrypted output artifacts. If the CustomJob is updated: The CryptoKey remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go index 408084f0..a74a09f8 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.endpoints.get", "aiplatform.endpoints.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "network": gcpshared.ComputeNetworkImpactInOnly, "deployedModels.model": { diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index 177372ad..c27daa77 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -110,7 +110,7 @@ func TestAIPlatformEndpoint(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS key link diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go index 26ffeca9..b1743718 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.modelDeploymentMonitoringJobs#JobState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "endpoint": { ToSDPItemType: gcpshared.AIPlatformEndpoint, diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go index 89ba61b6..312f907b 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go @@ -128,7 +128,7 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", combinedQuery, sdpItem.UniqueAttributeValue()) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS encryption key link diff --git a/sources/gcp/dynamic/adapters/ai-platform-model.go b/sources/gcp/dynamic/adapters/ai-platform-model.go index 0a358a1a..df7c8926 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model.go @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.models.get", "aiplatform.models.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "encryptionSpec.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Container image used for prediction (containerSpec.imageUri). "containerSpec.imageUri": { diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index c07e1167..61632c78 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -106,7 +106,7 @@ func TestAIPlatformModel(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // KMS key link diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go index 42476847..747ecb25 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"aiplatform.pipelineJobs.get", "aiplatform.pipelineJobs.list"}, PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The service account that the pipeline workload runs as (root-level). "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // The full name of the network to which the job should be peered (root-level). diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go index a9900076..18281532 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go @@ -24,7 +24,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"artifactregistry.dockerimages.get", "artifactregistry.dockerimages.list"}, PredefinedRole: "roles/artifactregistry.reader", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a link to its parent resource: ArtifactRegistryRepository // Linker will extract the repository name from the image name. "name": { diff --git a/sources/gcp/dynamic/adapters/artifact-registry-repository.go b/sources/gcp/dynamic/adapters/artifact-registry-repository.go index 51fcffe1..1d6d5c67 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-repository.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-repository.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ PredefinedRole: "roles/artifactregistry.reader", // HEALTH: Not currently exposed on the Repository resource (no status field providing operational state) }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Forward link from parent to child via SEARCH // Link to all docker images in this repository diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go index 21fd7f14..5489a605 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go @@ -25,7 +25,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // state: https://cloud.google.com/bigquery/docs/reference/datatransfer/rest/v1/projects.locations.transferConfigs#TransferState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "destinationDatasetId": { ToSDPItemType: gcpshared.BigQueryDataset, Description: "If the BigQuery Dataset is deleted or updated: The transfer config may fail to write data. If the transfer config is updated: The dataset remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go index e0fd1918..ab5c67fc 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go @@ -150,10 +150,10 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { t.Errorf("Expected notificationPubsubTopic field to be '%s', got %s", notificationPubsubTopic, notificationTopic) } - // Include static tests - MUST cover ALL blast propagation links + // Include static tests - MUST cover ALL link rule links t.Run("StaticTests", func(t *testing.T) { - // CRITICAL: Review the adapter's blast propagation configuration and create - // test cases for EVERY linked resource defined in the adapter's blastPropagation map + // CRITICAL: Review the adapter's link rules configuration and create + // test cases for EVERY linked resource defined in the adapter's link rules map queryTests := shared.QueryTests{ // destinationDatasetId link { diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go index 6b412b52..d57cd5fe 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.appProfiles.get", "bigtable.appProfiles.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The AppProfile may become invalid or inaccessible. If the AppProfile is updated: The instance remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup.go b/sources/gcp/dynamic/adapters/big-table-admin-backup.go index 5ba50cf2..f3eb2297 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup.go @@ -22,7 +22,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.backups.get", "bigtable.backups.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminCluster, Description: "If the BigTableAdmin Cluster is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The cluster remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go index 4cf5c585..b38a9f31 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go @@ -21,7 +21,7 @@ var bigTableAdminClusterAdapter = registerableAdapter{ //nolint:unused // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/bigtable/docs/reference/admin/rest/v2/projects.instances.clusters#State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Customer-managed encryption key protecting data in this cluster. "encryptionConfig.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // This is a backlink to instance. diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index c0f410aa..9de582ed 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // state: https://cloud.google.com/bigtable/docs/reference/admin/rest/v2/projects.instances#State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Forward link from parent to child via SEARCH // Link to all clusters in this instance (most fundamental infrastructure component) "name": { diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 8c7ffab8..9d8365b6 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -23,7 +23,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"bigtable.tables.get", "bigtable.tables.list"}, PredefinedRole: "roles/bigtable.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.BigTableAdminInstance, Description: "If the BigTableAdmin Instance is deleted or updated: The Table may become invalid or inaccessible. If the Table is updated: The instance remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go index 4243b6f7..167430c9 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go @@ -33,7 +33,7 @@ var _ = registerableAdapter{ // This role is required via ai adapters and it gives this exact permission. PredefinedRole: "roles/aiplatform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "projectId": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Cloud Resource Manager Project is deleted or updated: The billing information may become invalid or inaccessible. If the billing info is updated: The project remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/cloud-build-build.go b/sources/gcp/dynamic/adapters/cloud-build-build.go index 8f171550..94ec9101 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build.go @@ -24,7 +24,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudbuild.builds.get", "cloudbuild.builds.list"}, PredefinedRole: "roles/cloudbuild.builds.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, Description: "If the Storage Bucket is deleted or updated: The Cloud Build may fail to access source files. If the Cloud Build is updated: The bucket remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go index 51a51ded..c4f619c3 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go @@ -33,7 +33,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"resourcemanager.projects.get"}, PredefinedRole: "roles/resourcemanager.tagViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There are no links for this item type. // TODO: Currently our highest level of scope is the project. // This item has `parent` attribute that refers to organization or folder which are higher level scopes that we don't support yet. diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go index 42a91d61..00261328 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go @@ -32,6 +32,6 @@ var cloudResourceManagerTagKeyAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/resourcemanager.tagViewer", }, - // No blast propagation yet. TagValue already links back to TagKey via parent attribute. - blastPropagation: map[string]*gcpshared.Impact{}, + // No link rules yet. TagValue already links back to TagKey via parent attribute. + linkRules: map[string]*gcpshared.Impact{}, }.Register() diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go index dfd90e90..842e87f7 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go @@ -189,8 +189,8 @@ func TestCloudResourceManagerTagKey(t *testing.T) { } } - // Note: Since this adapter doesn't define blast propagation relationships, - // we don't run StaticTests here. The adapter's blastPropagation map is empty, + // Note: Since this adapter doesn't define link rule relationships, + // we don't run StaticTests here. The adapter's link rules map is empty, // which is correct as TagKeys are configuration resources rather than runtime resources. }) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go index 6309c7fa..54fadede 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go @@ -36,7 +36,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/resourcemanager.tagViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerTagKey, Description: "They are tightly coupled", diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function.go b/sources/gcp/dynamic/adapters/cloudfunctions-function.go index 34adfece..9ffa2e85 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function.go @@ -28,7 +28,7 @@ var cloudFunctionAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules#Status => state // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "buildConfig.source.storageSource.bucket": { ToSDPItemType: gcpshared.StorageBucket, diff --git a/sources/gcp/dynamic/adapters/compute-accelerator-type.go b/sources/gcp/dynamic/adapters/compute-accelerator-type.go index 9ea4dca6..b51dabf0 100644 --- a/sources/gcp/dynamic/adapters/compute-accelerator-type.go +++ b/sources/gcp/dynamic/adapters/compute-accelerator-type.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.acceleratorTypes.get", "compute.acceleratorTypes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-disk-type.go b/sources/gcp/dynamic/adapters/compute-disk-type.go index 69307f4a..f9c0bd8c 100644 --- a/sources/gcp/dynamic/adapters/compute-disk-type.go +++ b/sources/gcp/dynamic/adapters/compute-disk-type.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.diskTypes.get", "compute.diskTypes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go index 8bbd27d3..043d7c4e 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "interfaces.ipAddress": gcpshared.IPImpactBothWays, "interfaces.ipv6Address": gcpshared.IPImpactBothWays, }, diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index c752cf41..0b5256a1 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.firewalls.get", "compute.firewalls.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, diff --git a/sources/gcp/dynamic/adapters/compute-global-address.go b/sources/gcp/dynamic/adapters/compute-global-address.go index b404d806..62e22dc9 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address.go +++ b/sources/gcp/dynamic/adapters/compute-global-address.go @@ -31,7 +31,7 @@ var computeGlobalAddressAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalAddresses#Status => status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "network": gcpshared.ComputeNetworkImpactInOnly, "address": gcpshared.IPImpactBothWays, diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go index b60b1ebb..c00784ec 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go @@ -31,7 +31,7 @@ var computeGlobalForwardingRuleAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/globalForwardingRules#Status => pscConnectionStatus // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Network reference (global). If the network is changed it may impact the forwarding rule; forwarding rule updates don't impact the network. "network": gcpshared.ComputeNetworkImpactInOnly, "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check.go b/sources/gcp/dynamic/adapters/compute-http-health-check.go index 6bfe96c3..a45bcc0a 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check.go @@ -30,7 +30,7 @@ var _ = registerableAdapter{ }, // HTTP health checks are referenced by backend services and target pools for health monitoring. // Updates to health checks can affect traffic distribution and service availability. - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "host": gcpshared.IPImpactBothWays, }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go index 82233640..6325222f 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -74,7 +74,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { // Test that DNS names are correctly detected when using IPImpactBothWays - // Even though the blast propagation uses stdlib.NetworkIP, it should detect + // Even though the link rule uses stdlib.NetworkIP, it should detect // that "example.com" is a DNS name and create a DNS link queryTests := shared.QueryTests{ { @@ -88,7 +88,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { }) // Test with IP address - verify bidirectional detection works - // Even though the blast propagation uses stdlib.NetworkIP, it should detect + // Even though the link rule uses stdlib.NetworkIP, it should detect // that "192.168.1.1" is an IP address and create an IP link t.Run("StaticTestsWithIP", func(t *testing.T) { healthCheckWithIP := map[string]interface{}{ @@ -152,7 +152,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { // Verify that both IP and DNS are in potential links when using IPImpactBothWays // This demonstrates bidirectional behavior: even though we specify stdlib.NetworkIP - // in the blast propagation, both IP and DNS are included in potential links + // in the link rules, both IP and DNS are included in potential links potentialLinksMap := make(map[string]bool) for _, link := range metadata.GetPotentialLinks() { potentialLinksMap[link] = true diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index e010f6da..92040e41 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.instanceTemplates.get", "compute.instanceTemplates.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/get "properties.networkInterfaces.network": { Description: "If the network is deleted: Resources may experience connectivity changes or disruptions. If the template is deleted: Network itself is not affected.", diff --git a/sources/gcp/dynamic/adapters/compute-license.go b/sources/gcp/dynamic/adapters/compute-license.go index b4d56b85..2025eaf3 100644 --- a/sources/gcp/dynamic/adapters/compute-license.go +++ b/sources/gcp/dynamic/adapters/compute-license.go @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.licenses.get", "compute.licenses.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go index ad8023b0..69513568 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go @@ -28,7 +28,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Parent VPC network reference (changes to network can impact NEG reachability; NEG changes do not impact network) "network": gcpshared.ComputeNetworkImpactInOnly, // Subnetwork reference (regional) – subnetwork changes can affect endpoints, NEG changes do not affect subnetwork diff --git a/sources/gcp/dynamic/adapters/compute-network.go b/sources/gcp/dynamic/adapters/compute-network.go index 17ab1235..261553f8 100644 --- a/sources/gcp/dynamic/adapters/compute-network.go +++ b/sources/gcp/dynamic/adapters/compute-network.go @@ -19,7 +19,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.networks.get", "compute.networks.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "gatewayIPv4": gcpshared.IPImpactBothWays, "subnetworks": { Description: "If the Compute Subnetwork is deleted: The network remains unaffected, but its subnetwork configuration may change. If the network is deleted: All associated subnetworks are also deleted.", diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index abf127a8..2b69ffcd 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -40,7 +40,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.projects.get"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "defaultServiceAccount": { Description: "If the IAM Service Account is deleted: Project resources may fail to work as before. If the project is deleted: service account is deleted.", ToSDPItemType: gcpshared.IAMServiceAccount, diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go index a91500f0..26080368 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go @@ -36,7 +36,7 @@ var _ = registerableAdapter{ // HEALTH: status (e.g., LIVE/TO_BE_DELETED) may be present on the resource // TODO: https://linear.app/overmind/issue/ENG-631 }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Parent Public Advertised Prefix from which this delegated prefix is allocated. "parentPrefix": { ToSDPItemType: gcpshared.ComputePublicAdvertisedPrefix, diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment.go b/sources/gcp/dynamic/adapters/compute-region-commitment.go index 369aa89a..75d6701e 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment.go @@ -22,7 +22,7 @@ var _ = registerableAdapter{ // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/regionCommitments#Status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "reservations.name": { ToSDPItemType: gcpshared.ComputeReservation, Description: "If the Region Commitment is deleted or updated: Reservations that reference this commitment may lose associated discounts or resource guarantees. If the Reservation is updated or deleted: The commitment remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/compute-resource-policy.go b/sources/gcp/dynamic/adapters/compute-resource-policy.go index e7ee1eba..97370e4e 100644 --- a/sources/gcp/dynamic/adapters/compute-resource-policy.go +++ b/sources/gcp/dynamic/adapters/compute-resource-policy.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.resourcePolicies.get", "compute.resourcePolicies.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Cloud Storage bucket storage location where snapshots created by this policy are stored. // The storageLocations field can contain bucket names, gs:// URIs, or region identifiers. // The manual adapter linker will handle extraction of bucket names from various formats. diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index aa3a63e6..9c7c79ae 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.routes.get", "compute.routes.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get // Network that the route belongs to "network": { diff --git a/sources/gcp/dynamic/adapters/compute-router.go b/sources/gcp/dynamic/adapters/compute-router.go index 466d5d3d..ffb57ea7 100644 --- a/sources/gcp/dynamic/adapters/compute-router.go +++ b/sources/gcp/dynamic/adapters/compute-router.go @@ -28,7 +28,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.routers.get", "compute.routers.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": gcpshared.ComputeNetworkImpactInOnly, "interfaces.linkedInterconnectAttachment": { ToSDPItemType: gcpshared.ComputeInterconnectAttachment, @@ -63,7 +63,7 @@ var _ = registerableAdapter{ IsParentToChild: true, // Router discovers all its Route Policies via SEARCH }, // Note: BgpRoute is also a child resource with listBgpRoutes endpoint, but we can only use "name" - // once in the blastPropagation map. When BgpRoute adapter is created with SEARCH support, + // once in the link rules map. When BgpRoute adapter is created with SEARCH support, // we can consider using a different field or handling it separately. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index b3415689..13fef494 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -133,7 +133,7 @@ func TestComputeRouter(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", routerName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Network link diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go index adf5b755..5b97c6a5 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go @@ -29,7 +29,7 @@ var computeSSLCertificateAdapter = registerableAdapter{ //nolint:unused }, }, }, - blastPropagation: map[string]*gcpshared.Impact{ - // There is no blast propagation originating from Compute SSL Certificates + linkRules: map[string]*gcpshared.Impact{ + // There are no link rules originating from Compute SSL Certificates }, }.Register() diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy.go b/sources/gcp/dynamic/adapters/compute-ssl-policy.go index 9bb9cf32..eb130d4b 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // SSL Policies are configuration-only resources that define TLS/SSL parameters // They don't have dependencies on other GCP resources, but are referenced by: // - Target HTTPS Proxies (via sslPolicy field) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go index 48e4c892..99c3da7d 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go @@ -70,7 +70,7 @@ func TestComputeSSLPolicy(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", policyName, sdpItem.UniqueAttributeValue()) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter }) t.Run("List", func(t *testing.T) { diff --git a/sources/gcp/dynamic/adapters/compute-storage-pool.go b/sources/gcp/dynamic/adapters/compute-storage-pool.go index 35f589ed..5e50e787 100644 --- a/sources/gcp/dynamic/adapters/compute-storage-pool.go +++ b/sources/gcp/dynamic/adapters/compute-storage-pool.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.storagePools.get", "compute.storagePools.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link to the storage pool type that defines the characteristics of this storage pool "storagePoolType": { ToSDPItemType: gcpshared.ComputeStoragePoolType, diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork.go b/sources/gcp/dynamic/adapters/compute-subnetwork.go index fe23869e..ab32e06e 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"compute.subnetworks.get", "compute.subnetworks.list"}, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": { Description: "If the Compute Network is updated: The firewall rules may no longer apply correctly. If the firewall is updated: The network remains unaffected, but its security posture may change.", ToSDPItemType: gcpshared.ComputeNetwork, diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go index 9553913b..6c416fd6 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTP proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go index 5d55d515..558dadaa 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "urlMap": { ToSDPItemType: gcpshared.ComputeUrlMap, Description: "If the URL Map is updated or deleted: The HTTPS proxy routing behavior may change or break. If the proxy changes: The URL map remains structurally unaffected.", diff --git a/sources/gcp/dynamic/adapters/compute-target-pool.go b/sources/gcp/dynamic/adapters/compute-target-pool.go index d02d711d..af83e0a7 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instances": { ToSDPItemType: gcpshared.ComputeInstance, Description: "If the Compute Instance is deleted or updated: the pool membership becomes invalid or traffic may fail to reach it. If the pool is updated: the instance remains structurally unaffected.", diff --git a/sources/gcp/dynamic/adapters/compute-url-map.go b/sources/gcp/dynamic/adapters/compute-url-map.go index 7c73f2df..ae5b6300 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map.go +++ b/sources/gcp/dynamic/adapters/compute-url-map.go @@ -33,7 +33,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "defaultService": computeBackendImpact, "defaultRouteAction.weightedBackendServices.backendService": computeBackendImpact, "defaultRouteAction.requestMirrorPolicy.backendService": computeBackendImpact, diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go index b3cb30b4..13450425 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go @@ -28,7 +28,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/compute.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Network associated with the VPN gateway. "network": gcpshared.ComputeNetworkImpactInOnly, // IP addresses assigned to VPN interfaces (each interface may have an external IP). diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go index c01d507a..ac4e9ed6 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go @@ -30,7 +30,7 @@ var _ = registerableAdapter{ // HEALTH: https://cloud.google.com/compute/docs/reference/rest/v1/vpnTunnels#Status => status // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The peer IP address of the remote VPN gateway. "peerIp": gcpshared.IPImpactBothWays, "targetVpnGateway": { diff --git a/sources/gcp/dynamic/adapters/container-cluster.go b/sources/gcp/dynamic/adapters/container-cluster.go index b99851df..f62112db 100644 --- a/sources/gcp/dynamic/adapters/container-cluster.go +++ b/sources/gcp/dynamic/adapters/container-cluster.go @@ -32,7 +32,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/container.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "network": gcpshared.ComputeNetworkImpactInOnly, "subnetwork": gcpshared.ComputeSubnetworkImpactInOnly, "nodePools.config.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 8de4abb5..79330046 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -135,7 +135,7 @@ func TestContainerCluster(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", clusterName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Network link diff --git a/sources/gcp/dynamic/adapters/container-node-pool.go b/sources/gcp/dynamic/adapters/container-node-pool.go index 088505c6..b91f98a4 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool.go +++ b/sources/gcp/dynamic/adapters/container-node-pool.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.locations.clusters.nodePools#NodePool.Status }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a backlink to cluster. // Framework will extract the cluster name and create the linked item query with GET "name": { diff --git a/sources/gcp/dynamic/adapters/dataform-repository.go b/sources/gcp/dynamic/adapters/dataform-repository.go index a19dffa7..465f89b7 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository.go +++ b/sources/gcp/dynamic/adapters/dataform-repository.go @@ -24,7 +24,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dataform.repositories.get", "dataform.repositories.list"}, PredefinedRole: "roles/dataform.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name of the Secret Manager secret version to use as an authentication token for Git operations. Must be in the format projects/*/secrets/*/versions/*. "gitRemoteSettings.authenticationTokenSecretVersion": { ToSDPItemType: gcpshared.SecretManagerSecret, diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go index 22272151..e342c527 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go @@ -30,7 +30,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/dataplex.catalogViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Based on the AspectType structure from the API documentation, // AspectTypes typically define metadata schemas and don't have direct dependencies // on other GCP resources in their core definition. They are schema definitions diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go index 13d1d34d..907d17f8 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go @@ -152,8 +152,8 @@ func TestDataplexAspectType(t *testing.T) { t.Errorf("Expected etag field to be 'BwWWja0YfJA=', got %s", val) } - // Note: Since this adapter doesn't define blast propagation relationships, - // we don't run StaticTests here. The adapter's blastPropagation map is empty, + // Note: Since this adapter doesn't define link rule relationships, + // we don't run StaticTests here. The adapter's link rules map is empty, // which is correct as AspectTypes are schema definitions rather than runtime resources. }) diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan.go b/sources/gcp/dynamic/adapters/dataplex-data-scan.go index 16bb666a..cd16119b 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 state // https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.dataScans#DataScan }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Data source references - can scan various data sources "data.entity": { ToSDPItemType: gcpshared.DataplexEntity, diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group.go b/sources/gcp/dynamic/adapters/dataplex-entry-group.go index d5c087ad..1db80552 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group.go @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dataplex.entryGroups.get", "dataplex.entryGroups.list"}, PredefinedRole: "roles/dataplex.catalogViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go index ee7233cb..facb5f79 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go @@ -29,7 +29,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/dataproc.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // AutoscalingPolicies don't directly reference other resources, // but they are referenced by Dataproc clusters via config.autoscalingConfig.policyUri // The reverse relationship is handled in the cluster adapter diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go index 4b566767..aea41fac 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go @@ -120,7 +120,7 @@ func TestDataprocAutoscalingPolicy(t *testing.T) { t.Errorf("Expected unique attribute value '%s', got %s", policyName, sdpItem.UniqueAttributeValue()) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter }) t.Run("List", func(t *testing.T) { diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster.go b/sources/gcp/dynamic/adapters/dataproc-cluster.go index fc2b7496..b8e1bca8 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/dataproc/docs/reference/rest/v1/projects.regions.clusters#clusterstatus }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "config.gceClusterConfig.networkUri": gcpshared.ComputeNetworkImpactInOnly, "config.gceClusterConfig.subnetworkUri": gcpshared.ComputeSubnetworkImpactInOnly, "config.gceClusterConfig.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone.go b/sources/gcp/dynamic/adapters/dns-managed-zone.go index 5d7096c1..d05ec820 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone.go @@ -22,7 +22,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"dns.managedZones.get", "dns.managedZones.list"}, PredefinedRole: "roles/dns.reader", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "dnsName": { ToSDPItemType: stdlib.NetworkDNS, Description: "Tightly coupled with the DNS Managed Zone.", diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact.go b/sources/gcp/dynamic/adapters/essential-contacts-contact.go index cbaaea43..ac5a3929 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"essentialcontacts.contacts.get", "essentialcontacts.contacts.list"}, PredefinedRole: "roles/essentialcontacts.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/eventarc-trigger.go b/sources/gcp/dynamic/adapters/eventarc-trigger.go index 7f892729..e4d78838 100644 --- a/sources/gcp/dynamic/adapters/eventarc-trigger.go +++ b/sources/gcp/dynamic/adapters/eventarc-trigger.go @@ -30,7 +30,7 @@ var eventarcTriggerAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/eventarc.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Service account used by the trigger to invoke the target service "serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // Channel associated with the trigger for event delivery diff --git a/sources/gcp/dynamic/adapters/file-instance.go b/sources/gcp/dynamic/adapters/file-instance.go index 7cb37edb..1b5fde1e 100644 --- a/sources/gcp/dynamic/adapters/file-instance.go +++ b/sources/gcp/dynamic/adapters/file-instance.go @@ -34,7 +34,7 @@ var _ = registerableAdapter{ PredefinedRole: "roles/file.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 => state }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "networks.network": gcpshared.ComputeNetworkImpactInOnly, "networks.ipAddresses": gcpshared.IPImpactBothWays, "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/iam-role.go b/sources/gcp/dynamic/adapters/iam-role.go index 98175155..aeeb04fd 100644 --- a/sources/gcp/dynamic/adapters/iam-role.go +++ b/sources/gcp/dynamic/adapters/iam-role.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"iam.roles.get", "iam.roles.list"}, PredefinedRole: "roles/iam.roleViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/logging-bucket.go b/sources/gcp/dynamic/adapters/logging-bucket.go index e5d34072..53f101d3 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket.go +++ b/sources/gcp/dynamic/adapters/logging-bucket.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.buckets.get", "logging.buckets.list"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "cmekSettings.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "cmekSettings.kmsKeyVersionName": gcpshared.CryptoKeyVersionImpactInOnly, "cmekSettings.serviceAccountId": gcpshared.IAMServiceAccountImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/logging-link.go b/sources/gcp/dynamic/adapters/logging-link.go index 8229b158..e338c8de 100644 --- a/sources/gcp/dynamic/adapters/logging-link.go +++ b/sources/gcp/dynamic/adapters/logging-link.go @@ -25,7 +25,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.links.get", "logging.links.list"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Logging Link may lose its association or fail to function as expected. If the Logging Link is updated: The bucket remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/logging-saved-query.go b/sources/gcp/dynamic/adapters/logging-saved-query.go index 120f74c8..c23220a5 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query.go @@ -27,7 +27,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"logging.queries.getShared", "logging.queries.listShared"}, PredefinedRole: "roles/logging.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/models.go b/sources/gcp/dynamic/adapters/models.go index 77a03b42..5ef55932 100644 --- a/sources/gcp/dynamic/adapters/models.go +++ b/sources/gcp/dynamic/adapters/models.go @@ -8,13 +8,13 @@ import ( type registerableAdapter struct { sdpType shared.ItemType meta gcpshared.AdapterMeta - blastPropagation map[string]*gcpshared.Impact + linkRules map[string]*gcpshared.Impact terraformMapping gcpshared.TerraformMapping } func (d registerableAdapter) Register() registerableAdapter { gcpshared.SDPAssetTypeToAdapterMeta[d.sdpType] = d.meta - gcpshared.BlastPropagations[d.sdpType] = d.blastPropagation + gcpshared.LinkRules[d.sdpType] = d.linkRules gcpshared.SDPAssetTypeToTerraformMappings[d.sdpType] = d.terraformMapping return d diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go index c1f0a0b1..b3b44e37 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "notificationChannels": { ToSDPItemType: gcpshared.MonitoringNotificationChannel, Description: "The notification channels that are used to notify when this alert policy is triggered. If notification channels are deleted, the alert policy will not be able to notify when triggered. If the alert policy is deleted, the notification channels will not be affected.", diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index 64f756c4..4ff559be 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -104,7 +104,7 @@ func TestMonitoringAlertPolicy(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Notification channel links diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go index 034e317f..4970facf 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"monitoring.dashboards.get", "monitoring.dashboards.list"}, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // There is no links for this item type. }, terraformMapping: gcpshared.TerraformMapping{ diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go index 30682972..ced1a890 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/monitoring.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // For pubsub type notification channels, the topic label contains the Pub/Sub topic resource name // Format: projects/{project}/topics/{topic} "labels.topic": { diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go index f56347bc..4a44fe7d 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go @@ -95,7 +95,7 @@ func TestMonitoringNotificationChannel(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter // Static tests fail when linked queries are nil }) diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy.go b/sources/gcp/dynamic/adapters/orgpolicy-policy.go index 8da96f47..7c571eaa 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy.go @@ -34,7 +34,7 @@ var orgPolicyPolicyAdapter = registerableAdapter{ //nolint:unused }, PredefinedRole: "roles/orgpolicy.policyViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name field contains the parent resource identifier (project, folder, or organization) // Format: projects/{project_number}/policies/{constraint} or // folders/{folder_id}/policies/{constraint} or diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go index 9569e6ef..e87ba80b 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go @@ -88,7 +88,7 @@ func TestOrgPolicyPolicy(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Skip static tests - no blast propagations for this adapter + // Skip static tests - no link rules for this adapter // Static tests fail when linked queries are nil }) diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index 15606da7..c486b016 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -23,7 +23,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"pubsub.subscriptions.get", "pubsub.subscriptions.list"}, PredefinedRole: "roles/pubsub.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "topic": { ToSDPItemType: gcpshared.PubSubTopic, Description: "If the Pub/Sub Topic is deleted or updated: The Subscription may fail to receive messages. If the Subscription is updated: The topic remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index fa7b001b..34231585 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -23,7 +23,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"pubsub.topics.get", "pubsub.topics.list"}, PredefinedRole: "roles/pubsub.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Schema settings for message validation "schemaSettings.schema": { diff --git a/sources/gcp/dynamic/adapters/redis-instance.go b/sources/gcp/dynamic/adapters/redis-instance.go index 143e69f8..681230ad 100644 --- a/sources/gcp/dynamic/adapters/redis-instance.go +++ b/sources/gcp/dynamic/adapters/redis-instance.go @@ -39,7 +39,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/memorystore/docs/redis/reference/rest/v1/projects.locations.instances#Instance.State }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The name of the VPC network to which the instance is connected. "authorizedNetwork": gcpshared.ComputeNetworkImpactInOnly, // Optional. The KMS key reference that the customer provides when trying to create the instance. diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index e9588cae..42f5c233 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -106,7 +106,7 @@ func TestRedisInstance(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // Authorized network link diff --git a/sources/gcp/dynamic/adapters/run-revision.go b/sources/gcp/dynamic/adapters/run-revision.go index 38893cea..c7dea3c5 100644 --- a/sources/gcp/dynamic/adapters/run-revision.go +++ b/sources/gcp/dynamic/adapters/run-revision.go @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"run.revisions.get", "run.revisions.list"}, PredefinedRole: "roles/run.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "service": { ToSDPItemType: gcpshared.RunService, Description: "If the Run Service is deleted or updated: The Revision may lose its association or fail to run. If the Revision is updated: The service remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/run-service.go b/sources/gcp/dynamic/adapters/run-service.go index 252cd7e5..a0aa5d30 100644 --- a/sources/gcp/dynamic/adapters/run-service.go +++ b/sources/gcp/dynamic/adapters/run-service.go @@ -30,7 +30,7 @@ var _ = registerableAdapter{ PredefinedRole: "roles/run.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 - status field for health monitoring }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "template.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, "template.vpcAccess.connector": { ToSDPItemType: gcpshared.VPCAccessConnector, diff --git a/sources/gcp/dynamic/adapters/run-worker-pool.go b/sources/gcp/dynamic/adapters/run-worker-pool.go index fdec386b..32a72c6a 100644 --- a/sources/gcp/dynamic/adapters/run-worker-pool.go +++ b/sources/gcp/dynamic/adapters/run-worker-pool.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/run.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Service account used by revisions in the worker pool "template.serviceAccount": gcpshared.IAMServiceAccountImpactInOnly, // Encryption key for image encryption diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret.go b/sources/gcp/dynamic/adapters/secret-manager-secret.go index b83d2107..c8bb9047 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret.go @@ -32,7 +32,7 @@ var _ = registerableAdapter{ }, PredefinedRole: "roles/secretmanager.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // CMEK used with Automatic replication "replication.automatic.customerManagedEncryption.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, // CMEK used with User-managed replication per replica diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go index 17e4331b..1d7bfb41 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go @@ -30,7 +30,7 @@ var _ = registerableAdapter{ PredefinedRole: "roles/securitycentermanagement.viewer", // TODO: https://linear.app/overmind/issue/ENG-631 - check if SecurityCenterService has status/state attribute }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link to parent resource (project, folder, or organization) from name field // The name field format is: projects/{project}/locations/{location}/securityCenterServices/{service} // or: folders/{folder}/locations/{location}/securityCenterServices/{service} diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint.go b/sources/gcp/dynamic/adapters/service-directory-endpoint.go index a73140f4..ef527beb 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint.go @@ -23,7 +23,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"servicedirectory.endpoints.get", "servicedirectory.endpoints.list"}, PredefinedRole: "roles/servicedirectory.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "name": { ToSDPItemType: gcpshared.ServiceDirectoryService, Description: "If the Service Directory Service is deleted or updated: The Endpoint may lose its association or fail to resolve names. If the Endpoint is updated: The service remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/service-directory-service.go b/sources/gcp/dynamic/adapters/service-directory-service.go index 7ff59dbf..5f1f6741 100644 --- a/sources/gcp/dynamic/adapters/service-directory-service.go +++ b/sources/gcp/dynamic/adapters/service-directory-service.go @@ -22,7 +22,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"servicedirectory.services.get", "servicedirectory.services.list"}, PredefinedRole: "roles/servicedirectory.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Link from parent Service to child Endpoints via SEARCH // The framework will extract location, namespace, and service from the service name // and create a SEARCH query to find all endpoints under this service diff --git a/sources/gcp/dynamic/adapters/service-usage-service.go b/sources/gcp/dynamic/adapters/service-usage-service.go index 3da5bfec..713515e6 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service.go +++ b/sources/gcp/dynamic/adapters/service-usage-service.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"serviceusage.services.get", "serviceusage.services.list"}, PredefinedRole: "roles/serviceusage.serviceUsageViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "parent": { ToSDPItemType: gcpshared.CloudResourceManagerProject, Description: "If the Project is deleted or updated: The Service Usage Service may become invalid or inaccessible. If the Service Usage Service is updated: The project remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/spanner-backup.go b/sources/gcp/dynamic/adapters/spanner-backup.go index 69a10c59..ae851489 100644 --- a/sources/gcp/dynamic/adapters/spanner-backup.go +++ b/sources/gcp/dynamic/adapters/spanner-backup.go @@ -20,7 +20,7 @@ var _ = registerableAdapter{ UniqueAttributeKeys: []string{"instances", "backups"}, IAMPermissions: []string{"spanner.backups.get", "spanner.backups.list"}, }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // This is a backlink to instance. // Framework will extract the instance name and create the linked item query with GET "name": { diff --git a/sources/gcp/dynamic/adapters/spanner-database.go b/sources/gcp/dynamic/adapters/spanner-database.go index 8cfb4b5f..9b17b9aa 100644 --- a/sources/gcp/dynamic/adapters/spanner-database.go +++ b/sources/gcp/dynamic/adapters/spanner-database.go @@ -23,7 +23,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"spanner.databases.get", "spanner.databases.list"}, PredefinedRole: "overmind_custom_role", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // The Cloud KMS key used to encrypt the database. "encryptionConfig.kmsKeyName": gcpshared.CryptoKeyImpactInOnly, "encryptionConfig.kmsKeyNames": gcpshared.CryptoKeyImpactInOnly, diff --git a/sources/gcp/dynamic/adapters/spanner-instance-config.go b/sources/gcp/dynamic/adapters/spanner-instance-config.go index 2b6cf1d7..7c852024 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance-config.go +++ b/sources/gcp/dynamic/adapters/spanner-instance-config.go @@ -21,7 +21,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"spanner.instanceConfigs.get", "spanner.instanceConfigs.list"}, PredefinedRole: "roles/spanner.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{}, + linkRules: map[string]*gcpshared.Impact{}, terraformMapping: gcpshared.TerraformMapping{ Description: "There is no terraform resource for this type.", }, diff --git a/sources/gcp/dynamic/adapters/spanner-instance.go b/sources/gcp/dynamic/adapters/spanner-instance.go index 6523ad15..3ffceb66 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance.go +++ b/sources/gcp/dynamic/adapters/spanner-instance.go @@ -22,7 +22,7 @@ var spannerInstanceAdapter = registerableAdapter{ //nolint:unused // HEALTH: https://cloud.google.com/spanner/docs/reference/rest/v1/projects.instances#State // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "config": { ToSDPItemType: gcpshared.SpannerInstanceConfig, Description: "If the Spanner Instance Config is deleted or updated: The Spanner Instance may fail to operate correctly. If the Spanner Instance is updated: The config remains unaffected.", diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go index 853d1569..2d4f185a 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go @@ -24,7 +24,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudsql.backupRuns.get", "cloudsql.backupRuns.list"}, PredefinedRole: "roles/cloudsql.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "They are tightly coupled", diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup.go b/sources/gcp/dynamic/adapters/sql-admin-backup.go index 4ee80888..061c5eec 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup.go @@ -24,7 +24,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"cloudsql.backupRuns.get", "cloudsql.backupRuns.list"}, PredefinedRole: "roles/cloudsql.viewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ "instance": { ToSDPItemType: gcpshared.SQLAdminInstance, Description: "If the Cloud SQL Instance is deleted or updated: The Backup may become invalid or inaccessible. If the Backup is updated: The instance cannot recover from the backup.", diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index c84906f9..d54365d4 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -127,7 +127,7 @@ func TestSQLAdminBackup(t *testing.T) { ExpectedScope: "global", }, // Note: allocatedIpRange link is not tested here because the NetworkConnectivityInternalRange adapter doesn't exist yet. - // The blast propagation is defined in the adapter so it will work automatically when the adapter is created. + // The link rule is defined in the adapter so it will work automatically when the adapter is created. } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance.go b/sources/gcp/dynamic/adapters/sql-admin-instance.go index ea4f8094..a410c8b9 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance.go @@ -31,7 +31,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631/investigate-how-we-can-add-health-status-for-supporting-items // https://cloud.google.com/sql/docs/mysql/admin-api/rest/v1/instances#SqlInstanceState }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // VPC network used for private service connectivity. "settings.ipConfiguration.privateNetwork": gcpshared.ComputeNetworkImpactInOnly, // CMEK used to encrypt the primary data disk. diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index f27ce785..1bbcfd3c 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -111,7 +111,7 @@ func TestSQLAdminInstance(t *testing.T) { t.Errorf("Expected name field to be '%s', got %s", expectedName, val) } - // Include static tests - covers ALL blast propagation links + // Include static tests - covers ALL link rule links t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ // settings.ipConfiguration.privateNetwork diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index 11c2249d..bc18b7eb 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -29,7 +29,7 @@ var _ = registerableAdapter{ IAMPermissions: []string{"storage.buckets.get", "storage.buckets.list"}, PredefinedRole: "roles/storage.bucketViewer", }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // A Cloud KMS key that will be used to encrypt objects written to this bucket if no encryption method is specified as part of the object write request. "encryption.defaultKmsKeyName": gcpshared.CryptoKeyImpactInOnly, // Name of the network. diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index 692649d2..9c63605f 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -35,7 +35,7 @@ var _ = registerableAdapter{ // TODO: https://linear.app/overmind/issue/ENG-631 status // https://cloud.google.com/storage-transfer/docs/reference/rest/v1/transferJobs#TransferJob.status }, - blastPropagation: map[string]*gcpshared.Impact{ + linkRules: map[string]*gcpshared.Impact{ // Transfer spec references to source and destination storage "transferSpec.gcsDataSource.bucketName": { ToSDPItemType: gcpshared.StorageBucket, diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index 24c40ced..c86e08e1 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -317,9 +317,9 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. return nil } -func potentialLinksFromBlasts(itemType shared.ItemType, blasts map[shared.ItemType]map[string]*gcpshared.Impact) []string { +func potentialLinksFromLinkRules(itemType shared.ItemType, linkRules map[shared.ItemType]map[string]*gcpshared.Impact) []string { potentialLinksMap := make(map[string]bool) - for _, impact := range blasts[itemType] { + for _, impact := range linkRules[itemType] { potentialLinksMap[impact.ToSDPItemType.String()] = true // Special case: stdlib.NetworkIP and stdlib.NetworkDNS are interchangeable // because the linker automatically detects whether a value is an IP address or DNS name diff --git a/sources/gcp/shared/blast-propagations.go b/sources/gcp/shared/link-rules.go similarity index 91% rename from sources/gcp/shared/blast-propagations.go rename to sources/gcp/shared/link-rules.go index ef5d9aec..f03730b0 100644 --- a/sources/gcp/shared/blast-propagations.go +++ b/sources/gcp/shared/link-rules.go @@ -13,7 +13,7 @@ type Impact struct { var ( IPImpactBothWays = &Impact{ - Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in blast propagation - both will automatically detect the actual type.", + Description: "IP addresses and DNS names are tightly coupled with the source type. The linker automatically detects whether the value is an IP address or DNS name and creates the appropriate link. You can use either stdlib.NetworkIP or stdlib.NetworkDNS in the link rules - both will automatically detect the actual type.", ToSDPItemType: stdlib.NetworkIP, } SecurityPolicyImpactInOnly = &Impact{ @@ -46,6 +46,6 @@ var ( } ) -// BlastPropagations maps item types to their blast propagation rules. +// LinkRules maps item types to their link rules (attribute key -> target type metadata). // This map is populated during source initiation by individual adapter files. -var BlastPropagations = map[shared.ItemType]map[string]*Impact{} +var LinkRules = map[shared.ItemType]map[string]*Impact{} diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 1a1f5848..66516a74 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -32,15 +32,13 @@ type ItemLookup map[string]ItemTypeMeta // Linker is responsible for linking items based on their types and relationships. type Linker struct { sdpAssetTypeToAdapterMeta map[shared.ItemType]AdapterMeta - explicitBlastPropagations map[shared.ItemType]map[string]*Impact - manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string) *sdp.LinkedItemQuery + manualAdapterLinker map[shared.ItemType]func(scope, fromItemScope, query string) *sdp.LinkedItemQuery } // NewLinker creates a new Linker instance with the provided item lookup and predefined mappings. func NewLinker() *Linker { return &Linker{ sdpAssetTypeToAdapterMeta: SDPAssetTypeToAdapterMeta, - explicitBlastPropagations: BlastPropagations, manualAdapterLinker: ManualAdapterLinksByAssetType, } } @@ -63,9 +61,9 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd "ovm.gcp.key": key, } - impacts, ok := l.explicitBlastPropagations[fromSDPItemType] + impacts, ok := LinkRules[fromSDPItemType] if !ok { - log.WithContext(ctx).WithFields(lf).Warnf("there are no blast propagations for the FROM item type") + log.WithContext(ctx).WithFields(lf).Warnf("there are no link rules for the FROM item type") return } @@ -89,7 +87,7 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd if linkFunc, ok := l.manualAdapterLinker[impact.ToSDPItemType]; ok { // Special handling for stdlib.NetworkIP and stdlib.NetworkDNS - detect both IP and DNS // This handles fields like "host" that could contain either an IP address or DNS name - // You can specify either IP or DNS in the blast propagation, and it will automatically + // You can specify either IP or DNS in the link rules, and it will automatically // detect which type the value actually is and create the appropriate link if impact.ToSDPItemType == stdlib.NetworkIP || impact.ToSDPItemType == stdlib.NetworkDNS { l.linkIPOrDNS(ctx, fromSDPItem, toItemGCPResourceName) From 2ba43581f6dce9a7fa7b46a241c941f5d4b8f8a7 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 16 Feb 2026 15:43:27 +0100 Subject: [PATCH 23/51] Software Architecting (#3886) This PR contains a bunch of changes to the cursor rules and skills, as well as a bunch of added docs around architecture and ADR process. All of this is preliminary and draft but should give us a good basis to start for this week. --- > [!NOTE] > **Medium Risk** > Mostly documentation and developer-workflow changes, but it modifies CI/codegen and lint tooling (`go generate`, SQLC regeneration, linter switch) which can cause build/CI churn or unexpected diffs if versions or generation steps diverge. > > **Overview** > Introduces a full **ADR process** in-repo (`docs/adr/*` with `INDEX.md`, template, and 17 initial Accepted ADRs), plus new architecture docs (`docs/context-map.md`), a canonical `docs/domain-glossary.md`, and a DDD gaps writeup; internal docs are updated to reference ADRs and current tooling (e.g., frontend stack, logging, SQLC usage). > > Overhauls Cursor automation: adds a `doc-maintainer` agent and documentation workflow rule, refines/retargets many `.cursor/rules/*.mdc` globs and standards (Go/SQL/Frontend/Sources), adds ADR BUGBOT review rules, and removes some legacy rules/scripts. > > Tightens and standardizes dev/CI tooling: pins several devcontainer Go tool versions, switches linting guidance/settings from `golangci-lint-v2` to `golangci-lint` (and sets a default timeout in `.golangci.yml`), updates CI to run `go generate ./...` (with new `*/models/generate.go` wrappers to run `sqlc generate`), and adds a devcontainer mount for host `.cursor` settings. Also includes a small revlink test change to create nodes inside a Neo4j write transaction. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 67809feaf8a032d963406066f4285ff20d7a2cfc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 2c31acb7bd6f0f8bd5236195fb1a8dff8d8b156e --- .../rules/azure-manual-adapter-creation.mdc | 805 ------------------ .../rules/dynamic-adapter-creation.mdc | 477 ----------- .../.cursor/rules/dynamic-adapter-testing.mdc | 622 -------------- .../rules/gcp-manual-adapter-creation.mdc | 802 ----------------- 4 files changed, 2706 deletions(-) delete mode 100644 sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc delete mode 100644 sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc delete mode 100644 sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc delete mode 100644 sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc diff --git a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc b/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc deleted file mode 100644 index fb831091..00000000 --- a/sources/azure/manual/.cursor/rules/azure-manual-adapter-creation.mdc +++ /dev/null @@ -1,805 +0,0 @@ ---- -description: "Azure Manual Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# Azure Manual Adapter Creation Rules - -This document provides comprehensive rules and patterns for creating Azure manual adapters in the Overmind platform. Follow these guidelines to ensure consistency, maintainability, and proper integration with the SDP (State Description Protocol) framework. - -## Table of Contents - -1. [Adapter Structure and Naming](#adapter-structure-and-naming) -2. [Wrapper Type Selection](#wrapper-type-selection) -3. [Base Struct Selection](#base-struct-selection) -4. [Required Methods Implementation](#required-methods-implementation) -5. [Terraform Mappings](#terraform-mappings) -6. [Get and Search Lookups](#get-and-search-lookups) -7. [Linked Item Queries](#linked-item-queries) -8. [Error Handling](#error-handling) -9. [Testing Patterns](#testing-patterns) -10. [Client Interface Patterns](#client-interface-patterns) -11. [Common Gotchas and Best Practices](#common-gotchas-and-best-practices) - -## Adapter Structure and Naming - -### File Naming Convention - -- Use kebab-case for file names: `compute-instance.go`, `big-query-table.go`, `cloud-kms-crypto-key.go` -- Test files should follow the same pattern with `_test.go` suffix: `compute-instance_test.go` - -### Package and Import Structure - -```go -package manual - -import ( - "context" - "fmt" // if using string formatting - "strings" // if parsing paths or URLs - - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute" // Use specific Azure SDK imports - - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/cli/sources" - azureshared "github.com/overmindtech/cli/sources/azure/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" // Only if linking to stdlib resources -) -``` - -### Struct Naming - -- Wrapper struct: `{resourceName}Wrapper` (e.g., `computeInstanceWrapper`, `bigQueryTableWrapper`) -- Use camelCase with first letter lowercase for private structs -- Constructor function: `New{ResourceName}` (e.g., `NewComputeInstance`, `NewBigQueryTable`) - -## Wrapper Type Selection - -Choose the appropriate wrapper interface based on Azure API capabilities: - -### `Wrapper` (GET only) - -Use when the Azure API only supports individual resource retrieval: - -- Resources without list endpoints -- Resources that require specific identifiers for retrieval - -### `ListableWrapper` (GET + LIST) - -Use when the Azure API supports listing all resources in a scope: - -- Compute Virtual Machines (per resource group) -- Compute Disks (per resource group) -- Network Interfaces (per resource group) -- Resources scoped to resource groups or subscriptions - -### `SearchableWrapper` (GET + SEARCH) - -Use when the Azure API supports filtering/searching resources: - -- Resources that can be queried with filters -- Resources that support tag-based or property-based searches - -### `SearchableListableWrapper` (GET + LIST + SEARCH) - -Use when the Azure API supports both listing and searching: - -- Currently not used in existing adapters, but available for complex resources - -## Base Struct Selection - -Choose the appropriate base struct based on Azure resource scope: - -### `MultiResourceGroupBase` - Resource Group Scoped Resources (multi-scope) - -Resource-group-scoped adapters use **one adapter per resource type** that holds a slice of resource-group scopes. The engine calls List/Get once per scope; scope is resolved in each method via `ResourceGroupScopeFromScope(scope)`. - -```go -type computeVirtualMachineWrapper struct { - client clients.VirtualMachinesClient - *azureshared.MultiResourceGroupBase -} - -func NewComputeVirtualMachine(client clients.VirtualMachinesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { - return &computeVirtualMachineWrapper{ - client: client, - MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( - resourceGroupScopes, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - azureshared.ComputeVirtualMachine, - ), - } -} -``` - -In Get/List/ListStream/Search, resolve scope to resource group (and subscription when needed) with: - -```go -rgScope, err := c.ResourceGroupScopeFromScope(scope) -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -// use rgScope.ResourceGroup and rgScope.SubscriptionID -``` - -**Examples:** Compute Virtual Machines, Compute Disks, Network Interfaces, Network Security Groups - -### `SubscriptionBase` - Subscription-Level Resources - -For resources scoped to the entire subscription: - -```go -type subscriptionWrapper struct { - client clients.SubscriptionClient - *azureshared.SubscriptionBase -} - -func NewSubscription(client clients.SubscriptionClient, subscriptionID string) sources.ListableWrapper { - return &subscriptionWrapper{ - client: client, - SubscriptionBase: azureshared.NewSubscriptionBase( - subscriptionID, - sdp.AdapterCategory_ADAPTER_CATEGORY_GENERAL, - azureshared.Subscription, - ), - } -} -``` - -**Examples:** Subscriptions, Resource Groups, Management Groups - -## Required Methods Implementation - -### IAM Permissions - -Always implement with specific Azure RBAC permissions: - -```go -func (c computeVirtualMachineWrapper) IAMPermissions() []string { - return []string{ - "Microsoft.Compute/virtualMachines/read", - } -} -``` - -### Predefined Role - -Always implement with the most restrictive Azure built-in role: - -```go -func (c computeVirtualMachineWrapper) PredefinedRole() string { - return "Reader" // or more specific role like "Virtual Machine Contributor" if write access needed -} -``` - -### Potential Links - -Document all possible linked resources: - -```go -func (c computeVirtualMachineWrapper) PotentialLinks() map[shared.ItemType]bool { - return shared.NewItemTypesSet( - stdlib.NetworkIP, - azureshared.ComputeDisk, - azureshared.NetworkNetworkInterface, - azureshared.NetworkVirtualNetwork, - azureshared.NetworkSubnet, - azureshared.ComputeAvailabilitySet, - ) -} -``` - -## Terraform Mappings - -### GET Method (Direct ID Match) - -Use when Terraform resource has a unique identifier that directly matches your adapter's unique attribute: - -```go -func (c computeVirtualMachineWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_machine - TerraformQueryMap: "azurerm_virtual_machine.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine - TerraformQueryMap: "azurerm_linux_virtual_machine.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/windows_virtual_machine - TerraformQueryMap: "azurerm_windows_virtual_machine.name", - }, - } -} -``` - -### SEARCH Method (Multiple Parameters) - -Use when Terraform resource requires multiple parameters or different ID format: - -```go -func (b resourceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_SEARCH, - // https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/example - // Terraform uses: /subscriptions/{{subscription}}/resourceGroups/{{resourceGroup}}/providers/.../{{name}} - // Our adapter uses: resourceGroup + name as separate parameters - TerraformQueryMap: "azurerm_example.id", - }, - } -} -``` - -### Multiple Terraform Mappings - -For resources that can be referenced in multiple ways: - -```go -func (c resourceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "azurerm_resource.name", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "azurerm_resource.resource_id", - }, - } -} -``` - -## Get and Search Lookups - -### Single Key Lookup - -For resources with a single unique identifier: - -```go -var ComputeVirtualMachineLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeVirtualMachine) - -func (c computeVirtualMachineWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ComputeVirtualMachineLookupByName, - } -} -``` - -### Multiple Keys Lookup (Order Matters) - -The order of lookups determines the order of `queryParts` in the `Get` method: - -```go -var ( - ResourceGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ResourceGroup) - VirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) -) - -func (b subnetWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ResourceGroupLookupByName, // First key: resource group (queryParts[0]) - VirtualNetworkLookupByName, // Second key: virtual network (queryParts[1]) - SubnetLookupByName, // Third key: subnet (queryParts[2]) - } -} - -func (b subnetWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - resourceGroup := queryParts[0] // From ResourceGroupLookupByName - vnetName := queryParts[1] // From VirtualNetworkLookupByName - subnetName := queryParts[2] // From SubnetLookupByName - // ... implementation -} -``` - -### Multiple Keys with Composite Lookup - -For complex hierarchical resources: - -```go -var ( - SubscriptionLookupByID = shared.NewItemTypeLookup("id", azureshared.Subscription) - ResourceGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ResourceGroup) - ResourceLookupByName = shared.NewItemTypeLookup("name", azureshared.Resource) -) - -func (c resourceWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - SubscriptionLookupByID, // First key: subscription ID - ResourceGroupLookupByName, // Second key: resource group name - ResourceLookupByName, // Third key: resource name - } -} -``` - -### Search Lookups - -Define search parameters for SearchableWrapper: - -```go -// Single search lookup -func (b resourceWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - ResourceGroupLookupByName, // Search within a specific resource group - }, - } -} - -// Multiple search lookups (different search patterns) -func (c resourceWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - SubscriptionLookupByID, // Search by subscription only - }, - { - SubscriptionLookupByID, // Search within specific resource group - ResourceGroupLookupByName, - }, - } -} -``` - -## Linked Item Queries - -### Basic Pattern - -Always use this pattern for creating linked item queries: - -```go -sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.TargetResourceType.String(), - Method: sdp.QueryMethod_GET, // or SEARCH - Query: "resource-identifier", - Scope: "appropriate-scope", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // This resource affected if linked resource changes - Out: true, // Linked resource affected if this resource changes - }, -}) -``` - -### Resource ID Extraction - -Use helper functions for extracting parameters from Azure resource IDs: - -```go -// Extract resource name from Azure resource ID -// ID: /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/disks/{diskName} -diskName := azureshared.ExtractResourceName(*vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID) -``` - -### Composite Lookup Keys - -Use for multi-parameter queries: - -```go -Query: shared.CompositeLookupKey(resourceGroup, resourceName) -``` - -### Blast Propagation Rules - -- **In: true, Out: true** - Tightly coupled resources (parent-child relationships) -- **In: true, Out: false** - This resource depends on linked resource (e.g., disk depends on image) -- **In: false, Out: true** - Linked resource depends on this resource (e.g., instance depends on disk) - -### Common Linked Resource Patterns - -#### Network Resources - -```go -// Extract network interface name from Azure resource ID -if vm.Properties.NetworkProfile != nil && len(vm.Properties.NetworkProfile.NetworkInterfaces) > 0 { - for _, nicRef := range vm.Properties.NetworkProfile.NetworkInterfaces { - if nicRef.ID != nil { - nicName := azureshared.ExtractResourceName(*nicRef.ID) - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.NetworkNetworkInterface.String(), - Method: sdp.QueryMethod_GET, - Query: nicName, - Scope: scope, // Network interfaces are scoped to resource group - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } - } -} -``` - -#### Disk Resources - -```go -// Extract disk name from Azure resource ID -if vm.Properties.StorageProfile != nil && vm.Properties.StorageProfile.OSDisk != nil { - if vm.Properties.StorageProfile.OSDisk.ManagedDisk != nil && vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID != nil { - diskName := azureshared.ExtractResourceName(*vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID) - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.ComputeDisk.String(), - Method: sdp.QueryMethod_GET, - Query: diskName, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -## Error Handling - -### Standard Error Pattern - -Always use source-specific error wrapping: - -```go -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -``` - -### Pager Pattern - -Handle Azure SDK pagers consistently. For multi-scope resource group adapters, resolve scope first: - -```go -rgScope, err := c.ResourceGroupScopeFromScope(scope) -if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) -} -pager := c.client.NewListPager(rgScope.ResourceGroup, nil) -for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return nil, azureshared.QueryError(err, scope, c.Type()) - } - for _, item := range page.Value { - // Process item... - } -} -``` - -### Stream Error Handling - -For streaming operations: - -```go -if err != nil { - stream.SendError(azureshared.QueryError(err, scope, c.Type())) - return -} - -// For item processing errors, continue with next item -if sdpErr != nil { - stream.SendError(sdpErr) - continue -} -``` - -## Testing Patterns - -### Test Structure - -Follow this exact pattern for all adapter tests: - -```go -func TestComputeVirtualMachine(t *testing.T) { - ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockVirtualMachinesClient(ctrl) - subscriptionID := "test-subscription-id" - resourceGroup := "test-resource-group" - - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewComputeVirtualMachine(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) - vm := createAzureVirtualMachine("test-vm", "Succeeded") - mockClient.EXPECT().Get(ctx, resourceGroup, "test-vm", nil).Return(armcompute.VirtualMachinesClientGetResponse{ - VirtualMachine: *vm, - }, nil) - - adapter := sources.WrapperToAdapter(wrapper) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-vm", true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: azureshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-disk", - ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // ... more test cases - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) - }) - - t.Run("List", func(t *testing.T) { - // Test list functionality - }) - - t.Run("Search", func(t *testing.T) { - // Test search functionality (if applicable) - }) -} -``` - -### Required Test Cases - -- **Get method**: Test successful retrieval and error cases -- **StaticTests**: Test linked item queries using `shared.RunStaticTests` -- **HealthCheck**: Test health status mapping (if applicable) -- **List method**: Test the List method (for ListableWrapper) -- **Search method**: Test the Search method (for SearchableWrapper) -- **Interface compliance**: Verify adapter implements correct interfaces - -### Test Helper Functions - -Create helper functions for test data: - -```go -func createAzureVirtualMachine(vmName string, provisioningState string) *armcompute.VirtualMachine { - return &armcompute.VirtualMachine{ - Name: ptr.To(vmName), - Tags: map[string]*string{ - "env": ptr.To("test"), - }, - Properties: &armcompute.VirtualMachineProperties{ - ProvisioningState: ptr.To(provisioningState), - // ... other fields - }, - } -} -``` - -### Mock Expectations - -Use gomock for client mocking: - -```go -mockClient := mocks.NewMockVirtualMachinesClient(ctrl) -mockClient.EXPECT().Get(ctx, resourceGroup, vmName, nil).Return(expectedResult, nil) -``` - -## Client Interface Patterns - -### Typed Client Interfaces - -Define typed client interfaces in the clients package: - -```go -// In sources/azure/clients/virtual-machines-client.go -type VirtualMachinesClient interface { - Get(ctx context.Context, resourceGroupName string, vmName string, options *armcompute.VirtualMachinesClientGetOptions) (armcompute.VirtualMachinesClientGetResponse, error) - NewListPager(resourceGroupName string, options *armcompute.VirtualMachinesClientListOptions) *runtime.Pager[armcompute.VirtualMachinesClientListResponse] -} - -// Constructor function -func NewVirtualMachinesClient(client *armcompute.VirtualMachinesClient) VirtualMachinesClient { - return &virtualMachinesClientAdapter{client: client} -} -``` - -### Client Implementation - -Implement the interface with proper error handling: - -```go -type virtualMachinesClientAdapter struct { - client *armcompute.VirtualMachinesClient -} - -func (c *virtualMachinesClientAdapter) Get(ctx context.Context, resourceGroupName string, vmName string, options *armcompute.VirtualMachinesClientGetOptions) (armcompute.VirtualMachinesClientGetResponse, error) { - return c.client.Get(ctx, resourceGroupName, vmName, options) -} - -func (c *virtualMachinesClientAdapter) NewListPager(resourceGroupName string, options *armcompute.VirtualMachinesClientListOptions) *runtime.Pager[armcompute.VirtualMachinesClientListResponse] { - return c.client.NewListPager(resourceGroupName, options) -} -``` - -## Common Gotchas and Best Practices - -### 1. Unique Attribute Consistency - -Ensure the `UniqueAttribute` in the SDP item matches the lookup key: - -```go -// If using composite lookup key -err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) -sdpItem := &sdp.Item{ - Type: azureshared.ComputeVirtualMachine.String(), - UniqueAttribute: "uniqueAttr", // Must match the attribute name above - // ... -} -``` - -### 2. Scope Handling - -Use appropriate scope helper functions: - -```go -// For resource group scoped resources -scope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroup) - -// For subscription-level resources -scope := subscriptionID -``` - -### 3. Attributes and Tags - -Convert Azure SDK structs to attributes and tags: - -```go -attributes, err := shared.ToAttributesWithExclude(vm, "tags") -tags := convertAzureTags(vm.Tags) // Convert map[string]*string to map[string]string -``` - -### 4. Health Status Mapping - -Map Azure resource provisioning states to SDP health statuses: - -```go -switch *vm.Properties.ProvisioningState { -case "Succeeded": - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() -case "Creating", "Updating", "Deleting": - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() -case "Failed", "Canceled": - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() -default: - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() -} -``` - -### 5. Multiple Query Parameters - -For resources requiring multiple parameters: - -```go -func (b resourceWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - resourceGroup := queryParts[0] // First parameter - resourceName := queryParts[1] // Second parameter - // ... implementation -} -``` - -### 6. Conditional Linked Queries - -Only create linked queries when the referenced resource exists: - -```go -if vm.Properties.StorageProfile != nil && vm.Properties.StorageProfile.OSDisk != nil { - if vm.Properties.StorageProfile.OSDisk.ManagedDisk != nil && vm.Properties.StorageProfile.OSDisk.ManagedDisk.ID != nil { - // Create disk linked query - } -} -``` - -### 7. Resource ID Validation - -Always validate extracted resource IDs: - -```go -if resourceID != nil && *resourceID != "" { - resourceName := azureshared.ExtractResourceName(*resourceID) - if resourceName != "" { - // Use the extracted resource name - } -} -``` - -### 8. Adapter Registration - -Resource-group-scoped adapters are registered once per type with a slice of all resource group scopes. Build `resourceGroupScopes` after discovering resource groups, then pass it to each constructor: - -```go -// Build resource group scopes from discovered resource groups -resourceGroupScopes := make([]azureshared.ResourceGroupScope, 0, len(resourceGroups)) -for _, rg := range resourceGroups { - resourceGroupScopes = append(resourceGroupScopes, azureshared.NewResourceGroupScope(subscriptionID, rg)) -} - -// Multi-scope resource group adapters (one adapter per type handling all resource groups) -if len(resourceGroupScopes) > 0 { - adapters = append(adapters, - sources.WrapperToAdapter(NewComputeVirtualMachine( - clients.NewVirtualMachinesClient(vmClient), - resourceGroupScopes, - ), cache), - sources.WrapperToAdapter(NewStorageAccount(..., resourceGroupScopes), cache), - // ... one line per resource type (33 total) - ) -} - -// Subscription-level adapters are registered separately with subscriptionID -adapters = append(adapters, - sources.WrapperToAdapter(NewSubscription( - clients.NewSubscriptionClient(subClient), - subscriptionID, - ), cache), -) -``` - -### 9. Documentation Comments - -Always include Azure REST API documentation URLs in comments: - -```go -// Get retrieves a virtual machine by its name -// Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machines/get -// GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName} -``` - -### 10. Error Message Consistency - -Use consistent error messages and include context: - -```go -return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid virtual machine name: %s", vmName), -} -``` - -## Validation Checklist - -Before submitting a new adapter, ensure: - -- [ ] File follows naming convention (`{resource-name}.go`) -- [ ] Imports are properly organized and minimal -- [ ] Wrapper type matches Azure API capabilities -- [ ] Base struct matches resource scope (MultiResourceGroupBase/Subscription) -- [ ] All required methods implemented (IAMPermissions, PredefinedRole, PotentialLinks) -- [ ] Terraform mappings are correct and include documentation URLs -- [ ] Get/Search lookups match the resource's query parameters -- [ ] Linked item queries use proper blast propagation -- [ ] Error handling uses `azureshared.QueryError` -- [ ] Pager pattern is correctly implemented for list operations -- [ ] Health status mapping is implemented (if applicable) -- [ ] Test file covers all required test cases -- [ ] Static tests validate linked item queries -- [ ] Client interface is properly defined in clients package -- [ ] Adapter is registered in the main adapters file -- [ ] Documentation comments include Azure REST API URLs -- [ ] Unique attribute consistency is maintained -- [ ] Scope handling uses appropriate helper functions -- [ ] Resource ID extraction is validated -- [ ] Conditional linked queries are properly implemented - -## Examples Reference - -For complete examples, refer to these existing adapters: - -- **Simple ListableWrapper**: `sources/azure/manual/compute-virtual-machine.go` -- **Resource Group Scoped Resource**: `sources/azure/manual/compute-virtual-machine.go` -- **Subscription-level Resource**: (to be added) -- **Complex Linked Queries**: (to be added) - -These examples demonstrate all the patterns and best practices outlined in this document. diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc deleted file mode 100644 index 7709476c..00000000 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-creation.mdc +++ /dev/null @@ -1,477 +0,0 @@ ---- -description: "GCP Dynamic Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# Dynamic Adapter Creation Rules - -## Overview - -When creating new GCP dynamic adapters in the Overmind codebase, follow these patterns and requirements to ensure consistency, correctness, and maintainability. - -## Core Principles - -- Always use the `registerableAdapter` struct pattern (NOT legacy `SDPAssetTypeToAdapterMeta`) -- Implement proper scope detection (project/regional/zonal) -- Include comprehensive link rules analysis (attribute key → linked item type) -- Add proper terraform mapping support -- Follow naming conventions and IAM permissions -- Research from official API documentation - -## Implementation Steps - -### 1. Resource Analysis - -#### Understand the Resource -- **Add clear description**: What does this resource do? Add a descriptive comment at the top of the adapter -- **Research the API**: Study the official GCP API documentation thoroughly -- **Identify use cases**: How is this resource typically used in GCP infrastructure? - -#### Determine Scope -- **Project-level**: URLs with `/global/` or no location parameters - - Examples: `compute-global-forwarding-rule.go`, `monitoring-alert-policy.go` -- **Regional**: URLs with `/regions/{region}/` - - Examples: `dataproc-cluster.go`, `run-service.go` -- **Zonal**: URLs with `/zones/{zone}/` - - Examples: Check existing zonal adapters in the directory - -#### Identify Unique Attributes -- Usually the resource name in the URL path -- Extract from API URL path segments -- Consider multiple unique attributes for complex resources - -### 2. Adapter Structure - -Use the `registerableAdapter` struct pattern with these key components: - -```go -var YourResourceAdapter = ®isterableAdapter{ - SDPItemType: gcpshared.YourItemType, - Category: sdp.AdapterCategory_ADAPTER_CATEGORY_YOUR_CATEGORY, - Scope: gcpshared.ScopeYourScope, - UniqueAttributes: []string{"yourUniqueAttribute"}, - IAMPermissions: []string{"api.resource.get", "api.resource.list"}, - TerraformResource: "google_your_resource", - // ... other fields -} -``` - -#### Key Components to Determine: - -- **SDP Item Type**: Define in `gcpshared` package (follow existing naming patterns) -- **Category**: Choose appropriate `sdp.AdapterCategory` - see `sdp-go/account.pb.go` for accurate category definitions and descriptions -- **Scope**: `gcpshared.ScopeProject`, `gcpshared.ScopeRegional`, or `gcpshared.ScopeZonal` -- **Unique Attributes**: Extract from API URL path segments -- **IAM Permissions**: Research from official API documentation -- **Terraform Resource**: Find correct resource name in Terraform registry - -### 3. Endpoint Functions - -Choose the appropriate endpoint function based on scope: - -#### Project-level Resources - -**Single Query Parameter:** -```go -GetFunc: ProjectLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/resources/{resource}", - "resource", -), -ListFunc: ProjectLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/resources", -), -``` - -**Two Query Parameters (location + resource):** -```go -GetFunc: ProjectLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/locations/{location}/resources/{resource}", -), -SearchFunc: ProjectLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/locations/{location}/resources", -), -UniqueAttributeKeys: []string{"locations", "resources"}, -``` -*Examples: `cloudfunctions-function.go`, `run-service.go`, `file-instance.go`, `dataplex-data-scan.go`* - -#### Regional Resources - -**Single Query Parameter:** -```go -GetFunc: RegionalLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/resources/{resource}", - "resource", -), -ListFunc: RegionLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/resources", -), -``` - -**Multiple Query Parameters:** -```go -GetFunc: RegionalLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/regions/{region}/subresources/{subresource}/resources/{resource}", -), -UniqueAttributeKeys: []string{"subresources", "resources"}, -``` - -#### Zonal Resources - -**Single Query Parameter:** -```go -GetFunc: ZoneLevelEndpointFuncWithSingleQuery( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/resources/{resource}", - "resource", -), -ListFunc: ZoneLevelListFunc( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/resources", -), -``` - -**Multiple Query Parameters:** -```go -GetFunc: ZoneLevelEndpointFuncWithTwoQueries( - "https://api.googleapis.com/v1/projects/{project}/zones/{zone}/subresources/{subresource}/resources/{resource}", -), -UniqueAttributeKeys: []string{"subresources", "resources"}, -``` - -### 4. List vs Search Decision - -#### Use List if: -- No additional parameters needed to list all resources in scope -- Simple resource listing without complex filtering - -#### Use Search if: -- Additional parameters (like location) required -- Multiple unique attributes -- Complex filtering needed - -#### No-op Search if: -- Terraform mapping needs SEARCH but adapter doesn't support it -- See `orgpolicy-policy.go` for example - -### 5. Link Rules Analysis - -For each attribute that references another resource: - -#### Ask the Right Questions: -- **IN**: "What happens if the linked resource is deleted/updated?" -- **OUT**: "What happens to the linked resource if this resource is updated/deleted?" - -#### Implementation: -```go -linkRules: map[string]*gcpshared.Impact{ - "network": { - ToSDPItemType: gcpshared.ComputeNetwork, - Description: "This resource is affected if network changes. Network is not affected if this resource changes.", - }, - "subnet": { - ToSDPItemType: gcpshared.ComputeSubnetwork, - Description: "This resource is affected if subnet changes. Subnet is affected if this resource changes.", - }, -}, -``` - -#### Special Cases: -- **Use dot notation**: `"config.networkUri"` for nested attributes -- **If no adapter exists**: Create the SDP item type definition in `sources/gcp/shared/item-types.go` and `models.go` as if the adapter exists, then link to it -- **Follow naming**: `gcp--` for new adapter types - -#### Creating Backlinks from Child to Parent Resources - -When a resource name contains hierarchical information (e.g., a database name includes its parent instance), you can create a backlink using the `name` field. The framework will automatically extract the parent resource identifier and create the appropriate linked item query. - -**Use Case**: Child resources that reference their parent in their name structure. - -**Example: Spanner Database to Instance Backlink** - -A Spanner database name has the format: `projects/{project}/instances/{instance}/databases/{database}` - -To create a backlink from the database to its parent instance: - -```go -// In sources/gcp/shared/link-rules.go (or in the adapter's linkRules map) -SpannerDatabase: { - // ... other link rules ... - - // This is a backlink to instance. - // Framework will extract the instance name and create the linked item query with GET - "name": { - Description: "If the Spanner Instance is deleted or updated: The Database may become invalid or inaccessible. If the Database is updated: The instance remains unaffected.", - ToSDPItemType: SpannerInstance, - }, -} -``` - -**How It Works:** -1. The framework reads the `name` field value (e.g., `projects/my-project/instances/test-instance/databases/my-db`) -2. It extracts the parent instance identifier (`test-instance`) -3. It creates a GET query for the parent SpannerInstance -4. The linked item query will have: - - Type: `gcp-spanner-instance` - - Method: GET - - Query: `test-instance` (extracted from database name) - - Scope: Same project as the database - -**Testing the Backlink:** - -When adding backlink link rules, verify they work correctly in tests: - -```go -// In adapter_test.go -{ - // name field creates a backlink to the Spanner instance - ExpectedType: gcpshared.SpannerInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: instanceName, - ExpectedScope: projectID, -} -``` - -**When to Use This Pattern:** -- Child resources with hierarchical naming (database → instance, subnet → network) -- Parent resource is part of the child's name/path -- Relationship is one-way (child depends on parent, but parent doesn't depend on specific child) -- Use in-only impact (parent changes affect child, but not vice versa) - -#### Creating Forward Links from Parent to Child Resources - -When a parent resource needs to link to all its child resources, you can use the `IsParentToChild` flag to create a forward link using SEARCH. This is the inverse pattern of the backlink described above. - -**Use Case**: Parent resources that need to discover and link to all their child resources (e.g., instance → all databases). - -**Example: Spanner Instance to Databases Forward Link** - -A Spanner instance needs to link to all databases that belong to it. Since the instance doesn't have the database names, we use SEARCH to find all databases for that instance. - -To create a forward link from the instance to its databases: - -```go -// In the adapter file (e.g., spanner-instance.go) -var spannerInstanceAdapter = registerableAdapter{ - // ... other fields ... - - linkRules: map[string]*gcpshared.Impact{ - // This a link from parent to child via SEARCH - // We need to make sure that the linked item supports `SEARCH` method for the `instance` name. - "name": { - ToSDPItemType: gcpshared.SpannerDatabase, - Description: "If the Spanner Instance is deleted or updated: All associated databases may become invalid or inaccessible. If a database is updated: The instance remains unaffected.", - IsParentToChild: true, - }, - }, -} -``` - -**How It Works:** -1. The framework detects `IsParentToChild: true` on the link rule -2. It modifies the unique attribute keys by removing the last element (the child identifier) - - For databases: `["instances", "databases"]` becomes `["instances"]` -3. It extracts only the parent identifier from the resource name (e.g., `test-instance`) -4. It creates a SEARCH query (not GET) to find all child resources -5. The linked item query will have: - - Type: `gcp-spanner-database` - - Method: SEARCH - - Query: `test-instance` (the instance name) - - Scope: Same project as the instance - -**Testing the Forward Link:** - -When adding parent-to-child link rules, verify they work correctly in tests: - -```go -// In adapter_test.go (e.g., spanner-instance_test.go) -{ - ExpectedType: gcpshared.SpannerDatabase.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: instanceName, - ExpectedScope: projectID, -} -``` - -**Critical Requirements:** -- **Child adapter must support SEARCH**: The child resource type must implement the SEARCH method -- **Query parameter must match**: The child's SEARCH method must accept the parent identifier as a search parameter -- **Set `IsParentToChild: true`**: This flag triggers the special SEARCH behavior - -**When to Use This Pattern:** -- Parent resources that need to discover all their children (instance → databases, network → subnets) -- The parent's name/identifier is sufficient to search for children -- The child adapter supports SEARCH method - -**Comparison: Backlink vs Forward Link** - -| Aspect | Backlink (Child → Parent) | Forward Link (Parent → Child) | -|--------|---------------------------|------------------------------| -| Direction | Child points to parent | Parent discovers children | -| Method | GET | SEARCH | -| Flag | No special flag | `IsParentToChild: true` | -| Use Case | Database → Instance | Instance → All Databases | -| Query | Single parent identifier | Parent identifier searches all children | - -### 6. Special Considerations - -#### NameSelector -- Use if resource doesn't have name attribute -- See `dataproc-cluster.go` for example - -#### Health Status -- Add TODO for status/state fields: `"TODO: https://linear.app/overmind/issue/ENG-631"` - -#### InDevelopment -- **DO NOT USE**: This flag is reserved for human authors only -- Agents should never set `InDevelopment: true` - -#### IAM Permissions -- Research from official API documentation -- Most APIs provide required permissions in their reference docs -- Common patterns: - - `"api.resource.get"` for individual resource access - - `"api.resource.list"` for listing resources - -#### Custom Role for Additional Permissions -When creating adapters that require permissions not available in GCP predefined roles with limited read-only access, you may need to update the Overmind custom role. - -**Current Custom Role Permissions:** -```hcl -resource "google_project_iam_custom_role" "overmind_custom_role" { - role_id = "overmindCustomRole" - title = "Overmind Custom Role" - description = "Custom role for Overmind service account with additional BigQuery and Spanner permissions" - permissions = [ - "bigquery.transfers.get", # BigQuery transfer configuration discovery - "spanner.databases.get", # Spanner database detail discovery - "spanner.databases.list", # Spanner database enumeration - ] -} -``` - -**When to Update the Custom Role:** -- If you cannot find a suitable predefined role that provides read-only access to your resource -- If the existing predefined roles have overly broad permissions (e.g., include data access) -- If you need specific permissions that are not included in any predefined role - -**Process for Adding New Permissions:** -1. Research the exact IAM permissions needed for your adapter from GCP API documentation -2. Verify that no predefined role exists with the required permissions -3. Add the new permissions to the custom role in these locations: - - `deploy/sources.tf` (infrastructure configuration) - - `docs.overmind.tech/docs/sources/gcp/configuration.md` (documentation) - - `frontend/src/features/settings/sources/details/gcp-scripts.ts` (customer scripts) -4. Update the roles reference table in documentation -5. Test the permissions work correctly with your adapter - -**Note:** The custom role is assigned separately from predefined roles to avoid Terraform `for_each` issues with apply-time computed values. Each service account gets both predefined roles (via `for_each`) and the custom role (via individual resource blocks). - -### 7. Terraform Mapping - -#### Research Requirements: -- Find correct resource name in Terraform registry: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/ -- Determine if method should be GET or SEARCH -- Match terraform mapping method to adapter capabilities - -#### Example: -```go -TerraformResource: "google_compute_global_forwarding_rule", -TerraformMethod: sdp.QueryMethod_GET, -``` - -### 8. API Service Enablement - -#### Ensure Required APIs Are Enabled: -- **CRITICAL**: All GCP APIs used by the adapter must be enabled in the deployment configuration -- Check if the API service is already included in `deploy/modules/ovm-services/gke.tf` -- If the API is missing, add it to the `google_project_service` resource - -#### Process: -1. **Identify API Service**: Extract the API service name from the adapter's endpoint URLs - - Example: `https://bigquerydatatransfer.googleapis.com/v1/...` → `bigquerydatatransfer.googleapis.com` -2. **Check Current APIs**: Review the list in `deploy/modules/ovm-services/gke.tf` -3. **Add Missing APIs**: If not present, add the API to the `overmind_apis` resource -4. **Follow Naming Convention**: Use the full API endpoint URL format -5. **Add Descriptive Comments**: Include a clear comment explaining what the API is for - -#### Example: -```hcl -resource "google_project_service" "overmind_apis" { - for_each = toset([ - "bigquerydatatransfer.googleapis.com", # BigQuery Data Transfer API - "new-api.googleapis.com", # New API for your adapter - // ... other APIs - ]) -} -``` - -#### Verification: -- Ensure all IAM permissions used in the adapter have their corresponding APIs enabled -- Verify against the `PredefinedRoles` map in `sources/gcp/shared/predefined-roles.go` -- All APIs referenced in predefined roles should be enabled in the deployment - -## Reference Examples - -### Project-level Resources -- `compute-global-forwarding-rule.go` -- `monitoring-alert-policy.go` - -### Regional Resources -- `dataproc-cluster.go` -- `run-service.go` - -### Zonal Resources -- Check existing zonal adapters in the directory - -### Special Patterns -- **Multiple Unique Attributes**: Check adapters with complex URL structures -- **No-op Search**: `orgpolicy-policy.go` -- **NameSelector**: `dataproc-cluster.go` -- **Complex link rules**: `dataproc-cluster.go` - -## Files to Create/Modify - -### Required Files -- `sources/gcp/dynamic/adapters/{name}.go` (main adapter file) - -### Optional Files (if new SDP item type needed) -- `sources/gcp/shared/item-types.go` (add new item type) -- `sources/gcp/shared/models.go` (add new model) - -## Important Patterns - -### Always Use: -- `registerableAdapter` struct, never legacy maps -- Endpoint functions that match the exact API URL format -- Clear and specific link rule descriptions -- Terraform mapping method that matches adapter capabilities -- IAM permissions that match the actual API requirements - -### Never Use: -- Legacy `SDPAssetTypeToAdapterMeta` pattern -- `InDevelopment: true` (reserved for human authors) -- Assumptions about protobuf types or API structure - -## Validation Checklist - -- [ ] Adapter file compiles without errors -- [ ] Proper scope detection and endpoint functions -- [ ] Comprehensive link rules analysis -- [ ] Valid terraform mapping with correct method (GET/SEARCH) -- [ ] Appropriate IAM permissions defined -- [ ] Custom role updated if new permissions required (see Custom Role section) -- [ ] Clear resource description and comments -- [ ] Follows existing adapter patterns -- [ ] No legacy `SDPAssetTypeToAdapterMeta` usage -- [ ] API URLs match official GCP documentation exactly -- [ ] Link rules cover all linked resources -- [ ] Terraform resource name verified in registry -- [ ] **Required APIs enabled in deployment**: All APIs used by the adapter are included in `deploy/modules/ovm-services/gke.tf` - -## Key Resources - -- **GCP API Documentation**: Always verify against official docs -- **Terraform Registry**: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/ -- **Existing Adapters**: Study patterns in `sources/gcp/dynamic/adapters/` -- **SDP Types**: Check `sources/gcp/shared/item-types.go` for existing types -- **Deployment Configuration**: Check `deploy/modules/ovm-services/gke.tf` for enabled APIs -- **Predefined Roles**: Review `sources/gcp/shared/predefined-roles.go` for IAM permissions diff --git a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc b/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc deleted file mode 100644 index 86faf744..00000000 --- a/sources/gcp/dynamic/adapters/.cursor/rules/dynamic-adapter-testing.mdc +++ /dev/null @@ -1,622 +0,0 @@ ---- -description: "GCP Dynamic Adapter unit testing patterns and standards" -globs: **/*_test.go ---- - -# Dynamic Adapter Unit Testing Rules - -## Overview - -When writing unit tests for GCP dynamic adapters in the Overmind codebase, follow these patterns and requirements to ensure consistency and correctness. - -## Package and Imports - -- **Package**: Always use `package adapters_test` (never `package adapters` or `package main`) -- **Required Imports**: - - ```go - import ( - "context" - "fmt" - "net/http" - "testing" - - // IMPORTANT: Import the correct protobuf package that matches the adapter's API endpoint - // Examples: - // "cloud.google.com/go/compute/apiv1/computepb" // Compute API v1 - // "cloud.google.com/go/functions/apiv2/functionspb" // Cloud Functions v2 - // "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" // AI Platform v1 - // "cloud.google.com/go/bigtable/admin/apiv2/adminpb" // BigTable Admin v2 - "cloud.google.com/go/yourservice/apiv1/yourservicepb" // Replace with actual service - - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/cli/sources/gcp/dynamic" - gcpshared "github.com/overmindtech/cli/sources/gcp/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" - ) - ``` - -## Protobuf Types - -- **CRITICAL: Match API Version and Package**: Always use the correct protobuf package and API version that matches the adapter's endpoint - - **Step 1**: Check the adapter file (e.g., `cloudfunctions-function.go`) for the API endpoint URL - - **Step 2**: Extract the API service and version from the URL: - - `https://compute.googleapis.com/compute/v1/...` → `cloud.google.com/go/compute/apiv1/computepb` - - `https://cloudfunctions.googleapis.com/v2/...` → `cloud.google.com/go/functions/apiv2/functionspb` - - `https://aiplatform.googleapis.com/v1/...` → `cloud.google.com/go/aiplatform/apiv1/aiplatformpb` - - `https://bigtableadmin.googleapis.com/v2/...` → `cloud.google.com/go/bigtable/admin/apiv2/adminpb` - - **Step 3**: Import the matching protobuf package and use its types (NOT generic `computepb`) - - **Example**: If adapter uses `https://cloudfunctions.googleapis.com/v2/...`, use `apiv2/functionspb`, NOT `apiv1` or `apiv2beta` -- **Verify Types**: Check available types with `go doc | grep -i "type.*YourResource"` - - Example: `go doc cloud.google.com/go/compute/apiv1/computepb | grep -i "type.*Address"` -- **Verify List Response Structure**: Check the actual list response type - field names vary by service - - Example: `computepb.AddressList` has `.Items` field - - Example: `aiplatformpb.ListModelDeploymentMonitoringJobsResponse` has `.ModelDeploymentMonitoringJobs` field -- **Common Mistakes**: - - Using `computepb` for non-Compute resources (use service-specific package) - - Assuming `GlobalAddress` exists (use `Address` instead) - - Not verifying the list response field name (it's not always `.Items`) -- **Always verify**: Don't assume protobuf types or field names exist - check the actual API package - -## Test Structure Template - -### Single Query Parameter Resources - -```go -func TestYourResource(t *testing.T) { - ctx := context.Background() - projectID := "test-project" - linker := gcpshared.NewLinker() - resourceName := "test-resource" - - // IMPORTANT: Use the correct protobuf package that matches your adapter's API endpoint - // Examples: - // - Compute API v1: computepb.Address - // - Functions API v2: functionspb.Function - // - AI Platform v1: aiplatformpb.Model - // Check the adapter file for the API endpoint URL and match the protobuf version! - - // Create mock protobuf object (replace YourServicepb with actual service package) - resource := &YourServicepb.YourResource{ - Name: &resourceName, - // ... other fields using pointer helpers - } - - // Create second resource for list testing - resourceName2 := "test-resource-2" - resource2 := &YourServicepb.YourResource{ - Name: &resourceName2, - // ... other fields using pointer helpers - } - - // Create list response with multiple items - // IMPORTANT: List response structure varies by service - verify the actual type and field names! - // Examples: - // - computepb.AddressList has .Items field - // - aiplatformpb.ListModelsResponse has .Models field - resourceList := &YourServicepb.YourResourceList{ - Items: []*YourServicepb.YourResource{resource, resource2}, // Field name may vary! - } - - sdpItemType := gcpshared.YourItemType - - // Mock HTTP responses - expectedCallAndResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName): { - StatusCode: http.StatusOK, - Body: resource, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName2): { - StatusCode: http.StatusOK, - Body: resource2, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources", projectID): { - StatusCode: http.StatusOK, - Body: resourceList, - }, - } - - t.Run("Get", func(t *testing.T) { - // Test Get functionality with StaticTests - }) - - t.Run("Search", func(t *testing.T) { - // Test Search functionality (for location-based resources) - // OR use List for project-level resources - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test error responses (e.g., 404 Not Found) - }) -} -``` - -### Multiple Query Parameter Resources (e.g., location + resource) - -```go -func TestLocationBasedResource(t *testing.T) { - ctx := context.Background() - projectID := "test-project" - linker := gcpshared.NewLinker() - location := "us-central1" - resourceName := "test-resource" - - // IMPORTANT: Use the correct protobuf package that matches your adapter's API endpoint - // Check the adapter file for the API endpoint URL and match the service and version! - - // Create mock protobuf object (replace YourServicepb with actual service package) - resource := &YourServicepb.YourResource{ - Name: &resourceName, - // ... other fields using pointer helpers - } - - // Create second resource for list testing - resourceName2 := "test-resource-2" - resource2 := &YourServicepb.YourResource{ - Name: &resourceName2, - // ... other fields using pointer helpers - } - - // Create list response with multiple items - // IMPORTANT: List response structure varies - verify the actual type and field names! - resourceList := &YourServicepb.YourResourceList{ - Items: []*YourServicepb.YourResource{resource, resource2}, // Field name may vary! - } - - sdpItemType := gcpshared.YourItemType - - // Mock HTTP responses for location-based resources - expectedCallAndResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName): { - StatusCode: http.StatusOK, - Body: resource, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName2): { - StatusCode: http.StatusOK, - Body: resource2, - }, - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources", projectID, location): { - StatusCode: http.StatusOK, - Body: resourceList, - }, - } - - // Test Get with location + resource name - t.Run("Get", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - // For multiple query parameters, use the combined query format - combinedQuery := fmt.Sprintf("%s/%s", location, resourceName) - sdpItem, err := adapter.Get(ctx, projectID, combinedQuery, true) - if err != nil { - t.Fatalf("Failed to get resource: %v", err) - } - - // Validate SDP item properties - if sdpItem.GetType() != sdpItemType.String() { - t.Errorf("Expected type %s, got %s", sdpItemType.String(), sdpItem.GetType()) - } - if sdpItem.UniqueAttributeValue() != combinedQuery { - t.Errorf("Expected unique attribute value '%s', got %s", combinedQuery, sdpItem.UniqueAttributeValue()) - } - }) - - // Test Search (location-based resources typically use Search instead of List) - t.Run("Search", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - // Test location-based search - sdpItems, err := searchable.Search(ctx, projectID, location, true) - if err != nil { - t.Fatalf("Failed to search resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test with error responses to simulate API errors - errorResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/locations/%s/resources/%s", projectID, location, resourceName): { - StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Resource not found"}, - }, - } - - httpCli := shared.NewMockHTTPClientProvider(errorResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - combinedQuery := shared.CompositeLookupKey(location, resourceName) - _, err = adapter.Get(ctx, projectID, combinedQuery, true) - if err == nil { - t.Error("Expected error when getting non-existent resource, but got nil") - } - }) -} -``` - -## Required Test Functions (Limit to These 3 Only) - -### 1. Get Test (MUST include StaticTests) - -```go -t.Run("Get", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - sdpItem, err := adapter.Get(ctx, projectID, resourceName, true) - if err != nil { - t.Fatalf("Failed to get resource: %v", err) - } - - // Validate SDP item properties - if sdpItem.GetType() != sdpItemType.String() { - t.Errorf("Expected type %s, got %s", sdpItemType.String(), sdpItem.GetType()) - } - if sdpItem.UniqueAttributeValue() != resourceName { - t.Errorf("Expected unique attribute value '%s', got %s", resourceName, sdpItem.UniqueAttributeValue()) - } - if sdpItem.GetScope() != projectID { - t.Errorf("Expected scope '%s', got %s", projectID, sdpItem.GetScope()) - } - - // Validate specific attributes - val, err := sdpItem.GetAttributes().Get("name") - if err != nil { - t.Fatalf("Failed to get 'name' attribute: %v", err) - } - if val != resourceName { - t.Errorf("Expected name field to be '%s', got %s", resourceName, val) - } - - // Include static tests - MUST cover ALL linked resources the adapter produces - t.Run("StaticTests", func(t *testing.T) { - // CRITICAL: Review the adapter's linked-resource map and create - // test cases for EVERY linked item type the adapter produces. - // Check the adapter file (e.g., compute-global-address.go) for all linked resource types. - // IMPORTANT: Always use Item types with .String() for ExpectedType (e.g., gcpshared.ComputeNetwork.String()) - // NEVER use pure strings (e.g., "gcp-compute-network") for ExpectedType - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.LinkedResourceType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "linked-resource-name", - ExpectedScope: projectID, - }, - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) -}) -``` - -### 2. List or Search Test (Choose based on adapter type) - -```go -// For location-based resources (use Search) -t.Run("Search", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - sdpItems, err := searchable.Search(ctx, projectID, location, true) - if err != nil { - t.Fatalf("Failed to search resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } -}) - -// OR for project-level resources (use List) -t.Run("List", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - listable, ok := adapter.(discovery.ListableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement ListableAdapter", sdpItemType) - } - - sdpItems, err := listable.List(ctx, projectID, true) - if err != nil { - t.Fatalf("Failed to list resources: %v", err) - } - - if len(sdpItems) != 2 { - t.Errorf("Expected 2 resources, got %d", len(sdpItems)) - } -}) -``` - -### 3. ErrorHandling Test - -```go -t.Run("ErrorHandling", func(t *testing.T) { - // Test with error responses to simulate API errors - errorResponses := map[string]shared.MockResponse{ - fmt.Sprintf("https://api.googleapis.com/v1/projects/%s/resources/%s", projectID, resourceName): { - StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Resource not found"}, - }, - } - - httpCli := shared.NewMockHTTPClientProvider(errorResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - _, err = adapter.Get(ctx, projectID, resourceName, true) - if err == nil { - t.Error("Expected error when getting non-existent resource, but got nil") - } -}) -``` - -### 4. Search with Terraform Format Test (if adapter has terraform mappings with Search method) - -```go -t.Run("Search with Terraform format", func(t *testing.T) { - httpCli := shared.NewMockHTTPClientProvider(expectedCallAndResponses) - adapter, err := dynamic.MakeAdapter( sdpItemType, linker, httpCli, projectID) - if err != nil { - t.Fatalf("Failed to create adapter for %s: %v", sdpItemType, err) - } - - searchable, ok := adapter.(discovery.SearchableAdapter) - if !ok { - t.Skipf("Adapter for %s does not implement SearchableAdapter", sdpItemType) - } - - // Test Terraform format: projects/[project_id]/locations/[location]/resourceType/[resource_id] - // The adapter should extract the location from this format and search for the specific resource - terraformQuery := fmt.Sprintf("projects/%s/locations/%s/resourceType/%s", projectID, location, resourceName) - sdpItems, err := searchable.Search(ctx, projectID, terraformQuery, true) - if err != nil { - t.Fatalf("Failed to search resources with Terraform format: %v", err) - } - - // The search should return only the specific resource matching the Terraform format - if len(sdpItems) != 1 { - t.Errorf("Expected 1 resource, got %d", len(sdpItems)) - return - } - - // Verify the single item returned - firstItem := sdpItems[0] - if firstItem.GetType() != sdpItemType.String() { - t.Errorf("Expected first item type %s, got %s", sdpItemType.String(), firstItem.GetType()) - } - if firstItem.GetScope() != projectID { - t.Errorf("Expected first item scope '%s', got %s", projectID, firstItem.GetScope()) - } -}) -``` - -## Pointer Helper Functions - -Always define local helper functions for creating pointers: - -```go -func stringPtr(s string) *string { - return &s -} - -func uint64Ptr(u uint64) *uint64 { - return &u -} -``` - -## Common Mistakes to Avoid - -1. **Wrong Package**: Don't use `package main` or `package adapters` -2. **Wrong Protobuf Package**: Using `computepb` for non-Compute resources (e.g., using `computepb.Function` instead of `functionspb.Function`) -3. **Wrong Protobuf API Version**: Not matching the adapter's endpoint version (e.g., using `apiv1` when adapter uses `/v2/`) -4. **Wrong Protobuf Types**: Assuming types exist without verification (e.g., `GlobalAddress` doesn't exist, use `Address`) -5. **Wrong List Response Structure**: Assuming all list responses have `.Items` field (field names vary by service - verify with `go doc`) -6. **Missing Pointer Helpers**: Always define local pointer helper functions -7. **Incorrect HTTP URLs**: Match the exact API endpoint format from the adapter metadata -8. **Missing Static Tests**: Always include link rule tests for linked resources -9. **Missing Search Tests**: Include Search tests only if the adapter implements `SearchableAdapter` - use `t.Skipf()` if not supported -10. **Using Pure Strings for ExpectedType**: Always use Item types with `.String()` (e.g., `gcpshared.ComputeNetwork.String()` or `stdlib.NetworkIP.String()`), never pure strings like `"gcp-compute-network"` or `"ip"` - -## Link Rules Testing Requirements - -**CRITICAL**: Every adapter test SHOULD include comprehensive StaticTests that cover ALL linked resources defined in the adapter. - -### Steps to Ensure Complete Coverage - -1. **Review Adapter File**: Open the corresponding adapter file (e.g., `compute-global-address.go`) -2. **Find linked-resource map**: Locate the map in the adapter that defines which linked item types the adapter produces (field name is `linkRules`; values are `*gcpshared.Impact`). -3. **Create Test Cases**: Write a QueryTest for every linked item type the adapter produces (one test per distinct linked type, not per attribute path). -4. **Handle TODOs**: Note any entries marked with TODO comments—these may not work yet but should be documented in test comments. - -### Example Complete StaticTests - -```go -t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - // Network link - { - ExpectedType: gcpshared.ComputeNetwork.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "default", - ExpectedScope: projectID, - }, - // IP address link - { - ExpectedType: stdlib.NetworkIP.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "203.0.113.1", - ExpectedScope: "global", - }, - // Backend service link - { - ExpectedType: gcpshared.ComputeBackendService.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-backend-service", - ExpectedScope: projectID, - }, - // Subnetwork link (note special scope format) - { - ExpectedType: gcpshared.ComputeSubnetwork.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-subnet", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), - }, - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) -}) -``` - -## Validation Checklist - -- [ ] Package is `adapters_test` -- [ ] All required imports are present -- [ ] **CRITICAL**: Protobuf package matches the adapter's service (e.g., `aiplatformpb` for AI Platform, NOT `computepb`) -- [ ] **CRITICAL**: Protobuf API version matches the adapter's endpoint (e.g., `/v2/` → `apiv2/functionspb`, `/v1/` → `apiv1/aiplatformpb`) -- [ ] Protobuf types verified with `go doc` command and confirmed to exist -- [ ] List response structure verified (field name like `.Items`, `.Models`, etc. varies by service) -- [ ] Mock HTTP responses match actual API endpoints -- [ ] Get test validates all SDP item properties -- [ ] List test validates item count (expect 2+ items) and properties -- [ ] Search test validates item count (expect 2+ items) and properties (if adapter supports Search) -- [ ] Search with Terraform format test included (if adapter has terraform mappings with Search method) -- [ ] **ErrorHandling test**: Tests error responses (e.g., 404 Not Found) -- [ ] **LIMIT TO 3/4 TEST CASES ONLY**: Get, List/Search, Search with Terraform, ErrorHandling - no additional tests needed -- [ ] Multiple query parameter resources test combined query format (e.g., "location/resource") -- [ ] **CRITICAL**: Static tests include a test case for ALL linked resources defined in the adapter (every linked item type the adapter produces) -- [ ] Static test queries use correct scope formats (especially for subnetworks: "projectID.region") -- [ ] Static test queries use correct query formats (especially for KMS keys: use `shared.CompositeLookupKey()`) -- [ ] Pointer helper functions are defined locally -- [ ] Test compiles without errors -- [ ] Test runs successfully - -## Key Patterns - -- **SIMPLIFIED TEST STRUCTURE**: Only 3-4 test cases - Get (with StaticTests), List/Search, Search with Terraform format (if applicable), ErrorHandling -- Use `dynamic.MakeAdapter()` to create adapters -- Always validate SDP item type, scope, and unique attribute -- Include comprehensive attribute validation in Get tests using **camelCase** for attribute names -- Use `t.Skipf()` for optional functionality (like Search) -- Always include static tests that cover every linked resource the adapter produces -- Mock HTTP responses must match actual API endpoints exactly -- **Length checks**: Regular Search/List tests expect 2+ items, but Terraform format Search expects exactly 1 item - -## Post-Implementation Validation - -After completing the adapter and test implementation, you MUST run the following validation steps: - -### 1. Run golangci-lint - -Execute golangci-lint on the sources/gcp directory to check for code quality issues: - -```bash -golangci-lint run ./sources/gcp/... -``` - -**If golangci-lint fails:** - -- Analyze the reported issues carefully -- Fix all linting errors and warnings -- Common issues include: - - Unused variables or imports - - Missing error handling - - Code formatting issues - - Inefficient code patterns -- Re-run golangci-lint until all issues are resolved - -### 2. Run Unit Tests - -Execute the full test suite for the sources/gcp directory: - -```bash -go test -race ./sources/gcp/... -v -``` - -**If tests fail:** - -- Analyze the test failures and error messages -- Common issues include: - - Missing mock responses for HTTP calls - - Incorrect protobuf type usage - - Wrong API endpoint URLs - - Missing or incorrect linked-resource test cases in StaticTests - - Scope format issues (especially for regional resources) -- Fix the underlying issues in the adapter or test code -- Re-run tests until all tests pass - -### 3. Automatic Issue Resolution - -When either golangci-lint or tests fail: - -1. **Analyze the root cause** of each failure -2. **Fix the issues systematically** - don't just suppress warnings -3. **Verify the fix** by re-running the failing command -4. **Repeat until both commands pass successfully** - -### 4. Final Validation - -Both commands must pass before considering the implementation complete: - -- ✅ `golangci-lint run ./sources/gcp/...` (no errors or warnings) -- ✅ `go test -race ./sources/gcp/... -v` (all tests pass) - -**Do not proceed** with any pull request or consider the task complete until both validation steps pass successfully. - -## Test Structure Summary - -```go -TestYourAdapter(t *testing.T) { - // Setup mock data and responses - - t.Run("Get", func(t *testing.T) { - // Test Get functionality - t.Run("StaticTests", func(t *testing.T) { - // Test ALL linked resources the adapter produces - }) - }) - - t.Run("Search", func(t *testing.T) { // OR "List" for project-level - // Test Search/List functionality - }) - - t.Run("ErrorHandling", func(t *testing.T) { - // Test error responses - }) -} -``` diff --git a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc b/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc deleted file mode 100644 index ca06dbab..00000000 --- a/sources/gcp/manual/.cursor/rules/gcp-manual-adapter-creation.mdc +++ /dev/null @@ -1,802 +0,0 @@ ---- -description: "GCP Manual Adapter development patterns and standards" -globs: **/*.go -alwaysApply: false ---- - -# GCP Manual Adapter Creation Rules - -This document provides comprehensive rules and patterns for creating GCP manual adapters in the Overmind platform. Follow these guidelines to ensure consistency, maintainability, and proper integration with the SDP (State Description Protocol) framework. - -## Table of Contents - -1. [Adapter Structure and Naming](#adapter-structure-and-naming) -2. [Wrapper Type Selection](#wrapper-type-selection) -3. [Base Struct Selection](#base-struct-selection) -4. [Required Methods Implementation](#required-methods-implementation) -5. [Terraform Mappings](#terraform-mappings) -6. [Get and Search Lookups](#get-and-search-lookups) -7. [Linked Item Queries](#linked-item-queries) -8. [Error Handling](#error-handling) -9. [Testing Patterns](#testing-patterns) -10. [Client Interface Patterns](#client-interface-patterns) -11. [Common Gotchas and Best Practices](#common-gotchas-and-best-practices) - -## Adapter Structure and Naming - -### File Naming Convention - -- Use kebab-case for file names: `compute-instance.go`, `big-query-table.go`, `cloud-kms-crypto-key.go` -- Test files should follow the same pattern with `_test.go` suffix: `compute-instance_test.go` - -### Package and Import Structure - -```go -package manual - -import ( - "context" - "errors" // if using iterator.Done - "fmt" // if using string formatting - "strings" // if parsing paths or URLs - - "cloud.google.com/go/compute/apiv1/computepb" // Use specific protobuf imports - "google.golang.org/api/iterator" // For handling GCP iterators - - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/cli/sources" - gcpshared "github.com/overmindtech/cli/sources/gcp/shared" - "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/cli/sources/stdlib" // Only if linking to stdlib resources -) -``` - -### Struct Naming - -- Wrapper struct: `{resourceName}Wrapper` (e.g., `computeInstanceWrapper`, `bigQueryTableWrapper`) -- Use camelCase with first letter lowercase for private structs -- Constructor function: `New{ResourceName}` (e.g., `NewComputeInstance`, `NewBigQueryTable`) - -## Wrapper Type Selection - -Choose the appropriate wrapper interface based on GCP API capabilities: - -### `Wrapper` (GET only) - -Use when the GCP API only supports individual resource retrieval: - -- IAM Service Account Keys -- Compute Snapshots (individual retrieval) -- Resources without list/search endpoints - -### `ListableWrapper` (GET + LIST) - -Use when the GCP API supports listing all resources in a scope: - -- Compute Instances (per zone) -- Compute Disks (per zone) -- IAM Service Accounts (per project) -- BigQuery Datasets (per project) - -### `SearchableWrapper` (GET + SEARCH) - -Use when the GCP API supports filtering/searching resources: - -- BigQuery Tables (search within datasets) -- KMS Crypto Keys (search within key rings) -- KMS Key Rings (search within locations) - -### `SearchableListableWrapper` (GET + LIST + SEARCH) - -Use when the GCP API supports both listing and searching: - -- Currently not used in existing adapters, but available for complex resources - -## Base Struct Selection - -Choose the appropriate base struct based on GCP resource scope: - -### `ZoneBase` - Zonal Resources - -For resources scoped to a specific zone: - -```go -type computeInstanceWrapper struct { - client gcpshared.ComputeInstanceClient - *gcpshared.ZoneBase -} - -func NewComputeInstance(client gcpshared.ComputeInstanceClient, locations []gcpshared.LocationInfo) sources.ListableWrapper { - return &computeInstanceWrapper{ - client: client, - ZoneBase: gcpshared.NewZoneBase( - locations, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - gcpshared.ComputeInstance, - ), - } -} -``` - -**Examples:** Compute Instances, Compute Disks, Compute Snapshots, Compute Images - -### `RegionBase` - Regional Resources - -For resources scoped to a specific region: - -```go -type computeAddressWrapper struct { - client gcpshared.ComputeAddressClient - *gcpshared.RegionBase -} - -func NewComputeAddress(client gcpshared.ComputeAddressClient, projectID, region string) sources.ListableWrapper { - return &computeAddressWrapper{ - client: client, - RegionBase: gcpshared.NewRegionBase( - projectID, - region, - sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, - gcpshared.ComputeAddress, - ), - } -} -``` - -**Examples:** Compute Addresses, Compute Reservations, Compute Resource Policies - -### `ProjectBase` - Project-Level Resources - -For resources scoped to the entire project: - -```go -type iamServiceAccountWrapper struct { - client gcpshared.IAMServiceAccountClient - *gcpshared.ProjectBase -} - -func NewIAMServiceAccount(client gcpshared.IAMServiceAccountClient, projectID string) sources.ListableWrapper { - return &iamServiceAccountWrapper{ - client: client, - ProjectBase: gcpshared.NewProjectBase( - projectID, - sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, - gcpshared.IAMServiceAccount, - ), - } -} -``` - -**Examples:** IAM Service Accounts, BigQuery Datasets, KMS Key Rings, Logging Sinks - -## Required Methods Implementation - -### IAM Permissions - -Always implement with specific GCP API permissions: - -```go -func (c computeInstanceWrapper) IAMPermissions() []string { - return []string{ - "compute.instances.get", - "compute.instances.list", - } -} -``` - -### Predefined Role - -Always implement with the most restrictive GCP predefined role: - -```go -func (c computeInstanceWrapper) PredefinedRole() string { - return "roles/compute.viewer" -} -``` - -### Potential Links - -Document all possible linked resources: - -```go -func (c computeInstanceWrapper) PotentialLinks() map[shared.ItemType]bool { - return shared.NewItemTypesSet( - stdlib.NetworkIP, - gcpshared.ComputeDisk, - gcpshared.ComputeSubnetwork, - gcpshared.ComputeNetwork, - gcpshared.ComputeResourcePolicy, - ) -} -``` - -## Terraform Mappings - -### GET Method (Direct ID Match) - -Use when Terraform resource has a unique identifier that directly matches your adapter's unique attribute: - -```go -func (c computeInstanceWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance#argument-reference - TerraformQueryMap: "google_compute_instance.name", - }, - } -} -``` - -### SEARCH Method (Multiple Parameters) - -Use when Terraform resource requires multiple parameters or different ID format: - -```go -func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_SEARCH, - // https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table - // Terraform uses: projects/{{project}}/datasets/{{dataset}}/tables/{{name}} - // Our adapter uses: dataset_id + table_id as separate parameters - TerraformQueryMap: "google_bigquery_table.id", - }, - } -} -``` - -### Multiple Terraform Mappings - -For resources that can be referenced in multiple ways: - -```go -func (c iamServiceAccountWrapper) TerraformMappings() []*sdp.TerraformMapping { - return []*sdp.TerraformMapping{ - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "google_service_account.email", - }, - { - TerraformMethod: sdp.QueryMethod_GET, - TerraformQueryMap: "google_service_account.unique_id", - }, - } -} -``` - -## Get and Search Lookups - -### Single Key Lookup - -For resources with a single unique identifier: - -```go -var ComputeInstanceLookupByName = shared.NewItemTypeLookup("name", gcpshared.ComputeInstance) - -func (c computeInstanceWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - ComputeInstanceLookupByName, - } -} -``` - -### Multiple Keys Lookup (Order Matters) - -The order of lookups determines the order of `queryParts` in the `Get` method: - -```go -var ( - BigQueryDatasetLookupByID = shared.NewItemTypeLookup("id", gcpshared.BigQueryDataset) - BigQueryTableLookupByID = shared.NewItemTypeLookup("id", gcpshared.BigQueryTable) -) - -func (b BigQueryTableWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - BigQueryDatasetLookupByID, // First key: dataset ID (queryParts[0]) - BigQueryTableLookupByID, // Second key: table ID (queryParts[1]) - } -} - -func (b BigQueryTableWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - datasetID := queryParts[0] // From BigQueryDatasetLookupByID - tableID := queryParts[1] // From BigQueryTableLookupByID - // ... implementation -} -``` - -### Multiple Keys with Composite Lookup - -For complex hierarchical resources: - -```go -var ( - CloudKMSCryptoKeyRingLookupByLocation = shared.NewItemTypeLookup("location", gcpshared.CloudKMSKeyRing) - CloudKMSCryptoKeyRingLookupByName = shared.NewItemTypeLookup("name", gcpshared.CloudKMSKeyRing) - CloudKMSCryptoKeyLookupByName = shared.NewItemTypeLookup("name", gcpshared.CloudKMSCryptoKey) -) - -func (c cloudKMSCryptoKeyWrapper) GetLookups() sources.ItemTypeLookups { - return sources.ItemTypeLookups{ - CloudKMSCryptoKeyRingLookupByLocation, // First key: location - CloudKMSCryptoKeyRingLookupByName, // Second key: key ring name - CloudKMSCryptoKeyLookupByName, // Third key: crypto key name - } -} -``` - -### Search Lookups - -Define search parameters for SearchableWrapper: - -```go -// Single search lookup -func (b BigQueryTableWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - BigQueryDatasetLookupByID, // Search within a specific dataset - }, - } -} - -// Multiple search lookups (different search patterns) -func (c cloudKMSCryptoKeyWrapper) SearchLookups() []sources.ItemTypeLookups { - return []sources.ItemTypeLookups{ - { - CloudKMSCryptoKeyRingLookupByLocation, // Search by location only - }, - { - CloudKMSCryptoKeyRingLookupByLocation, // Search within specific key ring - CloudKMSCryptoKeyRingLookupByName, - }, - } -} -``` - -## Linked Item Queries - -### Basic Pattern - -Always use this pattern for creating linked item queries: - -```go -sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.TargetResourceType.String(), - Method: sdp.QueryMethod_GET, // or SEARCH - Query: "resource-identifier", - Scope: "appropriate-scope", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // This resource affected if linked resource changes - Out: true, // Linked resource affected if this resource changes - }, -}) -``` - -### Path Parameter Extraction - -Use helper functions for extracting parameters from GCP resource URLs: - -```go -// Extract parameters with their key names -// key: projects/my-project/locations/my-location/keyRings/my-key-ring/cryptoKeys/my-crypto-key -values := gcpshared.ExtractPathParams( - cryptoKey.GetName(), - "locations", "keyRings", "cryptoKeys" -) -``` - -### Composite Lookup Keys - -Use for multi-parameter queries: - -```go -Query: shared.CompositeLookupKey(location, keyRing, cryptoKey) -``` - -### Blast Propagation Rules - -- **In: true, Out: true** - Tightly coupled resources (parent-child relationships) -- **In: true, Out: false** - This resource depends on linked resource (e.g., disk depends on image) -- **In: false, Out: true** - Linked resource depends on this resource (e.g., instance depends on disk) - -### Common Linked Resource Patterns - -#### Network Resources - -```go -// Extract network name from full URL -if network := instance.GetNetwork(); network != "" { - if strings.Contains(network, "/") { - networkNameParts := strings.Split(network, "/") - networkName := networkNameParts[len(networkNameParts)-1] - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.ComputeNetwork.String(), - Method: sdp.QueryMethod_GET, - Query: networkName, - Scope: c.ProjectID(), // Networks are global - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -#### KMS Resources - -```go -// Extract KMS key version parameters -if keyName := encryptionKey.GetKmsKeyName(); keyName != "" { - location := gcpshared.ExtractPathParam("locations", keyName) - keyRing := gcpshared.ExtractPathParam("keyRings", keyName) - cryptoKey := gcpshared.ExtractPathParam("cryptoKeys", keyName) - cryptoKeyVersion := gcpshared.ExtractPathParam("cryptoKeyVersions", keyName) - - if location != "" && keyRing != "" && cryptoKey != "" && cryptoKeyVersion != "" { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: gcpshared.CloudKMSCryptoKeyVersion.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(location, keyRing, cryptoKey, cryptoKeyVersion), - Scope: c.ProjectID(), - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }) - } -} -``` - -## Error Handling - -### Standard Error Pattern - -Always use source-specific error wrapping: - -```go -if err != nil { - return nil, gcpshared.QueryError(err, scope, c.Type()) -} -``` - -### Iterator Pattern - -Handle GCP API iterators consistently: - -```go -for { - item, err := it.Next() - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - return nil, gcpshared.QueryError(err, scope, c.Type()) - } - // Process item... -} -``` - -### Stream Error Handling - -For streaming operations: - -```go -if err != nil { - stream.SendError(gcpshared.QueryError(err, scope, c.Type())) - return -} - -// For item processing errors, continue with next item -if sdpErr != nil { - stream.SendError(sdpErr) - continue -} -``` - -## Testing Patterns - -### Test Structure - -Follow this exact pattern for all adapter tests: - -```go -func TestComputeInstance(t *testing.T) { - ctx := context.Background() - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := mocks.NewMockComputeInstanceClient(ctrl) - projectID := "test-project-id" - zone := "us-central1-a" - - t.Run("Get", func(t *testing.T) { - wrapper := manual.NewComputeInstance(mockClient, projectID, zone) - mockClient.EXPECT().Get(ctx, gomock.Any()).Return(createComputeInstance("test-instance", computepb.Instance_RUNNING), nil) - - adapter := sources.WrapperToAdapter(wrapper) - sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-instance", true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - t.Run("StaticTests", func(t *testing.T) { - queryTests := shared.QueryTests{ - { - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: "test-project-id.us-central1-a", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - // ... more test cases - } - shared.RunStaticTests(t, adapter, sdpItem, queryTests) - }) - }) - - t.Run("List", func(t *testing.T) { - // Test list functionality - }) - - t.Run("Search", func(t *testing.T) { - // Test search functionality (if applicable) - }) -} -``` - -### Required Test Cases - -- **Get method**: Test successful retrieval and error cases -- **StaticTests**: Test linked item queries using `shared.RunStaticTests` -- **HealthCheck**: Test health status mapping (if applicable) -- **List method**: Test the List method (for ListableWrapper) -- **Search method**: Test the Search method (for SearchableWrapper) -- **Interface compliance**: Verify adapter implements correct interfaces - -### Test Helper Functions - -Create helper functions for test data: - -```go -func createComputeInstance(instanceName string, status computepb.Instance_Status) *computepb.Instance { - return &computepb.Instance{ - Name: ptr.To(instanceName), - Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), - // ... other fields - } -} -``` - -### Mock Expectations - -Use gomock for client mocking: - -```go -mockClient := mocks.NewMockComputeInstanceClient(ctrl) -mockClient.EXPECT().Get(ctx, gomock.Any()).Return(expectedResult, nil) -``` - -## Client Interface Patterns - -### Typed Client Interfaces - -Define typed client interfaces in the shared package: - -```go -// In sources/gcp/shared/compute-clients.go -type ComputeInstanceClient interface { - Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error) - List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator -} - -// Constructor function -func NewComputeInstanceClient(client *compute.InstancesClient) ComputeInstanceClient { - return &computeInstanceClientImpl{client: client} -} -``` - -### Client Implementation - -Implement the interface with proper error handling: - -```go -type computeInstanceClientImpl struct { - client *compute.InstancesClient -} - -func (c *computeInstanceClientImpl) Get(ctx context.Context, req *computepb.GetInstanceRequest, opts ...gax.CallOption) (*computepb.Instance, error) { - return c.client.Get(ctx, req, opts...) -} - -func (c *computeInstanceClientImpl) List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator { - return c.client.List(ctx, req, opts...) -} -``` - -## Common Gotchas and Best Practices - -### 1. Unique Attribute Consistency - -Ensure the `UniqueAttribute` in the SDP item matches the lookup key: - -```go -// If using composite lookup key -err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(values...)) -sdpItem := &sdp.Item{ - Type: gcpshared.CloudKMSCryptoKey.String(), - UniqueAttribute: "uniqueAttr", // Must match the attribute name above - // ... -} -``` - -### 2. Scope Handling - -Use appropriate scope helper functions: - -```go -// For zonal resources -scope := gcpshared.ZonalScope(projectID, zone) - -// For regional resources -scope := gcpshared.RegionalScope(projectID, region) - -// For project-level resources -scope := gcpshared.ProjectScope(projectID) -``` - -### 3. Attributes and Tags - -Use `ToAttributesWithExclude` to convert protobuf to attributes: - -```go -attributes, err := shared.ToAttributesWithExclude(instance, "labels") -tags := instance.GetLabels() // Use labels as tags -``` - -### 4. Health Status Mapping - -Map GCP resource statuses to SDP health statuses: - -```go -switch disk.GetStatus() { -case computepb.Disk_UNDEFINED_STATUS.String(): - sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() -case computepb.Disk_CREATING.String(), computepb.Disk_RESTORING.String(): - sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() -case computepb.Disk_FAILED.String(), computepb.Disk_UNAVAILABLE.String(): - sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() -case computepb.Disk_READY.String(): - sdpItem.Health = sdp.Health_HEALTH_OK.Enum() -} -``` - -### 5. Multiple Query Parameters - -For resources requiring multiple parameters: - -```go -func (b BigQueryTableWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { - datasetID := queryParts[0] // First parameter - tableID := queryParts[1] // Second parameter - // ... implementation -} -``` - -### 6. Conditional Linked Queries - -Only create linked queries when the referenced resource exists: - -```go -if metadata.EncryptionConfig != nil && metadata.EncryptionConfig.KMSKeyName != "" { - // Create KMS key linked query -} -``` - -### 7. Path Validation - -Always validate extracted path parameters: - -```go -values := gcpshared.ExtractPathParams(keyName, "locations", "keyRings", "cryptoKeys") -if len(values) == 3 && values[0] != "" && values[1] != "" && values[2] != "" { - // Use the extracted values -} -``` - -### 8. Adapter Registration - -Register adapters in the main adapters file with proper scoping: - -```go -// Register zonal adapters -for _, zone := range zones { - adapters = append(adapters, - sources.WrapperToAdapter(NewComputeInstance( - shared.NewComputeInstanceClient(instanceCli), - projectID, - zone, - )), - ) -} - -// Register project-level adapters -adapters = append(adapters, - sources.WrapperToAdapter(NewIAMServiceAccount( - shared.NewIAMServiceAccountClient(iamCli), - projectID, - )), -) -``` - -### 9. Documentation Comments - -Always include GCP API documentation URLs in comments: - -```go -// The resource URL for the disk type associated with this disk. -// GET https://compute.googleapis.com/compute/v1/projects/{project}/zones/{zone}/diskTypes/{diskType} -// https://cloud.google.com/compute/docs/reference/rest/v1/diskTypes/get -``` - -### 10. Error Message Consistency - -Use consistent error messages and include context: - -```go -return nil, &sdp.QueryError{ - ErrorType: sdp.QueryError_OTHER, - ErrorString: fmt.Sprintf("invalid CryptoKey name: %s", cryptoKey.GetName()), -} -``` - -## Validation Checklist - -Before submitting a new adapter, ensure: - -- [ ] File follows naming convention (`{resource-name}.go`) -- [ ] Imports are properly organized and minimal -- [ ] Wrapper type matches GCP API capabilities -- [ ] Base struct matches resource scope (Zone/Region/Project) -- [ ] All required methods implemented (IAMPermissions, PredefinedRole, PotentialLinks) -- [ ] Terraform mappings are correct and include documentation URLs -- [ ] Get/Search lookups match the resource's query parameters -- [ ] Linked item queries use proper blast propagation -- [ ] Error handling uses `gcpshared.QueryError` -- [ ] Iterator pattern is correctly implemented -- [ ] Health status mapping is implemented (if applicable) -- [ ] Test file covers all required test cases -- [ ] Static tests validate linked item queries -- [ ] Client interface is properly defined in shared package -- [ ] Adapter is registered in the main adapters file -- [ ] Documentation comments include GCP API URLs -- [ ] Unique attribute consistency is maintained -- [ ] Scope handling uses appropriate helper functions -- [ ] Path parameter extraction is validated -- [ ] Conditional linked queries are properly implemented - -## Examples Reference - -For complete examples, refer to these existing adapters: - -- **Simple ListableWrapper**: `sources/gcp/manual/compute-instance.go` -- **Complex ListableWrapper**: `sources/gcp/manual/compute-disk.go` -- **SearchableWrapper with Multiple Keys**: `sources/gcp/manual/big-query-table.go` -- **SearchableWrapper with Composite Lookup**: `sources/gcp/manual/cloud-kms-crypto-key.go` -- **Project-level Resource**: `sources/gcp/manual/iam-service-account.go` -- **Regional Resource**: `sources/gcp/manual/compute-address.go` -- **Complex Linked Queries**: `sources/gcp/manual/compute-backend-service.go` - -These examples demonstrate all the patterns and best practices outlined in this document. From adfacf2a59a1326eee35655ac6d46c44ed45e56f Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:58:56 +0100 Subject: [PATCH 24/51] Eng 2461 create dedicated adapter for google storage bucket iam binding (#3863) > [!NOTE] > **Medium Risk** > Introduces new GCP IAM-policy collection and linking plus new IAM permission requirements; mistakes could impact security-scoped discovery results or require additional permissions in customer projects. > > **Overview** > Adds a new GCP manual adapter `StorageBucketIAMPolicy` (one item per bucket) that fetches bucket IAM via the Storage `getIamPolicy` v3 API, serializes bindings, and emits links to related service accounts, custom roles, project principals, and domains. > > Wires this into discovery: initializes a GCS `storage.Client`, registers the adapter, introduces the new item type/resource and linker, and adds parent-to-child linking from `StorageBucket` plus Terraform mappings for `google_storage_bucket_iam_*` resources. > > Updates deployment IAM to grant `storage.buckets.getIamPolicy` via the existing `overmind_custom_role`, and adjusts a few tests (Azure adapter query validation expectations; GCP impersonation integration test credential/token-source handling and softer failure behavior). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1fe795d139c0793fdfa722846ecded22cd700e6c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent Co-authored-by: Dylan GitOrigin-RevId: e02d429b2bb3868ea24de9d2bc76f2ec74007ef1 --- go.mod | 16 +- go.sum | 21 +- .../authorization-role-assignment_test.go | 10 +- .../compute-virtual-machine-extension_test.go | 11 +- .../gcp/dynamic/adapters/compute-project.go | 5 + .../gcp/dynamic/adapters/storage-bucket.go | 8 +- .../dynamic/adapters/storage-bucket_test.go | 7 + .../service-account-impersonation_test.go | 127 ++-- sources/gcp/manual/adapters.go | 8 + .../gcp/manual/storage-bucket-iam-policy.go | 393 +++++++++++++ .../manual/storage-bucket-iam-policy_test.go | 552 ++++++++++++++++++ sources/gcp/shared/item-types.go | 1 + sources/gcp/shared/manual-adapter-links.go | 18 + sources/gcp/shared/models.go | 1 + sources/gcp/shared/predefined-roles.go | 5 +- sources/gcp/shared/storage-iam.go | 67 +++ 16 files changed, 1181 insertions(+), 69 deletions(-) create mode 100644 sources/gcp/manual/storage-bucket-iam-policy.go create mode 100644 sources/gcp/manual/storage-bucket-iam-policy_test.go create mode 100644 sources/gcp/shared/storage-iam.go diff --git a/go.mod b/go.mod index 7fd2924b..b869fb14 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( cloud.google.com/go/secretmanager v1.16.0 cloud.google.com/go/securitycentermanagement v1.1.6 cloud.google.com/go/spanner v1.88.0 + cloud.google.com/go/storage v1.60.0 cloud.google.com/go/storagetransfer v1.13.1 connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version connectrpc.com/otelconnect v0.9.0 @@ -206,7 +207,7 @@ require ( atomicgo.dev/schedule v0.1.0 // indirect cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 cloud.google.com/go/longrunning v0.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect @@ -475,3 +476,16 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) + +require ( + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect +) diff --git a/go.sum b/go.sum index 5deb8bd5..d0592667 100644 --- a/go.sum +++ b/go.sum @@ -89,10 +89,12 @@ cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbneh cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8= -cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= +cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/storagetransfer v1.13.1 h1:Sjukr1LtUt7vLTHNvGc2gaAqlXNFeDFRIRmWGrFaJlY= cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= @@ -148,10 +150,12 @@ github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -416,8 +420,11 @@ github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= @@ -1106,6 +1113,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index 12557f58..d94a0565 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -103,17 +103,13 @@ func TestAuthorizationRoleAssignment(t *testing.T) { wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) - // Test with insufficient query parts (empty) + // Test with empty query (adapter rejects before calling wrapper) _, qErr := adapter.Get(ctx, scope, "", true) if qErr == nil { t.Error("Expected error when getting role assignment with empty name, but got nil") } - - // Test with too many query parts - Get expects a single query string - _, qErr = adapter.Get(ctx, scope, shared.CompositeLookupKey("name", "extra"), true) - if qErr == nil { - t.Error("Expected error when getting role assignment with too many query parts, but got nil") - } + // Note: "too many" query parts are coalesced by the standard adapter into a single part, + // so the wrapper would receive one part and call the client. We only test empty here. }) t.Run("Get_EmptyRoleAssignmentName", func(t *testing.T) { diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index 9a374bf7..da4ccbba 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -403,17 +403,14 @@ func TestComputeVirtualMachineExtension(t *testing.T) { scope := subscriptionID + "." + resourceGroup - // Test with too few query parts + // Test with too few query parts (single segment - adapter rejects before calling wrapper) _, qErr := adapter.Get(ctx, scope, "only-vm-name", true) if qErr == nil { t.Error("Expected error for invalid query parts, got nil") } - - // Test with too many query parts - _, qErr = adapter.Get(ctx, scope, shared.CompositeLookupKey(vmName, extensionName, "extra"), true) - if qErr == nil { - t.Error("Expected error for too many query parts, got nil") - } + // Note: "too many" query parts are coalesced by the standard adapter (trailing segments + // merged into the last part), so the wrapper always receives exactly 2 parts and would + // call the client. We only test "too few" here to avoid requiring a mock Get expectation. }) t.Run("EmptyVirtualMachineName", func(t *testing.T) { diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index 2b69ffcd..52c8b272 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -91,6 +91,11 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_project_iam_policy.project", }, + { + // Configures which services and log types are audited for the project. + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_project_iam_audit_config.project", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index bc18b7eb..7a917ddc 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -40,11 +40,17 @@ var _ = registerableAdapter{ ToSDPItemType: gcpshared.LoggingBucket, Description: "If the Logging Bucket is deleted or updated: The Storage Bucket may fail to write logs. If the Storage Bucket is updated: The Logging Bucket remains unaffected.", }, + // Parent-to-child: bucket name links to this bucket's IAM policy (SEARCH returns one policy item). + "name": { + ToSDPItemType: gcpshared.StorageBucketIAMPolicy, + Description: "If the Storage Bucket is deleted or updated: Its IAM policy may become invalid. If the IAM policy is updated: The bucket remains unaffected.", + IsParentToChild: true, + }, // TODO: Add parent-to-child links once the child adapters are implemented: // - StorageBucketAccessControl (requires adapter implementation) // - StorageDefaultObjectAccessControl (requires adapter implementation) // - StorageNotificationConfig (requires adapter implementation) - // Note: Parent-to-child links must use the "name" field (not array fields like "acl") + // Note: Only one parent-to-child link per field (map limitation). "name" is used for StorageBucketIAMPolicy. // since the linkItem function iterates into arrays before calling AutoLink, causing // keys like "acl.entity" instead of "acl" which would never match. // The framework only supports one parent-to-child link per field (map limitation). diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index 5bac76a0..aa30b4ee 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -71,6 +71,13 @@ func TestStorageBucket(t *testing.T) { ExpectedQuery: shared.CompositeLookupKey("global", "my-keyring", "my-key"), ExpectedScope: projectID, }, + { + // name -> StorageBucketIAMPolicy (parent-to-child: one policy per bucket, GET by bucket name) + ExpectedType: gcpshared.StorageBucketIAMPolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, + }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/gcp/integration-tests/service-account-impersonation_test.go b/sources/gcp/integration-tests/service-account-impersonation_test.go index 6b7ff0dd..53b380d8 100644 --- a/sources/gcp/integration-tests/service-account-impersonation_test.go +++ b/sources/gcp/integration-tests/service-account-impersonation_test.go @@ -13,12 +13,13 @@ import ( compute "cloud.google.com/go/compute/apiv1" "cloud.google.com/go/compute/apiv1/computepb" + authcredentials "cloud.google.com/go/auth/credentials" + "cloud.google.com/go/auth/oauth2adapt" credentials "cloud.google.com/go/iam/credentials/apiv1" credentialspb "cloud.google.com/go/iam/credentials/apiv1/credentialspb" "github.com/google/uuid" "github.com/googleapis/gax-go/v2/apierror" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" "google.golang.org/api/iam/v1" @@ -49,7 +50,8 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // Initialize Cloud Resource Manager service crmService, err := cloudresourcemanager.NewService(t.Context()) if err != nil { - t.Fatalf("Failed to create Cloud Resource Manager service: %v", err) + t.Errorf("Failed to create Cloud Resource Manager service: %v", err) + return } state := &testState{ @@ -59,7 +61,8 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // Initialize IAM service using Application Default Credentials iamService, err := iam.NewService(t.Context()) if err != nil { - t.Fatalf("Failed to create IAM service: %v", err) + t.Errorf("Failed to create IAM service: %v", err) + return } // Create UUIDs for service account names @@ -73,7 +76,9 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { // since this test needs to keep state between tests, we wrap it in a Run function t.Run("Run", func(t *testing.T) { - setupTest(t, t.Context(), iamService, crmService, state) + if !setupTest(t, t.Context(), iamService, crmService, state) { + return + } t.Cleanup(func() { teardownTest(t, t.Context(), iamService, crmService, state) @@ -94,12 +99,13 @@ func TestServiceAccountImpersonationIntegration(t *testing.T) { }) } -func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmService *cloudresourcemanager.Service, state *testState) { +func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmService *cloudresourcemanager.Service, state *testState) bool { // Create "Our Service Account" t.Logf("Creating 'Our Service Account': %s", state.ourServiceAccountID) ourSA, err := createServiceAccount(ctx, iamService, state.projectID, state.ourServiceAccountID, "Our Service Account for impersonation test") if err != nil { - t.Fatalf("Failed to create 'Our Service Account': %v", err) + t.Errorf("Failed to create 'Our Service Account': %v", err) + return false } state.ourServiceAccountEmail = ourSA.Email t.Logf("Created 'Our Service Account': %s", state.ourServiceAccountEmail) @@ -108,7 +114,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Creating 'Customer Service Account': %s", state.customerServiceAccountID) customerSA, err := createServiceAccount(ctx, iamService, state.projectID, state.customerServiceAccountID, "Customer Service Account for impersonation test") if err != nil { - t.Fatalf("Failed to create 'Customer Service Account': %v", err) + t.Errorf("Failed to create 'Customer Service Account': %v", err) + return false } state.customerServiceAccountEmail = customerSA.Email t.Logf("Created 'Customer Service Account': %s", state.customerServiceAccountEmail) @@ -136,7 +143,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe if attempt < maxAttempts { time.Sleep(1 * time.Second) } else { - t.Fatalf("Service account verification failed after %d attempts. The service accounts may not have been created correctly.", maxAttempts) + t.Errorf("Service account verification failed after %d attempts. The service accounts may not have been created correctly.", maxAttempts) + return false } } @@ -144,7 +152,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Granting impersonation permission to 'Our Service Account'") err = grantServiceAccountTokenCreator(ctx, iamService, state.projectID, state.customerServiceAccountEmail, state.ourServiceAccountEmail) if err != nil { - t.Fatalf("Failed to grant serviceAccountTokenCreator role: %v", err) + t.Errorf("Failed to grant serviceAccountTokenCreator role: %v", err) + return false } // Verify IAM policy binding is effective @@ -164,7 +173,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe if attempt < maxAttempts { time.Sleep(1 * time.Second) } else { - t.Fatalf("IAM policy binding verification failed after %d attempts. The role may not have been granted correctly.", maxAttempts) + t.Errorf("IAM policy binding verification failed after %d attempts. The role may not have been granted correctly.", maxAttempts) + return false } } @@ -172,7 +182,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Granting roles/compute.viewer to 'Customer Service Account' at project level") err = grantProjectIAMRole(ctx, crmService, state.projectID, state.customerServiceAccountEmail, "roles/compute.viewer") if err != nil { - t.Fatalf("Failed to grant roles/compute.viewer role: %v", err) + t.Errorf("Failed to grant roles/compute.viewer role: %v", err) + return false } // Create service account keys for authentication @@ -181,7 +192,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe // Create key for "Our Service Account" ourKey, err := createServiceAccountKey(ctx, iamService, state.projectID, state.ourServiceAccountEmail) if err != nil { - t.Fatalf("Failed to create key for 'Our Service Account': %v", err) + t.Errorf("Failed to create key for 'Our Service Account': %v", err) + return false } state.ourServiceAccountKey = []byte(ourKey.PrivateKeyData) state.ourServiceAccountKeyID = extractKeyID(ourKey.Name) @@ -190,7 +202,8 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe // Create key for "Customer Service Account" customerKey, err := createServiceAccountKey(ctx, iamService, state.projectID, state.customerServiceAccountEmail) if err != nil { - t.Fatalf("Failed to create key for 'Customer Service Account': %v", err) + t.Errorf("Failed to create key for 'Customer Service Account': %v", err) + return false } state.customerServiceAccountKey = []byte(customerKey.PrivateKeyData) state.customerServiceAccountKeyID = extractKeyID(customerKey.Name) @@ -201,21 +214,25 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Log("Verifying permission is actually effective by attempting GenerateAccessToken...") keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key for verification: %v", err) + t.Errorf("Failed to decode service account key for verification: %v", err) + return false } maxAttempts = 60 // Allow more time for enforcement for attempt := 1; attempt <= maxAttempts; attempt++ { // Create credentials from "Our Service Account" key - testCreds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, iam.CloudPlatformScope) + testCreds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: []string{iam.CloudPlatformScope}}) if err != nil { - t.Fatalf("Failed to create credentials for verification: %v", err) + t.Errorf("Failed to create credentials for verification: %v", err) + return false } + testTokenSource := oauth2adapt.TokenSourceFromTokenProvider(testCreds) // Create IAM Credentials client - testClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(testCreds.TokenSource)) + testClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(testTokenSource)) if err != nil { - t.Fatalf("Failed to create IAM Credentials client for verification: %v", err) + t.Errorf("Failed to create IAM Credentials client for verification: %v", err) + return false } // Attempt to generate a token to verify the permission is actually effective @@ -235,9 +252,11 @@ func setupTest(t *testing.T, ctx context.Context, iamService *iam.Service, crmSe t.Logf("Attempt %d/%d: Permission not yet effective, error: %v, waiting...", attempt, maxAttempts, err) time.Sleep(2 * time.Second) } else { - t.Fatalf("Permission verification failed after %d attempts. The permission may not be enforced yet. Last error: %v", maxAttempts, err) + t.Errorf("Permission verification failed after %d attempts. The permission may not be enforced yet. Last error: %v", maxAttempts, err) + return false } } + return true } func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *testState) { @@ -246,20 +265,24 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t // Decode the service account key keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from the key - creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, compute.DefaultAuthScopes()...) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: compute.DefaultAuthScopes()}) if err != nil { t.Logf("Key data: %s", string(keyData)) - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create Compute Engine client using these credentials - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(creds.TokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -279,7 +302,8 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t // We expect a permission error if err == nil { - t.Fatal("Expected permission denied error, but listing succeeded") + t.Error("Expected permission denied error, but listing succeeded") + return } // Check if it's a permission error @@ -289,7 +313,8 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t t.Logf("✓ Correctly received permission denied error: %v", err) return } - t.Fatalf("Expected permission denied error, got: %v", err) + t.Errorf("Expected permission denied error, got: %v", err) + return } // Also check for googleapi.Error @@ -299,10 +324,11 @@ func testOurServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *t t.Logf("✓ Correctly received permission denied error: %v", err) return } - t.Fatalf("Expected permission denied error, got: %v", err) + t.Errorf("Expected permission denied error, got: %v", err) + return } - t.Fatalf("Expected permission denied error, got unexpected error: %v", err) + t.Errorf("Expected permission denied error, got unexpected error: %v", err) } func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, state *testState) { @@ -311,19 +337,23 @@ func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, sta // Decode the service account key keyData, err := base64.StdEncoding.DecodeString(string(state.customerServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from the key - creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, compute.DefaultAuthScopes()...) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: compute.DefaultAuthScopes()}) if err != nil { - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create Compute Engine client using these credentials - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(creds.TokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -341,7 +371,8 @@ func testCustomerServiceAccountDirectAuth(t *testing.T, ctx context.Context, sta it := client.List(ctx, req) _, err = it.Next() if err != nil { - t.Fatalf("Expected to successfully list instances, but got error: %v", err) + t.Errorf("Expected to successfully list instances, but got error: %v", err) + return } t.Log("✓ Successfully listed instances as 'Customer Service Account'") @@ -353,19 +384,23 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { // Decode the "Our Service Account" key keyData, err := base64.StdEncoding.DecodeString(string(state.ourServiceAccountKey)) if err != nil { - t.Fatalf("Failed to decode service account key: %v", err) + t.Errorf("Failed to decode service account key: %v", err) + return } // Create credentials from "Our Service Account" key - creds, err := google.CredentialsFromJSONWithType(ctx, keyData, google.ServiceAccount, iam.CloudPlatformScope) + creds, err := authcredentials.NewCredentialsFromJSON(authcredentials.ServiceAccount, keyData, &authcredentials.DetectOptions{Scopes: []string{iam.CloudPlatformScope}}) if err != nil { - t.Fatalf("Failed to create credentials from key: %v", err) + t.Errorf("Failed to create credentials from key: %v", err) + return } + tokenSource := oauth2adapt.TokenSourceFromTokenProvider(creds) // Create IAM Credentials client using "Our Service Account" credentials - iamCredsClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(creds.TokenSource)) + iamCredsClient, err := credentials.NewIamCredentialsClient(ctx, option.WithTokenSource(tokenSource)) if err != nil { - t.Fatalf("Failed to create IAM Credentials client: %v", err) + t.Errorf("Failed to create IAM Credentials client: %v", err) + return } defer iamCredsClient.Close() @@ -377,17 +412,18 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { tokenResp, err := iamCredsClient.GenerateAccessToken(ctx, generateTokenReq) if err != nil { - t.Fatalf("Failed to generate access token for impersonated service account: %v", err) + t.Errorf("Failed to generate access token for impersonated service account: %v", err) + return } // Create Compute Engine client using the impersonated token - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ + impersonatedTS := oauth2.StaticTokenSource(&oauth2.Token{ AccessToken: tokenResp.GetAccessToken(), }) - - client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(tokenSource)) + client, err := compute.NewInstancesRESTClient(ctx, option.WithTokenSource(impersonatedTS)) if err != nil { - t.Fatalf("Failed to create Compute client: %v", err) + t.Errorf("Failed to create Compute client: %v", err) + return } defer client.Close() @@ -405,7 +441,8 @@ func testImpersonation(t *testing.T, ctx context.Context, state *testState) { it := client.List(ctx, req) _, err = it.Next() if err != nil { - t.Fatalf("Expected to successfully list instances via impersonation, but got error: %v", err) + t.Errorf("Expected to successfully list instances via impersonation, but got error: %v", err) + return } t.Log("✓ Successfully listed instances via impersonation") diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index d927173b..eba915ec 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -9,6 +9,7 @@ import ( compute "cloud.google.com/go/compute/apiv1" iamAdmin "cloud.google.com/go/iam/admin/apiv1" logging "cloud.google.com/go/logging/apiv2" + "cloud.google.com/go/storage" "golang.org/x/oauth2" "google.golang.org/api/option" @@ -50,6 +51,7 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati nodeTemplateCli *compute.NodeTemplatesClient regionBackendServiceCli *compute.RegionBackendServicesClient regionHealthCheckCli *compute.RegionHealthChecksClient + storageCli *storage.Client ) if initGCPClients { @@ -222,6 +224,11 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati if err != nil { return nil, fmt.Errorf("failed to create compute region health checks client: %w", err) } + + storageCli, err = storage.NewClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create storage client: %w", err) + } } var adapters []discovery.Adapter @@ -290,6 +297,7 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati sources.WrapperToAdapter(NewBigQueryDataset(shared.NewBigQueryDatasetClient(bigQueryDatasetCli), projectLocations), cache), sources.WrapperToAdapter(NewLoggingSink(shared.NewLoggingConfigClient(loggingConfigCli), projectLocations), cache), sources.WrapperToAdapter(NewBigQueryRoutine(shared.NewBigQueryRoutineClient(bigQueryDatasetCli), projectLocations), cache), + sources.WrapperToAdapter(NewStorageBucketIAMPolicy(shared.NewStorageBucketIAMPolicyGetter(storageCli), projectLocations), cache), ) } diff --git a/sources/gcp/manual/storage-bucket-iam-policy.go b/sources/gcp/manual/storage-bucket-iam-policy.go new file mode 100644 index 00000000..1f573f50 --- /dev/null +++ b/sources/gcp/manual/storage-bucket-iam-policy.go @@ -0,0 +1,393 @@ +package manual + +import ( + "context" + "strings" + + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/sources" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// Storage Bucket IAM Policy adapter: one item per bucket representing the bucket's full IAM policy. +// Uses the Storage Bucket getIamPolicy V3 API. All Terraform bucket IAM resources (binding, member, policy) map to this item. +// See: https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy + +var ( + StorageBucketIAMPolicyLookupByBucket = shared.NewItemTypeLookup("bucket", gcpshared.StorageBucketIAMPolicy) +) + +type storageBucketIAMPolicyWrapper struct { + client gcpshared.StorageBucketIAMPolicyGetter + *gcpshared.ProjectBase +} + +// NewStorageBucketIAMPolicy creates a SearchableWrapper for Storage Bucket IAM policy (one item per bucket). +func NewStorageBucketIAMPolicy(client gcpshared.StorageBucketIAMPolicyGetter, locations []gcpshared.LocationInfo) sources.SearchableWrapper { + return &storageBucketIAMPolicyWrapper{ + client: client, + ProjectBase: gcpshared.NewProjectBase( + locations, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + gcpshared.StorageBucketIAMPolicy, + ), + } +} + +func (w *storageBucketIAMPolicyWrapper) IAMPermissions() []string { + return []string{"storage.buckets.getIamPolicy"} +} + +func (w *storageBucketIAMPolicyWrapper) PredefinedRole() string { + return "overmind_custom_role" +} + +func (w *storageBucketIAMPolicyWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + gcpshared.StorageBucket, + gcpshared.IAMServiceAccount, + gcpshared.IAMRole, + gcpshared.ComputeProject, + stdlib.NetworkDNS, + ) +} + +func (w *storageBucketIAMPolicyWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_binding.bucket", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_member.bucket", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_storage_bucket_iam_policy.bucket", + }, + } +} + +func (w *storageBucketIAMPolicyWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + StorageBucketIAMPolicyLookupByBucket, + } +} + +func (w *storageBucketIAMPolicyWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {StorageBucketIAMPolicyLookupByBucket}, + } +} + +func (w *storageBucketIAMPolicyWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + location, err := w.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + if len(queryParts) < 1 || queryParts[0] == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "GET requires bucket name", + } + } + bucketName := queryParts[0] + + bindings, getErr := w.client.GetBucketIAMPolicy(ctx, bucketName) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, w.Type()) + } + + return w.policyToItem(location, bucketName, bindings) +} + +func (w *storageBucketIAMPolicyWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + location, err := w.LocationFromScope(scope) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: err.Error(), + } + } + if len(queryParts) < 1 || queryParts[0] == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "SEARCH requires bucket name", + } + } + bucketName := queryParts[0] + + bindings, getErr := w.client.GetBucketIAMPolicy(ctx, bucketName) + if getErr != nil { + return nil, gcpshared.QueryError(getErr, scope, w.Type()) + } + + item, qErr := w.policyToItem(location, bucketName, bindings) + if qErr != nil { + return nil, qErr + } + return []*sdp.Item{item}, nil +} + +// policyBinding is the serialized shape of one binding in the policy item attributes. +type policyBinding struct { + Role string `json:"role"` + Members []string `json:"members"` + ConditionExpression string `json:"conditionExpression,omitempty"` + ConditionTitle string `json:"conditionTitle,omitempty"` + ConditionDescription string `json:"conditionDescription,omitempty"` +} + +// policyToItem builds one SDP item for the bucket's IAM policy and adds linked item queries from all bindings. +func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.LocationInfo, bucketName string, bindings []gcpshared.BucketIAMBinding) (*sdp.Item, *sdp.QueryError) { + policyBindings := make([]policyBinding, 0, len(bindings)) + for _, b := range bindings { + policyBindings = append(policyBindings, policyBinding{ + Role: b.Role, + Members: b.Members, + ConditionExpression: b.ConditionExpression, + ConditionTitle: b.ConditionTitle, + ConditionDescription: b.ConditionDescription, + }) + } + + type policyAttrs struct { + Bucket string `json:"bucket"` + Bindings []policyBinding `json:"bindings"` + } + attrs, err := shared.ToAttributesWithExclude(policyAttrs{Bucket: bucketName, Bindings: policyBindings}) + if err != nil { + return nil, gcpshared.QueryError(err, location.ToScope(), w.Type()) + } + if err = attrs.Set("uniqueAttr", bucketName); err != nil { + return nil, gcpshared.QueryError(err, location.ToScope(), w.Type()) + } + + item := &sdp.Item{ + Type: gcpshared.StorageBucketIAMPolicy.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attrs, + Scope: location.ToScope(), + } + + // Link to StorageBucket (In: true, Out: true) + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.StorageBucket.String(), + Method: sdp.QueryMethod_GET, + Query: bucketName, + Scope: location.ProjectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + + // Collect unique linked SAs, projects, domains, and custom IAM roles across all bindings. + linkedSAs := make(map[string]string) // email -> projectID + linkedProjects := make(map[string]struct{}) + linkedDomains := make(map[string]struct{}) + linkedRoles := make(map[string]map[string]struct{}) // projectID -> set of roleIDs + + for _, b := range bindings { + // Custom roles are in the form projects/{project}/roles/{roleId}; predefined roles are roles/... + if projectID, roleID := extractCustomRoleProjectAndID(b.Role); projectID != "" && roleID != "" { + if linkedRoles[projectID] == nil { + linkedRoles[projectID] = make(map[string]struct{}) + } + linkedRoles[projectID][roleID] = struct{}{} + } + for _, member := range b.Members { + saEmail := extractServiceAccountEmailFromMember(member) + if saEmail != "" { + projectID := extractProjectFromServiceAccountEmail(saEmail) + if projectID != "" && !isGoogleManagedServiceAccountDomain(projectID) { + linkedSAs[saEmail] = projectID + } + } + projectID := extractProjectIDFromProjectPrincipalMember(member) + if projectID != "" { + linkedProjects[projectID] = struct{}{} + } + domainName := extractDomainFromDomainMember(member) + if domainName != "" { + linkedDomains[domainName] = struct{}{} + } + } + } + + for saEmail, projectID := range linkedSAs { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.IAMServiceAccount.String(), + Method: sdp.QueryMethod_GET, + Query: saEmail, + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + for projectID, roleIDs := range linkedRoles { + for roleID := range roleIDs { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.IAMRole.String(), + Method: sdp.QueryMethod_GET, + Query: roleID, + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + } + for projectID := range linkedProjects { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeProject.String(), + Method: sdp.QueryMethod_GET, + Query: projectID, + Scope: projectID, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + for domainName := range linkedDomains { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: domainName, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }) + } + + return item, nil +} + +// extractCustomRoleProjectAndID parses a custom IAM role reference "projects/{project}/roles/{roleId}" +// and returns (projectID, roleID). For predefined roles (e.g. "roles/storage.objectViewer") returns ("", ""). +func extractCustomRoleProjectAndID(role string) (projectID, roleID string) { + const prefix = "projects/" + const suffix = "/roles/" + if !strings.HasPrefix(role, prefix) || !strings.Contains(role, suffix) { + return "", "" + } + rest := strings.TrimPrefix(role, prefix) + idx := strings.Index(rest, suffix) + if idx == -1 { + return "", "" + } + projectID = rest[:idx] + roleID = rest[idx+len(suffix):] + if projectID == "" || roleID == "" { + return "", "" + } + return projectID, roleID +} + +// extractDomainFromDomainMember returns the domain for "domain:example.com" or +// "deleted:domain:example.com", or "" otherwise. The value is a DNS name. +// For deleted members, any "?uid=..." suffix is stripped so the result is a valid DNS link. +func extractDomainFromDomainMember(member string) string { + var domain string + if strings.HasPrefix(member, "deleted:domain:") { + domain = strings.TrimPrefix(member, "deleted:domain:") + } else if strings.HasPrefix(member, "domain:") { + domain = strings.TrimPrefix(member, "domain:") + } else { + return "" + } + // Deleted domain members can include "?uid=123456789"; strip so link uses the actual domain. + if idx := strings.Index(domain, "?"); idx != -1 { + domain = domain[:idx] + } + return domain +} + +// extractProjectIDFromProjectPrincipalMember returns the project ID for project principal members +// (projectOwner:projectId, projectEditor:projectId, projectViewer:projectId), or "" otherwise. +func extractProjectIDFromProjectPrincipalMember(member string) string { + for _, prefix := range []string{"projectOwner:", "projectEditor:", "projectViewer:"} { + if strings.HasPrefix(member, prefix) { + return strings.TrimPrefix(member, prefix) + } + } + return "" +} + +// extractServiceAccountEmailFromMember returns the email for "serviceAccount:email" or "deleted:serviceAccount:email", or "" if not a service account member. +// For deleted members, any "?uid=..." suffix is stripped so the result is a valid IAMServiceAccount lookup query (email only). +func extractServiceAccountEmailFromMember(member string) string { + var email string + if strings.HasPrefix(member, "deleted:serviceAccount:") { + email = strings.TrimPrefix(member, "deleted:serviceAccount:") + } else if strings.HasPrefix(member, "serviceAccount:") { + email = strings.TrimPrefix(member, "serviceAccount:") + } else { + return "" + } + // Deleted SAs can include "?uid=123456789"; strip query part so link uses the actual SA email. + if idx := strings.Index(email, "?"); idx != -1 { + email = email[:idx] + } + return email +} + +// extractProjectFromServiceAccountEmail extracts project ID from "name@project.iam.gserviceaccount.com". +// Only project-scoped SAs use that domain; developer.gserviceaccount.com and appspot.gserviceaccount.com +// use a shared domain where the first label is not a project ID, so we return "" to avoid invalid links. +// For Google-managed SAs (e.g. name@gcp-sa-logging.iam.gserviceaccount.com) use isGoogleManagedServiceAccountDomain to skip. +func extractProjectFromServiceAccountEmail(email string) string { + at := strings.Index(email, "@") + if at == -1 { + return "" + } + domain := email[at+1:] + // Only use first label as project when domain is project.iam.gserviceaccount.com. + // developer.gserviceaccount.com and appspot.gserviceaccount.com must not be treated as project IDs. + if !strings.HasSuffix(domain, ".iam.gserviceaccount.com") { + return "" + } + dot := strings.Index(domain, ".") + if dot == -1 { + return "" + } + return domain[:dot] +} + +// isGoogleManagedServiceAccountDomain reports whether the domain's first label is a known +// Google-managed pattern (not a customer project ID). Such SAs cannot be resolved to a +// project-scoped IAMServiceAccount item with a valid Scope. +func isGoogleManagedServiceAccountDomain(firstLabel string) bool { + // gcp-sa-* (e.g. gcp-sa-logging, gcp-sa-datalabeling) + if strings.HasPrefix(firstLabel, "gcp-sa-") { + return true + } + // cloudservices.gserviceaccount.com, gs-project-accounts, system.gserviceaccount.com + switch firstLabel { + case "cloudservices", "gs-project-accounts", "system": + return true + } + return false +} diff --git a/sources/gcp/manual/storage-bucket-iam-policy_test.go b/sources/gcp/manual/storage-bucket-iam-policy_test.go new file mode 100644 index 00000000..a363d953 --- /dev/null +++ b/sources/gcp/manual/storage-bucket-iam-policy_test.go @@ -0,0 +1,552 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/gcp/manual" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" + "github.com/overmindtech/cli/sources/shared" +) + +// fakeBucketIAMPolicyGetter returns a fixed list of bindings for testing. +type fakeBucketIAMPolicyGetter struct { + bindings []gcpshared.BucketIAMBinding + returnErr error + bucketSeen string +} + +func (f *fakeBucketIAMPolicyGetter) GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]gcpshared.BucketIAMBinding, error) { + f.bucketSeen = bucketName + if f.returnErr != nil { + return nil, f.returnErr + } + return f.bindings, nil +} + +// policyWithBindings builds []BucketIAMBinding from role -> members (no condition). +// For conditional bindings, construct []BucketIAMBinding directly. +func policyWithBindings(bindings map[string][]string) []gcpshared.BucketIAMBinding { + out := make([]gcpshared.BucketIAMBinding, 0, len(bindings)) + for role, members := range bindings { + out = append(out, gcpshared.BucketIAMBinding{Role: role, Members: members, ConditionExpression: ""}) + } + return out +} + +func TestStorageBucketIAMPolicy_Get(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + bucketName := "my-bucket" + role := "roles/storage.objectViewer" + saMember := "serviceAccount:siem-sa@test-project.iam.gserviceaccount.com" + + bindings := policyWithBindings(map[string][]string{ + role: {saMember, "user:alice@example.com"}, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + scope := projectID + sdpItem, qErr := adapter.Get(ctx, scope, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + if sdpItem.GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("type: got %s, want %s", sdpItem.GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + if getter.bucketSeen != bucketName { + t.Errorf("bucket seen: got %s, want %s", getter.bucketSeen, bucketName) + } + + // Policy item has bucket and bindings attributes + if ua, _ := sdpItem.GetAttributes().Get("uniqueAttr"); ua != bucketName { + t.Errorf("uniqueAttr: got %v, want %s", ua, bucketName) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.StorageBucket.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + { + ExpectedType: gcpshared.IAMServiceAccount.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "siem-sa@test-project.iam.gserviceaccount.com", + ExpectedScope: projectID, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) +} + +func TestStorageBucketIAMPolicy_Get_ProjectPrincipalMembers_Linked(t *testing.T) { + ctx := context.Background() + projectID := "bucket-project" + bucketName := "my-bucket" + role := "roles/storage.objectViewer" + bindings := policyWithBindings(map[string][]string{ + role: { + "projectOwner:other-project", + "projectEditor:another-project", + "projectViewer:bucket-project", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: gcpshared.StorageBucket.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: bucketName, + ExpectedScope: projectID, + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "other-project", + ExpectedScope: "other-project", + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "another-project", + ExpectedScope: "another-project", + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }, + { + ExpectedType: gcpshared.ComputeProject.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "bucket-project", + ExpectedScope: "bucket-project", + ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) +} + +func TestStorageBucketIAMPolicy_Get_ProjectPrincipalMembers_Deduplicated(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.admin": { + "projectOwner:shared-project", + "projectEditor:shared-project", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var projectLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.ComputeProject.String() { + projectLinks++ + if q.GetQuery().GetQuery() != "shared-project" || q.GetQuery().GetScope() != "shared-project" { + t.Errorf("ComputeProject link: got query=%q scope=%q, want shared-project", q.GetQuery().GetQuery(), q.GetQuery().GetScope()) + } + } + } + if projectLinks != 1 { + t.Errorf("expected 1 ComputeProject link (deduplicated), got %d", projectLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DeletedServiceAccount_IsLinked(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "deleted:serviceAccount:old-sa@my-project.iam.gserviceaccount.com?uid=123456789", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + if q.GetQuery().GetScope() != "my-project" { + t.Errorf("IAM link scope: got %q, want my-project", q.GetQuery().GetScope()) + } + } + } + if iamLinks != 1 { + t.Errorf("expected 1 IAMServiceAccount link for deleted:serviceAccount: member, got %d", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DomainMembers_EmitDNSLinks(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "domain:example.com", + "domain:acme.co.uk", + "domain:example.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var dnsLinks int + dnsQueries := make(map[string]struct{}) + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == "dns" { + dnsLinks++ + dnsQueries[q.GetQuery().GetQuery()] = struct{}{} + if q.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH || q.GetQuery().GetScope() != "global" { + t.Errorf("dns link: method=%v scope=%q (want SEARCH, global)", q.GetQuery().GetMethod(), q.GetQuery().GetScope()) + } + } + } + if dnsLinks != 2 { + t.Errorf("expected 2 dns links (example.com, acme.co.uk; example.com deduped), got %d", dnsLinks) + } + if _, ok := dnsQueries["example.com"]; !ok { + t.Error("missing dns link for example.com") + } + if _, ok := dnsQueries["acme.co.uk"]; !ok { + t.Error("missing dns link for acme.co.uk") + } +} + +func TestStorageBucketIAMPolicy_Get_DeletedDomainMember_StripsUIDSuffix(t *testing.T) { + // deleted:domain:example.com?uid=123456789 should produce a DNS link with query "example.com", not "example.com?uid=123456789". + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "deleted:domain:example.com?uid=123456789", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var dnsLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == "dns" { + dnsLinks++ + query := q.GetQuery().GetQuery() + if query != "example.com" { + t.Errorf("dns link query: got %q, want example.com (?uid= suffix must be stripped)", query) + } + } + } + if dnsLinks != 1 { + t.Errorf("expected 1 dns link, got %d", dnsLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_CustomRole_EmitsIAMRoleLink(t *testing.T) { + // Bindings that reference custom IAM roles (projects/{project}/roles/{roleId}) should emit LinkedItemQuery to IAMRole. + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := []gcpshared.BucketIAMBinding{ + { + Role: "projects/custom-project/roles/myCustomRole", + Members: []string{"user:admin@example.com"}, + ConditionExpression: "", + ConditionTitle: "", + ConditionDescription: "", + }, + { + Role: "roles/storage.objectViewer", + Members: []string{"user:viewer@example.com"}, + ConditionExpression: "", + ConditionTitle: "", + ConditionDescription: "", + }, + } + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamRoleLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMRole.String() { + iamRoleLinks++ + if q.GetQuery().GetScope() != "custom-project" || q.GetQuery().GetQuery() != "myCustomRole" { + t.Errorf("IAMRole link: got scope=%q query=%q, want scope=custom-project query=myCustomRole", q.GetQuery().GetScope(), q.GetQuery().GetQuery()) + } + } + } + if iamRoleLinks != 1 { + t.Errorf("expected 1 IAMRole link for custom role, got %d", iamRoleLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_GoogleManagedSA_SkipsLink(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "serviceAccount:my-sa@my-project.iam.gserviceaccount.com", + "serviceAccount:123456@gcp-sa-logging.iam.gserviceaccount.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + if q.GetQuery().GetScope() != "my-project" || q.GetQuery().GetQuery() != "my-sa@my-project.iam.gserviceaccount.com" { + t.Errorf("IAM link: scope=%q query=%q (expected customer SA only)", q.GetQuery().GetScope(), q.GetQuery().GetQuery()) + } + } + } + if iamLinks != 1 { + t.Errorf("expected 1 IAMServiceAccount link (customer SA), got %d (Google-managed SA should be skipped)", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_DeveloperAndAppspotSA_SkipLink(t *testing.T) { + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": { + "serviceAccount:123456@developer.gserviceaccount.com", + "serviceAccount:my-app@appspot.gserviceaccount.com", + }, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed: %v", qErr) + return + } + + var iamLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.IAMServiceAccount.String() { + iamLinks++ + scope := q.GetQuery().GetScope() + if scope == "developer" || scope == "appspot" { + t.Errorf("must not create IAM link with scope %q (not a project ID)", scope) + } + } + } + if iamLinks != 0 { + t.Errorf("expected 0 IAMServiceAccount links for developer/appspot SAs, got %d", iamLinks) + } +} + +func TestStorageBucketIAMPolicy_Get_ClientError(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + getter := &fakeBucketIAMPolicyGetter{returnErr: errors.New("api error"), bindings: nil} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, projectID, "my-bucket", true) + if qErr == nil { + t.Error("expected error when getter returns error") + return + } +} + +func TestStorageBucketIAMPolicy_Search(t *testing.T) { + ctx := context.Background() + projectID := "test-project" + bucketName := "my-bucket" + bindings := policyWithBindings(map[string][]string{ + "roles/storage.objectViewer": {"serviceAccount:sa1@test-project.iam.gserviceaccount.com"}, + "roles/storage.admin": {"user:admin@example.com"}, + }) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("adapter does not implement SearchableAdapter") + return + } + + items, qErr := searchable.Search(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Search failed: %v", qErr) + return + } + + if len(items) != 1 { + t.Errorf("Search: got %d items, want 1 (one policy per bucket)", len(items)) + } + if getter.bucketSeen != bucketName { + t.Errorf("bucket seen: got %s, want %s", getter.bucketSeen, bucketName) + } + + if len(items) > 0 { + if err := items[0].Validate(); err != nil { + t.Errorf("item validation: %v", err) + } + if items[0].GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("Search item type: got %s, want %s", items[0].GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + } +} + +func TestStorageBucketIAMPolicy_TerraformMapping(t *testing.T) { + bindings := policyWithBindings(map[string][]string{"roles/storage.objectViewer": {"user:u@example.com"}}) + getter := &fakeBucketIAMPolicyGetter{bindings: bindings} + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation("p")}) + + mappings := wrapper.TerraformMappings() + wantMaps := map[string]bool{ + "google_storage_bucket_iam_binding.bucket": false, + "google_storage_bucket_iam_member.bucket": false, + "google_storage_bucket_iam_policy.bucket": false, + } + if len(mappings) != 3 { + t.Errorf("TerraformMappings: got %d entries, want 3", len(mappings)) + return + } + for _, m := range mappings { + if m.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("TerraformMethod: got %v, want GET", m.GetTerraformMethod()) + } + qm := m.GetTerraformQueryMap() + if _, ok := wantMaps[qm]; !ok { + t.Errorf("TerraformQueryMap: unexpected %q", qm) + } + wantMaps[qm] = true + } + for qm, seen := range wantMaps { + if !seen { + t.Errorf("TerraformQueryMap: missing %q", qm) + } + } +} + +func TestStorageBucketIAMPolicy_Get_InsufficientQueryParts(t *testing.T) { + ctx := context.Background() + getter := &fakeBucketIAMPolicyGetter{bindings: nil} + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation("p")}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Get with empty query should fail (no bucket name) + _, qErr := adapter.Get(ctx, "p", "", true) + if qErr == nil { + t.Error("expected error when query is empty (no bucket name)") + return + } +} + +func TestStorageBucketIAMPolicy_Get_EmptyPolicy_ReturnsItem(t *testing.T) { + // Bucket with no bindings still returns a valid policy item (empty bindings array). + ctx := context.Background() + projectID := "my-project" + bucketName := "my-bucket" + getter := &fakeBucketIAMPolicyGetter{bindings: []gcpshared.BucketIAMBinding{}} + + wrapper := manual.NewStorageBucketIAMPolicy(getter, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, projectID, bucketName, true) + if qErr != nil { + t.Errorf("Get failed for empty policy: %v", qErr) + return + } + if sdpItem.GetType() != gcpshared.StorageBucketIAMPolicy.String() { + t.Errorf("type: got %s, want %s", sdpItem.GetType(), gcpshared.StorageBucketIAMPolicy.String()) + } + // Should still link to the bucket + var bucketLinks int + for _, q := range sdpItem.GetLinkedItemQueries() { + if q.GetQuery().GetType() == gcpshared.StorageBucket.String() { + bucketLinks++ + } + } + if bucketLinks != 1 { + t.Errorf("expected 1 StorageBucket link, got %d", bucketLinks) + } +} diff --git a/sources/gcp/shared/item-types.go b/sources/gcp/shared/item-types.go index 0ebcd2a7..9b3e6ccb 100644 --- a/sources/gcp/shared/item-types.go +++ b/sources/gcp/shared/item-types.go @@ -66,6 +66,7 @@ var ( StorageBucketAccessControl = shared.NewItemType(GCP, Storage, BucketAccessControl) StorageDefaultObjectAccessControl = shared.NewItemType(GCP, Storage, DefaultObjectAccessControl) StorageNotificationConfig = shared.NewItemType(GCP, Storage, NotificationConfig) + StorageBucketIAMPolicy = shared.NewItemType(GCP, Storage, BucketIAMPolicy) ComputeNetworkAttachment = shared.NewItemType(GCP, Compute, NetworkAttachment) ComputeStoragePool = shared.NewItemType(GCP, Compute, StoragePool) ComputeStoragePoolType = shared.NewItemType(GCP, Compute, StoragePoolType) diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index 27c7e8dd..b1cba0d5 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -1034,6 +1034,24 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem return nil }, + // StorageBucketIAMPolicy: link by bucket name using GET (one policy item per bucket). + StorageBucketIAMPolicy: func(projectID, _, query string) *sdp.LinkedItemQuery { + bucketName := query + if idx := strings.Index(query, "/"); idx != -1 { + bucketName = query[:idx] + } + if projectID == "" || bucketName == "" { + return nil + } + return &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: StorageBucketIAMPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: bucketName, + Scope: projectID, + }, + } + }, // OrgPolicyPolicy name field can reference parent project, folder, or organization // This linker is registered for all three parent types since the name field can reference any of them // Format: projects/{project_number}/policies/{constraint} or diff --git a/sources/gcp/shared/models.go b/sources/gcp/shared/models.go index 23d62ca5..337480bd 100644 --- a/sources/gcp/shared/models.go +++ b/sources/gcp/shared/models.go @@ -115,6 +115,7 @@ const ( KeyRing shared.Resource = "key-ring" InstanceSettings shared.Resource = "instance-settings" Bucket shared.Resource = "bucket" + BucketIAMPolicy shared.Resource = "bucket-iam-policy" BucketAccessControl shared.Resource = "bucket-access-control" DefaultObjectAccessControl shared.Resource = "default-object-access-control" NotificationConfig shared.Resource = "storage-notification-config" diff --git a/sources/gcp/shared/predefined-roles.go b/sources/gcp/shared/predefined-roles.go index 4d481652..f5966f79 100644 --- a/sources/gcp/shared/predefined-roles.go +++ b/sources/gcp/shared/predefined-roles.go @@ -43,14 +43,15 @@ var PredefinedRoles = map[string]role{ }, }, "overmind_custom_role": { - // This is a custom role for Overmind service account with additional BigQuery and Spanner permissions - // It is created in deploy/sources.tf + // Custom role for Overmind with permissions not available in a single least-privilege predefined role. + // Created in deploy/sources.tf. Includes read-only Storage Bucket IAM (getIamPolicy) and BigQuery/Spanner extras. Role: "overmind_custom_role", Link: "deploy/sources.tf", IAMPermissions: []string{ "bigquery.transfers.get", "spanner.databases.get", "spanner.databases.list", + "storage.buckets.getIamPolicy", }, }, "roles/bigquery.metadataViewer": { diff --git a/sources/gcp/shared/storage-iam.go b/sources/gcp/shared/storage-iam.go new file mode 100644 index 00000000..9796a512 --- /dev/null +++ b/sources/gcp/shared/storage-iam.go @@ -0,0 +1,67 @@ +package shared + +import ( + "context" + "errors" + + "cloud.google.com/go/storage" +) + +// ErrStorageClientNotInitialized is returned when the Storage client was not initialized (e.g. when enumerating adapters without initGCPClients). +var ErrStorageClientNotInitialized = errors.New("storage client not initialized") + +// BucketIAMBinding represents one IAM binding (role + members, optionally with a condition) in a bucket's policy. +// The adapter emits one item per bucket (the full policy); bindings are serialized in that item's bindings array. +type BucketIAMBinding struct { + Role string + Members []string + ConditionExpression string // CEL expression; empty if no condition + ConditionTitle string // optional; empty if no condition or not set + ConditionDescription string // optional; empty if no condition or not set +} + +// StorageBucketIAMPolicyGetter retrieves the IAM policy for a GCS bucket as a slice of bindings. +// See: https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy +type StorageBucketIAMPolicyGetter interface { + GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]BucketIAMBinding, error) +} + +// storageBucketIAMPolicyGetterImpl implements StorageBucketIAMPolicyGetter using the Storage client. +type storageBucketIAMPolicyGetterImpl struct { + client *storage.Client +} + +// GetBucketIAMPolicy returns the IAM policy for the given bucket. +func (g *storageBucketIAMPolicyGetterImpl) GetBucketIAMPolicy(ctx context.Context, bucketName string) ([]BucketIAMBinding, error) { + if g.client == nil { + return nil, ErrStorageClientNotInitialized + } + policy3, err := g.client.Bucket(bucketName).IAM().V3().Policy(ctx) + if err != nil { + return nil, err + } + out := make([]BucketIAMBinding, 0, len(policy3.Bindings)) + for _, b := range policy3.Bindings { + condExpr := "" + condTitle := "" + condDesc := "" + if b.GetCondition() != nil { + condExpr = b.GetCondition().GetExpression() + condTitle = b.GetCondition().GetTitle() + condDesc = b.GetCondition().GetDescription() + } + out = append(out, BucketIAMBinding{ + Role: b.GetRole(), + Members: b.GetMembers(), + ConditionExpression: condExpr, + ConditionTitle: condTitle, + ConditionDescription: condDesc, + }) + } + return out, nil +} + +// NewStorageBucketIAMPolicyGetter creates a getter that uses the given Storage client. +func NewStorageBucketIAMPolicyGetter(client *storage.Client) StorageBucketIAMPolicyGetter { + return &storageBucketIAMPolicyGetterImpl{client: client} +} From d798beb4355ceaf612bd12b6db2274ea3d5914d6 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 17 Feb 2026 09:33:44 +0100 Subject: [PATCH 25/51] Go libraries directory structure (#3901) This pull request contains changes generated by a Cursor Cloud Agent

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Broad, mechanical import/path changes across many packages and CI steps; main risk is missed references causing build/test or workflow failures rather than behavioral changes. > > **Overview** > Updates the repo to use the new Go library layout under `go/` by rewriting imports throughout `aws-source` (and related tests) from `github.com/overmindtech/workspace/{discovery,sdp-go,sdpcache,tracing,...}` to `github.com/overmindtech/workspace/go/...`. > > Adjusts CI path filters and several workflow steps to run tests/codegen from `go/{discovery,features,sdp-go,sdpcache}` instead of the old top-level directories, and aligns lint/sqlc docs/config (e.g. `.golangci.yml` errcheck exclusion and sqlc override examples) with the new import paths. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 19144a2d7a5fe1fbb753379a00320c85c9dbdc97. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 31d333187a3fc28d5e15e3daf3b44a61af911c60 --- .../adapterhelpers_always_get_source.go | 6 +- .../adapterhelpers_always_get_source_test.go | 6 +- .../adapterhelpers_describe_source.go | 6 +- .../adapterhelpers_describe_source_test.go | 6 +- .../adapterhelpers_get_list_adapter_v2.go | 6 +- ...adapterhelpers_get_list_adapter_v2_test.go | 6 +- .../adapterhelpers_get_list_source.go | 4 +- .../adapterhelpers_get_list_source_test.go | 4 +- .../adapterhelpers_notfound_cache_test.go | 4 +- .../adapters/adapterhelpers_shared_tests.go | 2 +- aws-source/adapters/adapterhelpers_util.go | 4 +- aws-source/adapters/apigateway-api-key.go | 4 +- .../adapters/apigateway-api-key_test.go | 4 +- aws-source/adapters/apigateway-authorizer.go | 4 +- .../adapters/apigateway-authorizer_test.go | 4 +- aws-source/adapters/apigateway-deployment.go | 4 +- .../adapters/apigateway-deployment_test.go | 4 +- aws-source/adapters/apigateway-domain-name.go | 4 +- .../adapters/apigateway-domain-name_test.go | 4 +- aws-source/adapters/apigateway-integration.go | 4 +- .../adapters/apigateway-integration_test.go | 4 +- .../adapters/apigateway-method-response.go | 4 +- .../apigateway-method-response_test.go | 4 +- aws-source/adapters/apigateway-method.go | 4 +- aws-source/adapters/apigateway-method_test.go | 4 +- aws-source/adapters/apigateway-model.go | 4 +- aws-source/adapters/apigateway-model_test.go | 4 +- aws-source/adapters/apigateway-resource.go | 4 +- .../adapters/apigateway-resource_test.go | 2 +- aws-source/adapters/apigateway-rest-api.go | 4 +- .../adapters/apigateway-rest-api_test.go | 4 +- aws-source/adapters/apigateway-stage.go | 4 +- aws-source/adapters/apigateway-stage_test.go | 4 +- .../autoscaling-auto-scaling-group.go | 4 +- .../autoscaling-auto-scaling-group_test.go | 4 +- .../autoscaling-auto-scaling-policy.go | 4 +- .../autoscaling-auto-scaling-policy_test.go | 4 +- .../adapters/cloudfront-cache-policy.go | 4 +- .../adapters/cloudfront-cache-policy_test.go | 2 +- ...cloudfront-continuous-deployment-policy.go | 4 +- ...front-continuous-deployment-policy_test.go | 4 +- .../adapters/cloudfront-distribution.go | 4 +- .../adapters/cloudfront-distribution_test.go | 4 +- aws-source/adapters/cloudfront-function.go | 4 +- .../adapters/cloudfront-function_test.go | 2 +- aws-source/adapters/cloudfront-key-group.go | 4 +- .../adapters/cloudfront-key-group_test.go | 2 +- .../cloudfront-origin-access-control.go | 4 +- .../cloudfront-origin-access-control_test.go | 2 +- .../cloudfront-origin-request-policy.go | 4 +- .../cloudfront-origin-request-policy_test.go | 2 +- .../cloudfront-realtime-log-config.go | 4 +- .../cloudfront-realtime-log-config_test.go | 4 +- .../cloudfront-response-headers-policy.go | 4 +- ...cloudfront-response-headers-policy_test.go | 2 +- .../cloudfront-streaming-distribution.go | 4 +- .../cloudfront-streaming-distribution_test.go | 4 +- aws-source/adapters/cloudwatch-alarm.go | 4 +- aws-source/adapters/cloudwatch-alarm_test.go | 4 +- .../adapters/cloudwatch-instance-metric.go | 4 +- ...dwatch-instance-metric_integration_test.go | 2 +- .../cloudwatch-instance-metric_test.go | 2 +- .../adapters/cloudwatch_metric_links.go | 2 +- .../adapters/directconnect-connection.go | 4 +- .../adapters/directconnect-connection_test.go | 4 +- .../directconnect-customer-metadata.go | 4 +- .../directconnect-customer-metadata_test.go | 2 +- ...ct-connect-gateway-association-proposal.go | 4 +- ...nnect-gateway-association-proposal_test.go | 4 +- ...nect-direct-connect-gateway-association.go | 4 +- ...direct-connect-gateway-association_test.go | 4 +- ...nnect-direct-connect-gateway-attachment.go | 4 +- ...-direct-connect-gateway-attachment_test.go | 4 +- .../directconnect-direct-connect-gateway.go | 4 +- ...rectconnect-direct-connect-gateway_test.go | 4 +- .../directconnect-hosted-connection.go | 4 +- .../directconnect-hosted-connection_test.go | 4 +- .../adapters/directconnect-interconnect.go | 4 +- .../directconnect-interconnect_test.go | 4 +- aws-source/adapters/directconnect-lag.go | 4 +- aws-source/adapters/directconnect-lag_test.go | 4 +- aws-source/adapters/directconnect-location.go | 4 +- .../adapters/directconnect-location_test.go | 2 +- .../directconnect-router-configuration.go | 4 +- ...directconnect-router-configuration_test.go | 4 +- .../adapters/directconnect-virtual-gateway.go | 4 +- .../directconnect-virtual-gateway_test.go | 2 +- .../directconnect-virtual-interface.go | 4 +- .../directconnect-virtual-interface_test.go | 4 +- aws-source/adapters/dynamodb-backup.go | 4 +- aws-source/adapters/dynamodb-backup_test.go | 4 +- aws-source/adapters/dynamodb-table.go | 4 +- aws-source/adapters/dynamodb-table_test.go | 4 +- aws-source/adapters/ec2-address.go | 4 +- aws-source/adapters/ec2-address_test.go | 4 +- .../ec2-capacity-reservation-fleet.go | 4 +- .../ec2-capacity-reservation-fleet_test.go | 2 +- .../adapters/ec2-capacity-reservation.go | 4 +- .../adapters/ec2-capacity-reservation_test.go | 4 +- .../ec2-egress-only-internet-gateway.go | 4 +- .../ec2-egress-only-internet-gateway_test.go | 4 +- .../ec2-iam-instance-profile-association.go | 4 +- ...2-iam-instance-profile-association_test.go | 4 +- aws-source/adapters/ec2-image.go | 4 +- aws-source/adapters/ec2-image_test.go | 2 +- .../adapters/ec2-instance-event-window.go | 4 +- .../ec2-instance-event-window_test.go | 4 +- aws-source/adapters/ec2-instance-status.go | 4 +- .../adapters/ec2-instance-status_test.go | 4 +- aws-source/adapters/ec2-instance.go | 4 +- aws-source/adapters/ec2-instance_test.go | 4 +- aws-source/adapters/ec2-internet-gateway.go | 4 +- .../adapters/ec2-internet-gateway_test.go | 4 +- aws-source/adapters/ec2-key-pair.go | 4 +- aws-source/adapters/ec2-key-pair_test.go | 2 +- .../adapters/ec2-launch-template-version.go | 4 +- .../ec2-launch-template-version_test.go | 4 +- aws-source/adapters/ec2-launch-template.go | 4 +- .../adapters/ec2-launch-template_test.go | 2 +- aws-source/adapters/ec2-nat-gateway.go | 4 +- aws-source/adapters/ec2-nat-gateway_test.go | 4 +- aws-source/adapters/ec2-network-acl.go | 4 +- aws-source/adapters/ec2-network-acl_test.go | 4 +- .../ec2-network-interface-permission.go | 4 +- .../ec2-network-interface-permission_test.go | 4 +- aws-source/adapters/ec2-network-interface.go | 4 +- .../adapters/ec2-network-interface_test.go | 4 +- aws-source/adapters/ec2-placement-group.go | 4 +- .../adapters/ec2-placement-group_test.go | 2 +- aws-source/adapters/ec2-reserved-instance.go | 4 +- .../adapters/ec2-reserved-instance_test.go | 2 +- aws-source/adapters/ec2-route-table.go | 4 +- aws-source/adapters/ec2-route-table_test.go | 4 +- .../adapters/ec2-security-group-rule.go | 4 +- .../adapters/ec2-security-group-rule_test.go | 4 +- aws-source/adapters/ec2-security-group.go | 4 +- .../adapters/ec2-security-group_test.go | 4 +- aws-source/adapters/ec2-snapshot.go | 4 +- aws-source/adapters/ec2-snapshot_test.go | 4 +- aws-source/adapters/ec2-subnet.go | 4 +- aws-source/adapters/ec2-subnet_test.go | 4 +- aws-source/adapters/ec2-volume-status.go | 4 +- aws-source/adapters/ec2-volume-status_test.go | 4 +- aws-source/adapters/ec2-volume.go | 4 +- aws-source/adapters/ec2-volume_test.go | 4 +- aws-source/adapters/ec2-vpc-endpoint.go | 4 +- aws-source/adapters/ec2-vpc-endpoint_test.go | 4 +- .../adapters/ec2-vpc-peering-connection.go | 4 +- .../ec2-vpc-peering-connection_test.go | 4 +- aws-source/adapters/ec2-vpc.go | 4 +- aws-source/adapters/ec2-vpc_test.go | 2 +- aws-source/adapters/ecs-capacity-provider.go | 4 +- .../adapters/ecs-capacity-provider_test.go | 6 +- aws-source/adapters/ecs-cluster.go | 4 +- aws-source/adapters/ecs-cluster_test.go | 4 +- aws-source/adapters/ecs-container-instance.go | 4 +- .../adapters/ecs-container-instance_test.go | 4 +- aws-source/adapters/ecs-service.go | 4 +- aws-source/adapters/ecs-service_test.go | 4 +- aws-source/adapters/ecs-task-definition.go | 4 +- .../adapters/ecs-task-definition_test.go | 4 +- aws-source/adapters/ecs-task.go | 4 +- aws-source/adapters/ecs-task_test.go | 4 +- aws-source/adapters/efs-access-point.go | 4 +- aws-source/adapters/efs-access-point_test.go | 4 +- aws-source/adapters/efs-backup-policy.go | 4 +- aws-source/adapters/efs-file-system.go | 4 +- aws-source/adapters/efs-file-system_test.go | 4 +- aws-source/adapters/efs-mount-target.go | 4 +- aws-source/adapters/efs-mount-target_test.go | 2 +- .../adapters/efs-replication-configuration.go | 4 +- .../efs-replication-configuration_test.go | 2 +- aws-source/adapters/efs.go | 2 +- aws-source/adapters/eks-addon.go | 4 +- aws-source/adapters/eks-addon_test.go | 2 +- aws-source/adapters/eks-cluster.go | 4 +- aws-source/adapters/eks-cluster_test.go | 4 +- aws-source/adapters/eks-fargate-profile.go | 4 +- .../adapters/eks-fargate-profile_test.go | 4 +- aws-source/adapters/eks-nodegroup.go | 4 +- aws-source/adapters/eks-nodegroup_test.go | 4 +- aws-source/adapters/elb-instance-health.go | 4 +- .../adapters/elb-instance-health_test.go | 2 +- aws-source/adapters/elb-load-balancer.go | 4 +- aws-source/adapters/elb-load-balancer_test.go | 2 +- aws-source/adapters/elbv2-listener.go | 4 +- aws-source/adapters/elbv2-listener_test.go | 2 +- aws-source/adapters/elbv2-load-balancer.go | 4 +- .../adapters/elbv2-load-balancer_test.go | 2 +- aws-source/adapters/elbv2-rule.go | 4 +- aws-source/adapters/elbv2-rule_test.go | 6 +- aws-source/adapters/elbv2-target-group.go | 4 +- .../adapters/elbv2-target-group_test.go | 4 +- aws-source/adapters/elbv2-target-health.go | 4 +- .../adapters/elbv2-target-health_test.go | 2 +- aws-source/adapters/elbv2.go | 2 +- aws-source/adapters/elbv2_test.go | 2 +- aws-source/adapters/iam-group.go | 4 +- aws-source/adapters/iam-group_test.go | 2 +- aws-source/adapters/iam-instance-profile.go | 4 +- .../adapters/iam-instance-profile_test.go | 2 +- aws-source/adapters/iam-policy.go | 4 +- aws-source/adapters/iam-policy_test.go | 6 +- aws-source/adapters/iam-role.go | 4 +- aws-source/adapters/iam-role_test.go | 6 +- aws-source/adapters/iam-user.go | 4 +- aws-source/adapters/iam-user_test.go | 6 +- aws-source/adapters/iam.go | 2 +- aws-source/adapters/iam_test.go | 2 +- .../integration/apigateway/apigateway_test.go | 4 +- .../adapters/integration/ec2/instance_test.go | 6 +- .../adapters/integration/kms/kms_test.go | 6 +- .../networkmanager/networkmanager_test.go | 6 +- .../adapters/integration/ssm/main_test.go | 6 +- aws-source/adapters/integration/util.go | 4 +- aws-source/adapters/kms-alias.go | 4 +- aws-source/adapters/kms-alias_test.go | 4 +- aws-source/adapters/kms-custom-key-store.go | 4 +- .../adapters/kms-custom-key-store_test.go | 4 +- aws-source/adapters/kms-grant.go | 4 +- aws-source/adapters/kms-grant_test.go | 4 +- aws-source/adapters/kms-key-policy.go | 4 +- aws-source/adapters/kms-key-policy_test.go | 4 +- aws-source/adapters/kms-key.go | 4 +- aws-source/adapters/kms-key_test.go | 2 +- .../adapters/lambda-event-source-mapping.go | 4 +- .../lambda-event-source-mapping_test.go | 4 +- aws-source/adapters/lambda-function.go | 4 +- aws-source/adapters/lambda-function_test.go | 4 +- aws-source/adapters/lambda-layer-version.go | 4 +- .../adapters/lambda-layer-version_test.go | 4 +- aws-source/adapters/lambda-layer.go | 4 +- aws-source/adapters/lambda-layer_test.go | 4 +- aws-source/adapters/main.go | 2 +- .../network-firewall-firewall-policy.go | 4 +- .../network-firewall-firewall-policy_test.go | 2 +- .../adapters/network-firewall-firewall.go | 4 +- .../network-firewall-firewall_test.go | 2 +- .../adapters/network-firewall-rule-group.go | 4 +- .../network-firewall-rule-group_test.go | 2 +- ...k-firewall-tls-inspection-configuration.go | 4 +- ...ewall-tls-inspection-configuration_test.go | 2 +- aws-source/adapters/networkfirewall.go | 2 +- .../networkmanager-connect-attachment.go | 4 +- .../networkmanager-connect-attachment_test.go | 2 +- ...networkmanager-connect-peer-association.go | 4 +- ...rkmanager-connect-peer-association_test.go | 2 +- .../adapters/networkmanager-connect-peer.go | 4 +- .../networkmanager-connect-peer_test.go | 2 +- .../adapters/networkmanager-connection.go | 4 +- .../networkmanager-connection_test.go | 4 +- .../networkmanager-core-network-policy.go | 4 +- ...networkmanager-core-network-policy_test.go | 2 +- .../adapters/networkmanager-core-network.go | 4 +- .../networkmanager-core-network_test.go | 2 +- aws-source/adapters/networkmanager-device.go | 4 +- .../adapters/networkmanager-device_test.go | 4 +- .../adapters/networkmanager-global-network.go | 4 +- .../networkmanager-global-network_test.go | 2 +- .../networkmanager-link-association.go | 4 +- .../networkmanager-link-association_test.go | 2 +- aws-source/adapters/networkmanager-link.go | 4 +- .../adapters/networkmanager-link_test.go | 4 +- ...rkmanager-network-resource-relationship.go | 4 +- ...ager-network-resource-relationship_test.go | 2 +- ...workmanager-site-to-site-vpn-attachment.go | 4 +- ...anager-site-to-site-vpn-attachment_test.go | 2 +- aws-source/adapters/networkmanager-site.go | 4 +- .../adapters/networkmanager-site_test.go | 4 +- ...ransit-gateway-connect-peer-association.go | 4 +- ...t-gateway-connect-peer-association_test.go | 2 +- .../networkmanager-transit-gateway-peering.go | 4 +- ...orkmanager-transit-gateway-peering_test.go | 2 +- ...orkmanager-transit-gateway-registration.go | 4 +- ...nager-transit-gateway-registration_test.go | 2 +- ...-transit-gateway-route-table-attachment.go | 4 +- ...sit-gateway-route-table-attachment_test.go | 2 +- .../adapters/networkmanager-vpc-attachment.go | 4 +- .../networkmanager-vpc-attachment_test.go | 2 +- .../rds-db-cluster-parameter-group.go | 4 +- .../rds-db-cluster-parameter-group_test.go | 2 +- aws-source/adapters/rds-db-cluster.go | 4 +- aws-source/adapters/rds-db-cluster_test.go | 4 +- aws-source/adapters/rds-db-instance.go | 4 +- aws-source/adapters/rds-db-instance_test.go | 4 +- aws-source/adapters/rds-db-parameter-group.go | 4 +- .../adapters/rds-db-parameter-group_test.go | 2 +- aws-source/adapters/rds-db-subnet-group.go | 4 +- .../adapters/rds-db-subnet-group_test.go | 4 +- aws-source/adapters/rds-option-group.go | 4 +- aws-source/adapters/route53-health-check.go | 4 +- .../adapters/route53-health-check_test.go | 4 +- aws-source/adapters/route53-hosted-zone.go | 4 +- .../adapters/route53-hosted-zone_test.go | 4 +- .../adapters/route53-resource-record-set.go | 4 +- .../route53-resource-record-set_test.go | 4 +- aws-source/adapters/s3.go | 4 +- aws-source/adapters/s3_test.go | 4 +- .../adapters/sns-data-protection-policy.go | 4 +- .../sns-data-protection-policy_test.go | 2 +- aws-source/adapters/sns-endpoint.go | 4 +- aws-source/adapters/sns-endpoint_test.go | 2 +- .../adapters/sns-platform-application.go | 4 +- .../adapters/sns-platform-application_test.go | 2 +- aws-source/adapters/sns-subscription.go | 4 +- aws-source/adapters/sns-subscription_test.go | 2 +- aws-source/adapters/sns-topic.go | 4 +- aws-source/adapters/sns-topic_test.go | 2 +- aws-source/adapters/sqs-queue.go | 4 +- aws-source/adapters/sqs-queue_test.go | 4 +- aws-source/adapters/ssm-parameter.go | 4 +- aws-source/adapters/ssm-parameter_test.go | 4 +- aws-source/build/package/Dockerfile | 2 +- aws-source/cmd/root.go | 6 +- aws-source/proc/proc.go | 4 +- aws-source/proc/proc_test.go | 4 +- cmd/auth_client.go | 8 +- cmd/auth_client_test.go | 4 +- cmd/bookmarks_create_bookmark.go | 2 +- cmd/bookmarks_get_affected_bookmarks.go | 2 +- cmd/bookmarks_get_bookmark.go | 2 +- cmd/changes_end_change.go | 2 +- cmd/changes_get_change.go | 2 +- cmd/changes_get_change_test.go | 2 +- cmd/changes_get_signals.go | 2 +- cmd/changes_list_changes.go | 2 +- cmd/changes_start_change.go | 2 +- cmd/changes_submit_plan.go | 2 +- cmd/changes_submit_signal.go | 2 +- cmd/explore.go | 6 +- cmd/flags.go | 2 +- cmd/flags_test.go | 2 +- cmd/integrations_tfc.go | 2 +- cmd/invites_crud.go | 2 +- cmd/pterm.go | 10 +- cmd/request.go | 4 +- cmd/request_load.go | 6 +- cmd/request_query.go | 6 +- cmd/root.go | 6 +- cmd/root_test.go | 2 +- cmd/snapshots_create.go | 6 +- cmd/snapshots_get_snapshot.go | 2 +- cmd/terraform_apply.go | 2 +- cmd/terraform_plan.go | 2 +- go/auth/auth.go | 448 + go/auth/auth_client.go | 124 + go/auth/auth_test.go | 201 + go/auth/gcpauth.go | 58 + go/auth/middleware.go | 499 ++ go/auth/middleware_test.go | 792 ++ go/auth/nats.go | 258 + go/auth/nats_test.go | 423 + go/auth/tracing.go | 18 + go/discovery/adapter.go | 205 + go/discovery/adapter_test.go | 736 ++ go/discovery/adapterhost.go | 203 + go/discovery/adapterhost_bench_test.go | 822 ++ go/discovery/adapterhost_test.go | 362 + go/discovery/cmd.go | 326 + go/discovery/cmd_test.go | 235 + go/discovery/doc.go | 33 + go/discovery/engine.go | 897 ++ go/discovery/engine_initerror_test.go | 365 + go/discovery/engine_test.go | 808 ++ go/discovery/enginerequests.go | 532 ++ go/discovery/enginerequests_test.go | 460 ++ go/discovery/getfindmutex.go | 80 + go/discovery/getfindmutex_test.go | 194 + go/discovery/heartbeat.go | 165 + go/discovery/heartbeat_test.go | 207 + go/discovery/item_tests.go | 100 + go/discovery/logs.go | 102 + go/discovery/logs_test.go | 398 + go/discovery/main_test.go | 24 + go/discovery/nats_shared_test.go | 111 + go/discovery/nats_watcher.go | 126 + go/discovery/nats_watcher_test.go | 572 ++ go/discovery/nil_publisher.go | 111 + go/discovery/performance_test.go | 216 + go/discovery/querytracker.go | 92 + go/discovery/querytracker_test.go | 299 + go/discovery/shared_test.go | 275 + go/discovery/tracing.go | 20 + go/logging/logging.go | 53 + go/logging/logging_test.go | 86 + go/sdp-go/.gitignore | 2 + go/sdp-go/account.go | 19 + go/sdp-go/account.pb.go | 4649 +++++++++++ go/sdp-go/apikey.go | 11 + go/sdp-go/apikeys.pb.go | 1111 +++ go/sdp-go/area51.pb.go | 336 + go/sdp-go/auth0support.pb.go | 258 + go/sdp-go/bookmarks.go | 23 + go/sdp-go/bookmarks.pb.go | 886 ++ go/sdp-go/cached_entry.pb.go | 188 + go/sdp-go/changes.go | 484 ++ go/sdp-go/changes.pb.go | 7264 +++++++++++++++++ go/sdp-go/changes_test.go | 474 ++ go/sdp-go/changetimeline.go | 164 + go/sdp-go/changetimeline_test.go | 154 + go/sdp-go/cli.pb.go | 270 + go/sdp-go/compare.go | 41 + go/sdp-go/config.pb.go | 1931 +++++ go/sdp-go/connection.go | 180 + go/sdp-go/connection_test.go | 27 + go/sdp-go/encoder_test.go | 175 + go/sdp-go/errors.go | 54 + go/sdp-go/gateway.go | 190 + go/sdp-go/gateway.pb.go | 2336 ++++++ go/sdp-go/gateway_test.go | 120 + go/sdp-go/genhandler.go | 108 + go/sdp-go/graph/main.go | 362 + go/sdp-go/graph/main_test.go | 300 + go/sdp-go/handler_cancelquery.go | 56 + go/sdp-go/handler_gatewayresponse.go | 56 + go/sdp-go/handler_natsgetlogrecordsrequest.go | 56 + .../handler_natsgetlogrecordsresponse.go | 56 + go/sdp-go/handler_query.go | 56 + go/sdp-go/handler_queryresponse.go | 56 + go/sdp-go/instance_detect.go | 91 + go/sdp-go/invites.pb.go | 546 ++ go/sdp-go/items.go | 679 ++ go/sdp-go/items.pb.go | 1840 +++++ go/sdp-go/items_test.go | 695 ++ go/sdp-go/link_extract.go | 247 + go/sdp-go/link_extract_test.go | 795 ++ go/sdp-go/logs.go | 77 + go/sdp-go/logs.pb.go | 1083 +++ go/sdp-go/logs_test.go | 155 + go/sdp-go/progress.go | 734 ++ go/sdp-go/progress_test.go | 1525 ++++ go/sdp-go/proto_clone_test.go | 146 + go/sdp-go/responses.go | 27 + go/sdp-go/responses.pb.go | 246 + go/sdp-go/revlink.pb.go | 447 + go/sdp-go/sdpconnect/account.connect.go | 1187 +++ go/sdp-go/sdpconnect/apikeys.connect.go | 298 + go/sdp-go/sdpconnect/area51.connect.go | 113 + go/sdp-go/sdpconnect/auth0support.connect.go | 145 + go/sdp-go/sdpconnect/bookmarks.connect.go | 262 + go/sdp-go/sdpconnect/changes.connect.go | 1132 +++ go/sdp-go/sdpconnect/cli.connect.go | 136 + go/sdp-go/sdpconnect/config.connect.go | 434 + go/sdp-go/sdpconnect/invites.connect.go | 196 + go/sdp-go/sdpconnect/logs.connect.go | 117 + go/sdp-go/sdpconnect/revlink.connect.go | 189 + go/sdp-go/sdpconnect/signal.connect.go | 330 + go/sdp-go/sdpconnect/snapshots.connect.go | 254 + go/sdp-go/sdpws/client.go | 525 ++ go/sdp-go/sdpws/client_test.go | 1064 +++ go/sdp-go/sdpws/messagehandler.go | 191 + go/sdp-go/sdpws/utils.go | 343 + go/sdp-go/signal.pb.go | 1286 +++ go/sdp-go/signals.go | 10 + go/sdp-go/snapshots.go | 41 + go/sdp-go/snapshots.pb.go | 1011 +++ go/sdp-go/test_utils.go | 240 + go/sdp-go/test_utils_test.go | 143 + go/sdp-go/tracing.go | 106 + go/sdp-go/tracing_test.go | 72 + go/sdp-go/util.go | 156 + go/sdp-go/util.pb.go | 285 + go/sdp-go/util_test.go | 113 + go/sdp-go/validation.go | 168 + go/sdp-go/validation_test.go | 924 +++ go/sdpcache/bolt_cache.go | 1505 ++++ go/sdpcache/cache.go | 1009 +++ go/sdpcache/cache_benchmark_test.go | 774 ++ go/sdpcache/cache_stuck_test.go | 377 + go/sdpcache/cache_test.go | 2156 +++++ go/sdpcache/item_generator_test.go | 135 + go/sdpcache/pending.go | 114 + go/tracing/deferlog.go | 77 + go/tracing/header_carrier.go | 31 + go/tracing/main.go | 383 + go/tracing/main_test.go | 12 + go/tracing/memory.go | 69 + go/tracing/memory_test.go | 75 + k8s-source/adapters/clusterrole.go | 6 +- k8s-source/adapters/clusterrole_test.go | 2 +- k8s-source/adapters/clusterrolebinding.go | 6 +- .../adapters/clusterrolebinding_test.go | 4 +- k8s-source/adapters/configmap.go | 6 +- k8s-source/adapters/configmap_test.go | 4 +- k8s-source/adapters/cronjob.go | 6 +- k8s-source/adapters/cronjob_test.go | 2 +- k8s-source/adapters/daemonset.go | 6 +- k8s-source/adapters/daemonset_test.go | 2 +- k8s-source/adapters/deployment.go | 6 +- k8s-source/adapters/deployment_test.go | 4 +- k8s-source/adapters/endpoints.go | 6 +- k8s-source/adapters/endpoints_test.go | 4 +- k8s-source/adapters/endpointslice.go | 6 +- k8s-source/adapters/endpointslice_test.go | 4 +- k8s-source/adapters/generic_source.go | 4 +- k8s-source/adapters/generic_source_test.go | 6 +- .../adapters/horizontalpodautoscaler.go | 6 +- .../adapters/horizontalpodautoscaler_test.go | 4 +- k8s-source/adapters/ingress.go | 6 +- k8s-source/adapters/ingress_test.go | 4 +- k8s-source/adapters/job.go | 6 +- k8s-source/adapters/job_test.go | 4 +- k8s-source/adapters/limitrange.go | 6 +- k8s-source/adapters/limitrange_test.go | 2 +- k8s-source/adapters/main.go | 4 +- k8s-source/adapters/networkpolicy.go | 6 +- k8s-source/adapters/networkpolicy_test.go | 4 +- k8s-source/adapters/node.go | 6 +- k8s-source/adapters/node_test.go | 4 +- k8s-source/adapters/persistentvolume.go | 6 +- k8s-source/adapters/persistentvolume_test.go | 2 +- k8s-source/adapters/persistentvolumeclaim.go | 6 +- .../adapters/persistentvolumeclaim_test.go | 4 +- k8s-source/adapters/poddisruptionbudget.go | 6 +- .../adapters/poddisruptionbudget_test.go | 4 +- k8s-source/adapters/pods.go | 6 +- k8s-source/adapters/pods_test.go | 4 +- k8s-source/adapters/priorityclass.go | 6 +- k8s-source/adapters/priorityclass_test.go | 2 +- k8s-source/adapters/replicaset.go | 6 +- k8s-source/adapters/replicaset_test.go | 4 +- k8s-source/adapters/replicationcontroller.go | 6 +- .../adapters/replicationcontroller_test.go | 4 +- k8s-source/adapters/resourcequota.go | 6 +- k8s-source/adapters/resourcequota_test.go | 2 +- k8s-source/adapters/role.go | 6 +- k8s-source/adapters/role_test.go | 2 +- k8s-source/adapters/rolebinding.go | 6 +- k8s-source/adapters/rolebinding_test.go | 4 +- k8s-source/adapters/secret.go | 6 +- k8s-source/adapters/secret_test.go | 2 +- k8s-source/adapters/service.go | 6 +- k8s-source/adapters/service_test.go | 4 +- k8s-source/adapters/serviceaccount.go | 6 +- k8s-source/adapters/serviceaccount_test.go | 4 +- k8s-source/adapters/shared_util.go | 2 +- k8s-source/adapters/statefulset.go | 6 +- k8s-source/adapters/statefulset_test.go | 4 +- k8s-source/adapters/storageclass.go | 6 +- k8s-source/adapters/storageclass_test.go | 2 +- k8s-source/adapters/volumeattachment.go | 6 +- k8s-source/adapters/volumeattachment_test.go | 4 +- k8s-source/build/package/Dockerfile | 2 +- k8s-source/cmd/root.go | 8 +- sources/aws/apigateway-api-key.go | 2 +- sources/aws/apigateway-stage.go | 2 +- sources/aws/base.go | 2 +- sources/aws/errors.go | 2 +- sources/aws/validation_test.go | 4 +- sources/azure/build/package/Dockerfile | 2 +- sources/azure/cmd/root.go | 6 +- .../authorization-role-assignment_test.go | 6 +- .../batch-batch-accounts_test.go | 6 +- .../compute-availability-set_test.go | 6 +- ...compute-capacity-reservation-group_test.go | 6 +- .../compute-dedicated-host-group_test.go | 6 +- .../compute-disk-access_test.go | 6 +- .../compute-disk-encryption-set_test.go | 6 +- .../integration-tests/compute-disk_test.go | 6 +- ...ompute-gallery-application-version_test.go | 6 +- .../integration-tests/compute-image_test.go | 6 +- .../compute-proximity-placement-group_test.go | 6 +- .../compute-virtual-machine-extension_test.go | 6 +- ...ompute-virtual-machine-run-command_test.go | 6 +- .../compute-virtual-machine-scale-set_test.go | 6 +- .../compute-virtual-machine_test.go | 6 +- .../dbforpostgresql-database_test.go | 6 +- .../dbforpostgresql-flexible-server_test.go | 6 +- .../documentdb-database-accounts_test.go | 4 +- .../keyvault-managed-hsm_test.go | 6 +- .../integration-tests/keyvault-secret_test.go | 4 +- .../integration-tests/keyvault-vault_test.go | 4 +- ...gedidentity-user-assigned-identity_test.go | 6 +- .../network-application-gateway_test.go | 6 +- .../network-load-balancer_test.go | 4 +- .../network-network-interface_test.go | 4 +- .../network-network-security-group_test.go | 6 +- .../network-public-ip-address_test.go | 6 +- .../network-route-table_test.go | 6 +- .../network-virtual-network_test.go | 6 +- .../integration-tests/network-zone_test.go | 6 +- .../integration-tests/sql-database_test.go | 6 +- .../integration-tests/sql-server_test.go | 6 +- .../integration-tests/storage-account_test.go | 6 +- .../storage-blob-container_test.go | 4 +- .../storage-fileshare_test.go | 4 +- .../integration-tests/storage-queues_test.go | 4 +- .../integration-tests/storage-table_test.go | 4 +- sources/azure/manual/adapters.go | 4 +- .../manual/authorization-role-assignment.go | 6 +- .../authorization-role-assignment_test.go | 6 +- sources/azure/manual/batch-batch-accounts.go | 6 +- .../azure/manual/batch-batch-accounts_test.go | 6 +- .../azure/manual/compute-availability-set.go | 6 +- .../manual/compute-availability-set_test.go | 6 +- .../compute-capacity-reservation-group.go | 6 +- ...compute-capacity-reservation-group_test.go | 6 +- .../manual/compute-dedicated-host-group.go | 6 +- .../compute-dedicated-host-group_test.go | 6 +- sources/azure/manual/compute-disk-access.go | 6 +- .../azure/manual/compute-disk-access_test.go | 6 +- .../manual/compute-disk-encryption-set.go | 6 +- .../compute-disk-encryption-set_test.go | 6 +- sources/azure/manual/compute-disk.go | 6 +- sources/azure/manual/compute-disk_test.go | 6 +- .../compute-gallery-application-version.go | 2 +- ...ompute-gallery-application-version_test.go | 6 +- sources/azure/manual/compute-image.go | 6 +- sources/azure/manual/compute-image_test.go | 6 +- .../compute-proximity-placement-group.go | 6 +- .../compute-proximity-placement-group_test.go | 6 +- .../compute-virtual-machine-extension.go | 2 +- .../compute-virtual-machine-extension_test.go | 6 +- .../compute-virtual-machine-run-command.go | 2 +- ...ompute-virtual-machine-run-command_test.go | 6 +- .../compute-virtual-machine-scale-set.go | 6 +- .../compute-virtual-machine-scale-set_test.go | 6 +- .../azure/manual/compute-virtual-machine.go | 6 +- .../manual/compute-virtual-machine_test.go | 6 +- .../azure/manual/dbforpostgresql-database.go | 2 +- .../manual/dbforpostgresql-database_test.go | 6 +- .../manual/dbforpostgresql-flexible-server.go | 2 +- .../dbforpostgresql-flexible-server_test.go | 6 +- sources/azure/manual/dns_links.go | 2 +- .../manual/documentdb-database-accounts.go | 2 +- .../documentdb-database-accounts_test.go | 6 +- sources/azure/manual/keyvault-managed-hsm.go | 6 +- .../azure/manual/keyvault-managed-hsm_test.go | 6 +- sources/azure/manual/keyvault-secret.go | 2 +- sources/azure/manual/keyvault-secret_test.go | 6 +- sources/azure/manual/keyvault-vault.go | 6 +- sources/azure/manual/keyvault-vault_test.go | 6 +- sources/azure/manual/links_helpers.go | 2 +- .../managedidentity-user-assigned-identity.go | 6 +- ...gedidentity-user-assigned-identity_test.go | 6 +- .../manual/network-application-gateway.go | 6 +- .../network-application-gateway_test.go | 6 +- sources/azure/manual/network-load-balancer.go | 6 +- .../manual/network-load-balancer_test.go | 6 +- .../azure/manual/network-network-interface.go | 6 +- .../manual/network-network-interface_test.go | 6 +- .../manual/network-network-security-group.go | 6 +- .../network-network-security-group_test.go | 6 +- .../azure/manual/network-public-ip-address.go | 6 +- .../manual/network-public-ip-address_test.go | 6 +- sources/azure/manual/network-route-table.go | 6 +- .../azure/manual/network-route-table_test.go | 6 +- .../azure/manual/network-virtual-network.go | 6 +- .../manual/network-virtual-network_test.go | 6 +- sources/azure/manual/network-zone.go | 6 +- sources/azure/manual/network-zone_test.go | 6 +- sources/azure/manual/sql-database.go | 2 +- sources/azure/manual/sql-database_test.go | 6 +- sources/azure/manual/sql-server.go | 6 +- sources/azure/manual/sql-server_test.go | 6 +- sources/azure/manual/storage-account.go | 6 +- sources/azure/manual/storage-account_test.go | 6 +- .../azure/manual/storage-blob-container.go | 2 +- .../manual/storage-blob-container_test.go | 6 +- sources/azure/manual/storage-fileshare.go | 2 +- .../azure/manual/storage-fileshare_test.go | 6 +- sources/azure/manual/storage-queues.go | 2 +- sources/azure/manual/storage-queues_test.go | 6 +- sources/azure/manual/storage-table.go | 2 +- sources/azure/manual/storage-table_test.go | 6 +- sources/azure/proc/proc.go | 6 +- sources/azure/proc/proc_test.go | 2 +- sources/azure/shared/adapter-meta.go | 2 +- sources/azure/shared/base.go | 2 +- sources/azure/shared/errors.go | 2 +- sources/azure/shared/scope.go | 2 +- sources/example/base.go | 2 +- sources/example/custom_searchable_listable.go | 2 +- sources/example/errors.go | 2 +- sources/example/metadata_test.go | 4 +- .../example/standard_searchable_listable.go | 2 +- .../standard_searchable_listable_test.go | 2 +- sources/example/validation_test.go | 4 +- sources/gcp/build/package/Dockerfile | 2 +- sources/gcp/cmd/root.go | 6 +- sources/gcp/dynamic/adapter-listable.go | 6 +- .../dynamic/adapter-searchable-listable.go | 6 +- sources/gcp/dynamic/adapter-searchable.go | 6 +- sources/gcp/dynamic/adapter.go | 6 +- sources/gcp/dynamic/adapters.go | 4 +- .../ai-platform-batch-prediction-job.go | 2 +- .../ai-platform-batch-prediction-job_test.go | 6 +- .../adapters/ai-platform-custom-job.go | 2 +- .../adapters/ai-platform-custom-job_test.go | 6 +- .../dynamic/adapters/ai-platform-endpoint.go | 2 +- .../adapters/ai-platform-endpoint_test.go | 6 +- ...latform-model-deployment-monitoring-job.go | 2 +- ...rm-model-deployment-monitoring-job_test.go | 6 +- .../gcp/dynamic/adapters/ai-platform-model.go | 2 +- .../adapters/ai-platform-model_test.go | 6 +- .../adapters/ai-platform-pipeline-job.go | 2 +- .../adapters/ai-platform-pipeline-job_test.go | 6 +- .../artifact-registry-docker-image.go | 2 +- .../artifact-registry-docker-image_test.go | 6 +- .../adapters/artifact-registry-repository.go | 2 +- ...big-query-data-transfer-transfer-config.go | 2 +- ...uery-data-transfer-transfer-config_test.go | 6 +- .../adapters/big-table-admin-app-profile.go | 2 +- .../big-table-admin-app-profile_test.go | 6 +- .../adapters/big-table-admin-backup.go | 2 +- .../adapters/big-table-admin-backup_test.go | 6 +- .../adapters/big-table-admin-cluster.go | 2 +- .../adapters/big-table-admin-cluster_test.go | 6 +- .../adapters/big-table-admin-instance.go | 2 +- .../adapters/big-table-admin-instance_test.go | 6 +- .../dynamic/adapters/big-table-admin-table.go | 2 +- .../adapters/big-table-admin-table_test.go | 6 +- .../adapters/cloud-billing-billing-info.go | 2 +- .../cloud-billing-billing-info_test.go | 2 +- .../gcp/dynamic/adapters/cloud-build-build.go | 2 +- .../adapters/cloud-build-build_test.go | 6 +- .../cloud-resource-manager-project.go | 2 +- .../cloud-resource-manager-project_test.go | 2 +- .../cloud-resource-manager-tag-key.go | 2 +- .../cloud-resource-manager-tag-key_test.go | 4 +- .../cloud-resource-manager-tag-value.go | 2 +- .../cloud-resource-manager-tag-value_test.go | 6 +- .../adapters/cloudfunctions-function.go | 2 +- .../adapters/cloudfunctions-function_test.go | 6 +- .../adapters/compute-accelerator-type.go | 2 +- .../gcp/dynamic/adapters/compute-disk-type.go | 2 +- .../adapters/compute-external-vpn-gateway.go | 2 +- .../compute-external-vpn-gateway_test.go | 6 +- .../gcp/dynamic/adapters/compute-firewall.go | 2 +- .../dynamic/adapters/compute-firewall_test.go | 6 +- .../adapters/compute-global-address.go | 2 +- .../adapters/compute-global-address_test.go | 6 +- .../compute-global-forwarding-rule.go | 2 +- .../compute-global-forwarding-rule_test.go | 6 +- .../adapters/compute-http-health-check.go | 2 +- .../compute-http-health-check_test.go | 6 +- .../adapters/compute-instance-template.go | 2 +- .../compute-instance-template_test.go | 6 +- .../gcp/dynamic/adapters/compute-license.go | 2 +- .../compute-network-endpoint-group.go | 2 +- .../compute-network-endpoint-group_test.go | 6 +- .../gcp/dynamic/adapters/compute-network.go | 2 +- .../dynamic/adapters/compute-network_test.go | 6 +- .../gcp/dynamic/adapters/compute-project.go | 2 +- .../dynamic/adapters/compute-project_test.go | 4 +- .../compute-public-delegated-prefix.go | 2 +- .../compute-public-delegated-prefix_test.go | 6 +- .../adapters/compute-region-commitment.go | 2 +- .../compute-region-commitment_test.go | 6 +- .../adapters/compute-resource-policy.go | 2 +- sources/gcp/dynamic/adapters/compute-route.go | 2 +- .../dynamic/adapters/compute-route_test.go | 6 +- .../gcp/dynamic/adapters/compute-router.go | 2 +- .../dynamic/adapters/compute-router_test.go | 6 +- .../adapters/compute-ssl-certificate.go | 2 +- .../adapters/compute-ssl-certificate_test.go | 4 +- .../dynamic/adapters/compute-ssl-policy.go | 2 +- .../adapters/compute-ssl-policy_test.go | 4 +- .../dynamic/adapters/compute-storage-pool.go | 2 +- .../dynamic/adapters/compute-subnetwork.go | 2 +- .../adapters/compute-subnetwork_test.go | 6 +- .../adapters/compute-target-http-proxy.go | 2 +- .../compute-target-http-proxy_test.go | 6 +- .../adapters/compute-target-https-proxy.go | 2 +- .../compute-target-https-proxy_test.go | 6 +- .../dynamic/adapters/compute-target-pool.go | 2 +- .../adapters/compute-target-pool_test.go | 6 +- .../gcp/dynamic/adapters/compute-url-map.go | 2 +- .../dynamic/adapters/compute-url-map_test.go | 6 +- .../dynamic/adapters/compute-vpn-gateway.go | 2 +- .../adapters/compute-vpn-gateway_test.go | 6 +- .../dynamic/adapters/compute-vpn-tunnel.go | 2 +- .../adapters/compute-vpn-tunnel_test.go | 6 +- .../gcp/dynamic/adapters/container-cluster.go | 2 +- .../adapters/container-cluster_test.go | 6 +- .../dynamic/adapters/container-node-pool.go | 2 +- .../adapters/container-node-pool_test.go | 6 +- .../dynamic/adapters/dataform-repository.go | 2 +- .../adapters/dataform-repository_test.go | 6 +- .../dynamic/adapters/dataplex-aspect-type.go | 2 +- .../adapters/dataplex-aspect-type_test.go | 4 +- .../dynamic/adapters/dataplex-data-scan.go | 2 +- .../adapters/dataplex-data-scan_test.go | 6 +- .../dynamic/adapters/dataplex-entry-group.go | 2 +- .../adapters/dataplex-entry-group_test.go | 4 +- .../adapters/dataproc-auto-scaling-policy.go | 2 +- .../dataproc-auto-scaling-policy_test.go | 4 +- .../gcp/dynamic/adapters/dataproc-cluster.go | 2 +- .../dynamic/adapters/dataproc-cluster_test.go | 6 +- .../gcp/dynamic/adapters/dns-managed-zone.go | 2 +- .../dynamic/adapters/dns-managed-zone_test.go | 6 +- .../adapters/essential-contacts-contact.go | 2 +- .../essential-contacts-contact_test.go | 4 +- .../gcp/dynamic/adapters/eventarc-trigger.go | 2 +- sources/gcp/dynamic/adapters/file-instance.go | 2 +- .../dynamic/adapters/file-instance_test.go | 6 +- sources/gcp/dynamic/adapters/iam-role.go | 2 +- sources/gcp/dynamic/adapters/iam-role_test.go | 4 +- .../gcp/dynamic/adapters/logging-bucket.go | 2 +- .../dynamic/adapters/logging-bucket_test.go | 6 +- sources/gcp/dynamic/adapters/logging-link.go | 2 +- .../gcp/dynamic/adapters/logging-link_test.go | 6 +- .../dynamic/adapters/logging-saved-query.go | 2 +- .../adapters/logging-saved-query_test.go | 4 +- .../adapters/monitoring-alert-policy.go | 2 +- .../adapters/monitoring-alert-policy_test.go | 6 +- .../adapters/monitoring-custom-dashboard.go | 2 +- .../monitoring-custom-dashboard_test.go | 4 +- .../monitoring-notification-channel.go | 2 +- .../monitoring-notification-channel_test.go | 4 +- .../gcp/dynamic/adapters/orgpolicy-policy.go | 2 +- .../dynamic/adapters/orgpolicy-policy_test.go | 4 +- .../dynamic/adapters/pubsub-subscription.go | 2 +- .../adapters/pubsub-subscription_test.go | 6 +- sources/gcp/dynamic/adapters/pubsub-topic.go | 2 +- .../gcp/dynamic/adapters/pubsub-topic_test.go | 6 +- .../gcp/dynamic/adapters/redis-instance.go | 2 +- .../dynamic/adapters/redis-instance_test.go | 6 +- sources/gcp/dynamic/adapters/run-revision.go | 2 +- .../gcp/dynamic/adapters/run-revision_test.go | 6 +- sources/gcp/dynamic/adapters/run-service.go | 2 +- .../gcp/dynamic/adapters/run-service_test.go | 6 +- .../gcp/dynamic/adapters/run-worker-pool.go | 2 +- .../dynamic/adapters/secret-manager-secret.go | 2 +- .../adapters/secret-manager-secret_test.go | 6 +- ...nter-management-security-center-service.go | 2 +- ...management-security-center-service_test.go | 6 +- .../adapters/service-directory-endpoint.go | 2 +- .../service-directory-endpoint_test.go | 6 +- .../adapters/service-directory-service.go | 2 +- .../dynamic/adapters/service-usage-service.go | 2 +- .../adapters/service-usage-service_test.go | 6 +- .../gcp/dynamic/adapters/spanner-backup.go | 2 +- .../gcp/dynamic/adapters/spanner-database.go | 2 +- .../dynamic/adapters/spanner-database_test.go | 6 +- .../adapters/spanner-instance-config.go | 2 +- .../gcp/dynamic/adapters/spanner-instance.go | 2 +- .../dynamic/adapters/spanner-instance_test.go | 6 +- .../dynamic/adapters/sql-admin-backup-run.go | 2 +- .../gcp/dynamic/adapters/sql-admin-backup.go | 2 +- .../dynamic/adapters/sql-admin-backup_test.go | 6 +- .../dynamic/adapters/sql-admin-instance.go | 2 +- .../adapters/sql-admin-instance_test.go | 6 +- .../gcp/dynamic/adapters/storage-bucket.go | 2 +- .../dynamic/adapters/storage-bucket_test.go | 6 +- .../adapters/storage-transfer-transfer-job.go | 2 +- .../storage-transfer-transfer-job_test.go | 6 +- sources/gcp/dynamic/adapters_test.go | 6 +- sources/gcp/dynamic/shared.go | 6 +- sources/gcp/dynamic/shared_test.go | 6 +- .../integration-tests/big-query-model_test.go | 2 +- .../integration-tests/compute-address_test.go | 4 +- .../compute-autoscaler_test.go | 4 +- .../integration-tests/compute-disk_test.go | 4 +- .../compute-forwarding-rule_test.go | 4 +- .../compute-healthcheck_test.go | 4 +- .../integration-tests/compute-image_test.go | 4 +- .../compute-instance-group-manager_test.go | 4 +- .../compute-instance-group_test.go | 4 +- .../compute-instance_test.go | 4 +- .../compute-instant-snapshot_test.go | 4 +- .../compute-machine-image_test.go | 4 +- .../integration-tests/compute-network_test.go | 4 +- .../compute-node-group_test.go | 6 +- .../compute-reservation_test.go | 4 +- .../compute-snapshot_test.go | 4 +- .../compute-subnetwork_test.go | 4 +- .../computer-instance-template_test.go | 4 +- .../spanner-database_test.go | 2 +- .../spanner-instance_test.go | 4 +- sources/gcp/manual/adapters.go | 4 +- sources/gcp/manual/big-query-dataset.go | 6 +- sources/gcp/manual/big-query-dataset_test.go | 6 +- sources/gcp/manual/big-query-model.go | 6 +- sources/gcp/manual/big-query-model_test.go | 6 +- sources/gcp/manual/big-query-routine.go | 6 +- sources/gcp/manual/big-query-routine_test.go | 6 +- sources/gcp/manual/big-query-table.go | 6 +- sources/gcp/manual/big-query-table_test.go | 6 +- .../manual/certificate-manager-certificate.go | 6 +- .../certificate-manager-certificate_test.go | 6 +- .../manual/cloud-kms-crypto-key-version.go | 6 +- .../cloud-kms-crypto-key-version_test.go | 6 +- sources/gcp/manual/cloud-kms-crypto-key.go | 6 +- .../gcp/manual/cloud-kms-crypto-key_test.go | 6 +- sources/gcp/manual/cloud-kms-key-ring.go | 6 +- sources/gcp/manual/cloud-kms-key-ring_test.go | 6 +- sources/gcp/manual/compute-address.go | 6 +- sources/gcp/manual/compute-address_test.go | 6 +- sources/gcp/manual/compute-autoscaler.go | 6 +- sources/gcp/manual/compute-autoscaler_test.go | 6 +- sources/gcp/manual/compute-backend-service.go | 6 +- .../manual/compute-backend-service_test.go | 6 +- sources/gcp/manual/compute-disk.go | 6 +- sources/gcp/manual/compute-disk_test.go | 6 +- sources/gcp/manual/compute-forwarding-rule.go | 6 +- .../manual/compute-forwarding-rule_test.go | 6 +- sources/gcp/manual/compute-healthcheck.go | 6 +- .../gcp/manual/compute-healthcheck_test.go | 6 +- sources/gcp/manual/compute-image.go | 6 +- sources/gcp/manual/compute-image_test.go | 6 +- .../compute-instance-group-manager-shared.go | 2 +- .../manual/compute-instance-group-manager.go | 6 +- .../compute-instance-group-manager_test.go | 6 +- sources/gcp/manual/compute-instance-group.go | 6 +- .../gcp/manual/compute-instance-group_test.go | 6 +- sources/gcp/manual/compute-instance.go | 6 +- sources/gcp/manual/compute-instance_test.go | 6 +- .../gcp/manual/compute-instant-snapshot.go | 6 +- .../manual/compute-instant-snapshot_test.go | 6 +- sources/gcp/manual/compute-machine-image.go | 6 +- .../gcp/manual/compute-machine-image_test.go | 6 +- sources/gcp/manual/compute-node-group.go | 6 +- sources/gcp/manual/compute-node-group_test.go | 6 +- sources/gcp/manual/compute-node-template.go | 6 +- .../gcp/manual/compute-node-template_test.go | 6 +- .../compute-region-instance-group-manager.go | 6 +- ...pute-region-instance-group-manager_test.go | 6 +- sources/gcp/manual/compute-reservation.go | 6 +- .../gcp/manual/compute-reservation_test.go | 6 +- sources/gcp/manual/compute-security-policy.go | 6 +- .../manual/compute-security-policy_test.go | 6 +- sources/gcp/manual/compute-snapshot.go | 6 +- sources/gcp/manual/compute-snapshot_test.go | 6 +- sources/gcp/manual/iam-service-account-key.go | 6 +- .../manual/iam-service-account-key_test.go | 6 +- sources/gcp/manual/iam-service-account.go | 6 +- .../gcp/manual/iam-service-account_test.go | 6 +- sources/gcp/manual/logging-sink.go | 6 +- sources/gcp/manual/logging-sink_test.go | 6 +- .../gcp/manual/storage-bucket-iam-policy.go | 2 +- .../manual/storage-bucket-iam-policy_test.go | 6 +- sources/gcp/proc/proc.go | 6 +- sources/gcp/proc/proc_test.go | 6 +- sources/gcp/shared/adapter-meta.go | 2 +- sources/gcp/shared/base.go | 6 +- sources/gcp/shared/big-query-clients.go | 6 +- .../gcp/shared/cross_project_linking_test.go | 2 +- sources/gcp/shared/errors.go | 2 +- sources/gcp/shared/kms-asset-loader.go | 6 +- sources/gcp/shared/linker.go | 2 +- sources/gcp/shared/linker_test.go | 2 +- sources/gcp/shared/manual-adapter-links.go | 2 +- .../gcp/shared/manual-adapter-links_test.go | 2 +- .../mocks/mock_big_query_dataset_client.go | 6 +- sources/gcp/shared/terraform-mappings.go | 2 +- sources/shared/base.go | 2 +- sources/shared/testing.go | 4 +- sources/shared/util.go | 2 +- sources/transformer.go | 6 +- sources/transformer_test.go | 4 +- stdlib-source/adapters/certificate.go | 2 +- stdlib-source/adapters/certificate_test.go | 4 +- stdlib-source/adapters/dns.go | 4 +- stdlib-source/adapters/dns_test.go | 6 +- stdlib-source/adapters/http.go | 4 +- stdlib-source/adapters/http_test.go | 6 +- stdlib-source/adapters/ip.go | 2 +- stdlib-source/adapters/ip_test.go | 4 +- stdlib-source/adapters/main.go | 6 +- stdlib-source/adapters/rdap-asn.go | 4 +- stdlib-source/adapters/rdap-asn_test.go | 2 +- stdlib-source/adapters/rdap-domain.go | 4 +- stdlib-source/adapters/rdap-domain_test.go | 2 +- stdlib-source/adapters/rdap-entity.go | 4 +- stdlib-source/adapters/rdap-entity_test.go | 4 +- stdlib-source/adapters/rdap-ip-network.go | 4 +- .../adapters/rdap-ip-network_test.go | 2 +- stdlib-source/adapters/rdap-nameserver.go | 4 +- .../adapters/rdap-nameserver_test.go | 2 +- stdlib-source/adapters/test/data.go | 2 +- stdlib-source/adapters/test/testdog.go | 2 +- stdlib-source/adapters/test/testfood.go | 2 +- stdlib-source/adapters/test/testgroup.go | 2 +- stdlib-source/adapters/test/testhobby.go | 2 +- stdlib-source/adapters/test/testlocation.go | 2 +- stdlib-source/adapters/test/testperson.go | 2 +- stdlib-source/adapters/test/testregion.go | 2 +- stdlib-source/build/package/Dockerfile | 2 +- stdlib-source/cmd/root.go | 6 +- tfutils/plan_mapper.go | 2 +- tfutils/plan_mapper_test.go | 2 +- 982 files changed, 63750 insertions(+), 1740 deletions(-) create mode 100644 go/auth/auth.go create mode 100644 go/auth/auth_client.go create mode 100644 go/auth/auth_test.go create mode 100644 go/auth/gcpauth.go create mode 100644 go/auth/middleware.go create mode 100644 go/auth/middleware_test.go create mode 100644 go/auth/nats.go create mode 100644 go/auth/nats_test.go create mode 100644 go/auth/tracing.go create mode 100644 go/discovery/adapter.go create mode 100644 go/discovery/adapter_test.go create mode 100644 go/discovery/adapterhost.go create mode 100644 go/discovery/adapterhost_bench_test.go create mode 100644 go/discovery/adapterhost_test.go create mode 100644 go/discovery/cmd.go create mode 100644 go/discovery/cmd_test.go create mode 100644 go/discovery/doc.go create mode 100644 go/discovery/engine.go create mode 100644 go/discovery/engine_initerror_test.go create mode 100644 go/discovery/engine_test.go create mode 100644 go/discovery/enginerequests.go create mode 100644 go/discovery/enginerequests_test.go create mode 100644 go/discovery/getfindmutex.go create mode 100644 go/discovery/getfindmutex_test.go create mode 100644 go/discovery/heartbeat.go create mode 100644 go/discovery/heartbeat_test.go create mode 100644 go/discovery/item_tests.go create mode 100644 go/discovery/logs.go create mode 100644 go/discovery/logs_test.go create mode 100644 go/discovery/main_test.go create mode 100644 go/discovery/nats_shared_test.go create mode 100644 go/discovery/nats_watcher.go create mode 100644 go/discovery/nats_watcher_test.go create mode 100644 go/discovery/nil_publisher.go create mode 100644 go/discovery/performance_test.go create mode 100644 go/discovery/querytracker.go create mode 100644 go/discovery/querytracker_test.go create mode 100644 go/discovery/shared_test.go create mode 100644 go/discovery/tracing.go create mode 100644 go/logging/logging.go create mode 100644 go/logging/logging_test.go create mode 100644 go/sdp-go/.gitignore create mode 100644 go/sdp-go/account.go create mode 100644 go/sdp-go/account.pb.go create mode 100644 go/sdp-go/apikey.go create mode 100644 go/sdp-go/apikeys.pb.go create mode 100644 go/sdp-go/area51.pb.go create mode 100644 go/sdp-go/auth0support.pb.go create mode 100644 go/sdp-go/bookmarks.go create mode 100644 go/sdp-go/bookmarks.pb.go create mode 100644 go/sdp-go/cached_entry.pb.go create mode 100644 go/sdp-go/changes.go create mode 100644 go/sdp-go/changes.pb.go create mode 100644 go/sdp-go/changes_test.go create mode 100644 go/sdp-go/changetimeline.go create mode 100644 go/sdp-go/changetimeline_test.go create mode 100644 go/sdp-go/cli.pb.go create mode 100644 go/sdp-go/compare.go create mode 100644 go/sdp-go/config.pb.go create mode 100644 go/sdp-go/connection.go create mode 100644 go/sdp-go/connection_test.go create mode 100644 go/sdp-go/encoder_test.go create mode 100644 go/sdp-go/errors.go create mode 100644 go/sdp-go/gateway.go create mode 100644 go/sdp-go/gateway.pb.go create mode 100644 go/sdp-go/gateway_test.go create mode 100644 go/sdp-go/genhandler.go create mode 100644 go/sdp-go/graph/main.go create mode 100644 go/sdp-go/graph/main_test.go create mode 100644 go/sdp-go/handler_cancelquery.go create mode 100644 go/sdp-go/handler_gatewayresponse.go create mode 100644 go/sdp-go/handler_natsgetlogrecordsrequest.go create mode 100644 go/sdp-go/handler_natsgetlogrecordsresponse.go create mode 100644 go/sdp-go/handler_query.go create mode 100644 go/sdp-go/handler_queryresponse.go create mode 100644 go/sdp-go/instance_detect.go create mode 100644 go/sdp-go/invites.pb.go create mode 100644 go/sdp-go/items.go create mode 100644 go/sdp-go/items.pb.go create mode 100644 go/sdp-go/items_test.go create mode 100644 go/sdp-go/link_extract.go create mode 100644 go/sdp-go/link_extract_test.go create mode 100644 go/sdp-go/logs.go create mode 100644 go/sdp-go/logs.pb.go create mode 100644 go/sdp-go/logs_test.go create mode 100644 go/sdp-go/progress.go create mode 100644 go/sdp-go/progress_test.go create mode 100644 go/sdp-go/proto_clone_test.go create mode 100644 go/sdp-go/responses.go create mode 100644 go/sdp-go/responses.pb.go create mode 100644 go/sdp-go/revlink.pb.go create mode 100644 go/sdp-go/sdpconnect/account.connect.go create mode 100644 go/sdp-go/sdpconnect/apikeys.connect.go create mode 100644 go/sdp-go/sdpconnect/area51.connect.go create mode 100644 go/sdp-go/sdpconnect/auth0support.connect.go create mode 100644 go/sdp-go/sdpconnect/bookmarks.connect.go create mode 100644 go/sdp-go/sdpconnect/changes.connect.go create mode 100644 go/sdp-go/sdpconnect/cli.connect.go create mode 100644 go/sdp-go/sdpconnect/config.connect.go create mode 100644 go/sdp-go/sdpconnect/invites.connect.go create mode 100644 go/sdp-go/sdpconnect/logs.connect.go create mode 100644 go/sdp-go/sdpconnect/revlink.connect.go create mode 100644 go/sdp-go/sdpconnect/signal.connect.go create mode 100644 go/sdp-go/sdpconnect/snapshots.connect.go create mode 100644 go/sdp-go/sdpws/client.go create mode 100644 go/sdp-go/sdpws/client_test.go create mode 100644 go/sdp-go/sdpws/messagehandler.go create mode 100644 go/sdp-go/sdpws/utils.go create mode 100644 go/sdp-go/signal.pb.go create mode 100644 go/sdp-go/signals.go create mode 100644 go/sdp-go/snapshots.go create mode 100644 go/sdp-go/snapshots.pb.go create mode 100644 go/sdp-go/test_utils.go create mode 100644 go/sdp-go/test_utils_test.go create mode 100644 go/sdp-go/tracing.go create mode 100644 go/sdp-go/tracing_test.go create mode 100644 go/sdp-go/util.go create mode 100644 go/sdp-go/util.pb.go create mode 100644 go/sdp-go/util_test.go create mode 100644 go/sdp-go/validation.go create mode 100644 go/sdp-go/validation_test.go create mode 100644 go/sdpcache/bolt_cache.go create mode 100644 go/sdpcache/cache.go create mode 100644 go/sdpcache/cache_benchmark_test.go create mode 100644 go/sdpcache/cache_stuck_test.go create mode 100644 go/sdpcache/cache_test.go create mode 100644 go/sdpcache/item_generator_test.go create mode 100644 go/sdpcache/pending.go create mode 100644 go/tracing/deferlog.go create mode 100644 go/tracing/header_carrier.go create mode 100644 go/tracing/main.go create mode 100644 go/tracing/main_test.go create mode 100644 go/tracing/memory.go create mode 100644 go/tracing/memory_test.go diff --git a/aws-source/adapters/adapterhelpers_always_get_source.go b/aws-source/adapters/adapterhelpers_always_get_source.go index 19c80f6f..a946888a 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source.go +++ b/aws-source/adapters/adapterhelpers_always_get_source.go @@ -9,9 +9,9 @@ import ( "buf.build/go/protovalidate" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/pool" ) diff --git a/aws-source/adapters/adapterhelpers_always_get_source_test.go b/aws-source/adapters/adapterhelpers_always_get_source_test.go index 4801e8f6..94bfd498 100644 --- a/aws-source/adapters/adapterhelpers_always_get_source_test.go +++ b/aws-source/adapters/adapterhelpers_always_get_source_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_describe_source.go b/aws-source/adapters/adapterhelpers_describe_source.go index c7049d59..e52bdc1f 100644 --- a/aws-source/adapters/adapterhelpers_describe_source.go +++ b/aws-source/adapters/adapterhelpers_describe_source.go @@ -8,9 +8,9 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // relatively short cache duration to cover a single Change Analysis run. diff --git a/aws-source/adapters/adapterhelpers_describe_source_test.go b/aws-source/adapters/adapterhelpers_describe_source_test.go index 10edbded..50732af3 100644 --- a/aws-source/adapters/adapterhelpers_describe_source_test.go +++ b/aws-source/adapters/adapterhelpers_describe_source_test.go @@ -7,9 +7,9 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go index 424a9132..05099a01 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go @@ -6,9 +6,9 @@ import ( "fmt" "time" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // GetListAdapterV2 A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go index 389bf47a..57f81e07 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go @@ -6,9 +6,9 @@ import ( "fmt" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_get_list_source.go b/aws-source/adapters/adapterhelpers_get_list_source.go index 7363b281..35b5a739 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source.go +++ b/aws-source/adapters/adapterhelpers_get_list_source.go @@ -7,8 +7,8 @@ import ( "time" "buf.build/go/protovalidate" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // GetListAdapter A adapter for AWS APIs where the Get and List functions both diff --git a/aws-source/adapters/adapterhelpers_get_list_source_test.go b/aws-source/adapters/adapterhelpers_get_list_source_test.go index 08471eda..f6e01435 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_source_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/aws-source/adapters/adapterhelpers_notfound_cache_test.go b/aws-source/adapters/adapterhelpers_notfound_cache_test.go index 9fb26ca5..25d67216 100644 --- a/aws-source/adapters/adapterhelpers_notfound_cache_test.go +++ b/aws-source/adapters/adapterhelpers_notfound_cache_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TestGetListAdapterV2_GetNotFoundCaching tests that GetListAdapterV2 caches not-found error results diff --git a/aws-source/adapters/adapterhelpers_shared_tests.go b/aws-source/adapters/adapterhelpers_shared_tests.go index 65107d27..8bf6cda0 100644 --- a/aws-source/adapters/adapterhelpers_shared_tests.go +++ b/aws-source/adapters/adapterhelpers_shared_tests.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func PtrString(v string) *string { diff --git a/aws-source/adapters/adapterhelpers_util.go b/aws-source/adapters/adapterhelpers_util.go index ad49e56f..c77ac4e4 100644 --- a/aws-source/adapters/adapterhelpers_util.go +++ b/aws-source/adapters/adapterhelpers_util.go @@ -17,8 +17,8 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/apigateway-api-key.go b/aws-source/adapters/apigateway-api-key.go index cba52585..5efd2493 100644 --- a/aws-source/adapters/apigateway-api-key.go +++ b/aws-source/adapters/apigateway-api-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetApiKeyOutputToApiKey converts a GetApiKeyOutput to an ApiKey diff --git a/aws-source/adapters/apigateway-api-key_test.go b/aws-source/adapters/apigateway-api-key_test.go index 3e011531..1925e300 100644 --- a/aws-source/adapters/apigateway-api-key_test.go +++ b/aws-source/adapters/apigateway-api-key_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestApiKeyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-authorizer.go b/aws-source/adapters/apigateway-authorizer.go index e8e89ee0..f7d99eeb 100644 --- a/aws-source/adapters/apigateway-authorizer.go +++ b/aws-source/adapters/apigateway-authorizer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetAuthorizerOutputToAuthorizer converts a GetAuthorizerOutput to an Authorizer diff --git a/aws-source/adapters/apigateway-authorizer_test.go b/aws-source/adapters/apigateway-authorizer_test.go index babafe43..31830aaa 100644 --- a/aws-source/adapters/apigateway-authorizer_test.go +++ b/aws-source/adapters/apigateway-authorizer_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAuthorizerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-deployment.go b/aws-source/adapters/apigateway-deployment.go index 8d23731f..f0dcb3c5 100644 --- a/aws-source/adapters/apigateway-deployment.go +++ b/aws-source/adapters/apigateway-deployment.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // convertGetDeploymentOutputToDeployment converts a GetDeploymentOutput to a Deployment diff --git a/aws-source/adapters/apigateway-deployment_test.go b/aws-source/adapters/apigateway-deployment_test.go index c7af47e5..0904e875 100644 --- a/aws-source/adapters/apigateway-deployment_test.go +++ b/aws-source/adapters/apigateway-deployment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDeploymentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-domain-name.go b/aws-source/adapters/apigateway-domain-name.go index 2f536bb7..c96883a4 100644 --- a/aws-source/adapters/apigateway-domain-name.go +++ b/aws-source/adapters/apigateway-domain-name.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetDomainNameOutputToDomainName(output *apigateway.GetDomainNameOutput) *types.DomainName { diff --git a/aws-source/adapters/apigateway-domain-name_test.go b/aws-source/adapters/apigateway-domain-name_test.go index acad7369..80798499 100644 --- a/aws-source/adapters/apigateway-domain-name_test.go +++ b/aws-source/adapters/apigateway-domain-name_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-integration.go b/aws-source/adapters/apigateway-integration.go index 51355005..4424306b 100644 --- a/aws-source/adapters/apigateway-integration.go +++ b/aws-source/adapters/apigateway-integration.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type apiGatewayIntegrationGetter interface { diff --git a/aws-source/adapters/apigateway-integration_test.go b/aws-source/adapters/apigateway-integration_test.go index b6170185..e400680c 100644 --- a/aws-source/adapters/apigateway-integration_test.go +++ b/aws-source/adapters/apigateway-integration_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type mockAPIGatewayIntegrationClient struct{} diff --git a/aws-source/adapters/apigateway-method-response.go b/aws-source/adapters/apigateway-method-response.go index 6ac891ad..9d3609d3 100644 --- a/aws-source/adapters/apigateway-method-response.go +++ b/aws-source/adapters/apigateway-method-response.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodResponseInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/apigateway-method-response_test.go b/aws-source/adapters/apigateway-method-response_test.go index 9152c93b..97699a12 100644 --- a/aws-source/adapters/apigateway-method-response_test.go +++ b/aws-source/adapters/apigateway-method-response_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (m *mockAPIGatewayClient) GetMethodResponse(ctx context.Context, params *apigateway.GetMethodResponseInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodResponseOutput, error) { diff --git a/aws-source/adapters/apigateway-method.go b/aws-source/adapters/apigateway-method.go index 86e71f51..bee6dbd0 100644 --- a/aws-source/adapters/apigateway-method.go +++ b/aws-source/adapters/apigateway-method.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type apigatewayClient interface { diff --git a/aws-source/adapters/apigateway-method_test.go b/aws-source/adapters/apigateway-method_test.go index ed715c4e..69b8b1f9 100644 --- a/aws-source/adapters/apigateway-method_test.go +++ b/aws-source/adapters/apigateway-method_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type mockAPIGatewayClient struct{} diff --git a/aws-source/adapters/apigateway-model.go b/aws-source/adapters/apigateway-model.go index 6e47df54..51093764 100644 --- a/aws-source/adapters/apigateway-model.go +++ b/aws-source/adapters/apigateway-model.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetModelOutputToModel(output *apigateway.GetModelOutput) *types.Model { diff --git a/aws-source/adapters/apigateway-model_test.go b/aws-source/adapters/apigateway-model_test.go index 339d23e1..28f539f9 100644 --- a/aws-source/adapters/apigateway-model_test.go +++ b/aws-source/adapters/apigateway-model_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestModelOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/apigateway-resource.go b/aws-source/adapters/apigateway-resource.go index f2815201..e0dc22d8 100644 --- a/aws-source/adapters/apigateway-resource.go +++ b/aws-source/adapters/apigateway-resource.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetResourceOutputToResource(output *apigateway.GetResourceOutput) *types.Resource { diff --git a/aws-source/adapters/apigateway-resource_test.go b/aws-source/adapters/apigateway-resource_test.go index 837f2a23..94352d27 100644 --- a/aws-source/adapters/apigateway-resource_test.go +++ b/aws-source/adapters/apigateway-resource_test.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-rest-api.go b/aws-source/adapters/apigateway-rest-api.go index a8d8aff7..ac7bf41d 100644 --- a/aws-source/adapters/apigateway-rest-api.go +++ b/aws-source/adapters/apigateway-rest-api.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/apigateway-rest-api_test.go b/aws-source/adapters/apigateway-rest-api_test.go index 632102c4..459fd95b 100644 --- a/aws-source/adapters/apigateway-rest-api_test.go +++ b/aws-source/adapters/apigateway-rest-api_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/apigateway-stage.go b/aws-source/adapters/apigateway-stage.go index 411dc774..c2fae5d8 100644 --- a/aws-source/adapters/apigateway-stage.go +++ b/aws-source/adapters/apigateway-stage.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func convertGetStageOutputToStage(output *apigateway.GetStageOutput) *types.Stage { diff --git a/aws-source/adapters/apigateway-stage_test.go b/aws-source/adapters/apigateway-stage_test.go index 950a4936..ad4bd098 100644 --- a/aws-source/adapters/apigateway-stage_test.go +++ b/aws-source/adapters/apigateway-stage_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestStageOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-group.go b/aws-source/adapters/autoscaling-auto-scaling-group.go index 29088faa..68eac0b9 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribeAutoScalingGroupsInput, output *autoscaling.DescribeAutoScalingGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-group_test.go b/aws-source/adapters/autoscaling-auto-scaling-group_test.go index a5e25683..fb14825b 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAutoScalingGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy.go b/aws-source/adapters/autoscaling-auto-scaling-policy.go index 1fbe10d0..16954579 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope string, _ *autoscaling.DescribePoliciesInput, output *autoscaling.DescribePoliciesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go index 55112b26..70929e68 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestScalingPolicyOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-cache-policy.go b/aws-source/adapters/cloudfront-cache-policy.go index c18cbfc6..68b9a8a2 100644 --- a/aws-source/adapters/cloudfront-cache-policy.go +++ b/aws-source/adapters/cloudfront-cache-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func cachePolicyListFunc(ctx context.Context, client CloudFrontClient, scope string) ([]*types.CachePolicy, error) { diff --git a/aws-source/adapters/cloudfront-cache-policy_test.go b/aws-source/adapters/cloudfront-cache-policy_test.go index 763fc286..8380c496 100644 --- a/aws-source/adapters/cloudfront-cache-policy_test.go +++ b/aws-source/adapters/cloudfront-cache-policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var testCachePolicy = &types.CachePolicy{ diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy.go b/aws-source/adapters/cloudfront-continuous-deployment-policy.go index 21386807..2895ef10 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func continuousDeploymentPolicyItemMapper(_, scope string, awsItem *types.ContinuousDeploymentPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go index 72bc651c..77c38358 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestContinuousDeploymentPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-distribution.go b/aws-source/adapters/cloudfront-distribution.go index 2f181467..a21d5c6b 100644 --- a/aws-source/adapters/cloudfront-distribution.go +++ b/aws-source/adapters/cloudfront-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var s3DnsRegex = regexp.MustCompile(`([^\.]+)\.s3\.([^\.]+)\.amazonaws\.com`) diff --git a/aws-source/adapters/cloudfront-distribution_test.go b/aws-source/adapters/cloudfront-distribution_test.go index dfc09d41..20c713df 100644 --- a/aws-source/adapters/cloudfront-distribution_test.go +++ b/aws-source/adapters/cloudfront-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloudfront.GetDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetDistributionOutput, error) { diff --git a/aws-source/adapters/cloudfront-function.go b/aws-source/adapters/cloudfront-function.go index 08a2ede3..ea4239cc 100644 --- a/aws-source/adapters/cloudfront-function.go +++ b/aws-source/adapters/cloudfront-function.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func functionItemMapper(_, scope string, awsItem *types.FunctionSummary) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-function_test.go b/aws-source/adapters/cloudfront-function_test.go index d87382f1..83f3fe9b 100644 --- a/aws-source/adapters/cloudfront-function_test.go +++ b/aws-source/adapters/cloudfront-function_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestFunctionItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-key-group.go b/aws-source/adapters/cloudfront-key-group.go index 91a74dd1..2c80906f 100644 --- a/aws-source/adapters/cloudfront-key-group.go +++ b/aws-source/adapters/cloudfront-key-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func KeyGroupItemMapper(_, scope string, awsItem *types.KeyGroup) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-key-group_test.go b/aws-source/adapters/cloudfront-key-group_test.go index aa5d399c..c874fae0 100644 --- a/aws-source/adapters/cloudfront-key-group_test.go +++ b/aws-source/adapters/cloudfront-key-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestKeyGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-access-control.go b/aws-source/adapters/cloudfront-origin-access-control.go index b32a5450..582502e5 100644 --- a/aws-source/adapters/cloudfront-origin-access-control.go +++ b/aws-source/adapters/cloudfront-origin-access-control.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func originAccessControlListFunc(ctx context.Context, client *cloudfront.Client, scope string) ([]*types.OriginAccessControl, error) { diff --git a/aws-source/adapters/cloudfront-origin-access-control_test.go b/aws-source/adapters/cloudfront-origin-access-control_test.go index ee769e3b..25621cce 100644 --- a/aws-source/adapters/cloudfront-origin-access-control_test.go +++ b/aws-source/adapters/cloudfront-origin-access-control_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestOriginAccessControlItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy.go b/aws-source/adapters/cloudfront-origin-request-policy.go index 03920407..e3f97e51 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy.go +++ b/aws-source/adapters/cloudfront-origin-request-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func originRequestPolicyItemMapper(_, scope string, awsItem *types.OriginRequestPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-origin-request-policy_test.go b/aws-source/adapters/cloudfront-origin-request-policy_test.go index b1894816..4458f998 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy_test.go +++ b/aws-source/adapters/cloudfront-origin-request-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestOriginRequestPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-realtime-log-config.go b/aws-source/adapters/cloudfront-realtime-log-config.go index aafc5099..069de999 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config.go +++ b/aws-source/adapters/cloudfront-realtime-log-config.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogConfig) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-realtime-log-config_test.go b/aws-source/adapters/cloudfront-realtime-log-config_test.go index e0692268..58ae1589 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config_test.go +++ b/aws-source/adapters/cloudfront-realtime-log-config_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRealtimeLogConfigsItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy.go b/aws-source/adapters/cloudfront-response-headers-policy.go index a441f366..4ff4cab0 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy.go +++ b/aws-source/adapters/cloudfront-response-headers-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ResponseHeadersPolicyItemMapper(_, scope string, awsItem *types.ResponseHeadersPolicy) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-response-headers-policy_test.go b/aws-source/adapters/cloudfront-response-headers-policy_test.go index 06ad692c..e188964a 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy_test.go +++ b/aws-source/adapters/cloudfront-response-headers-policy_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestResponseHeadersPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/cloudfront-streaming-distribution.go b/aws-source/adapters/cloudfront-streaming-distribution.go index c2b1f89f..e0eb306c 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution.go +++ b/aws-source/adapters/cloudfront-streaming-distribution.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, scope string, input *cloudfront.GetStreamingDistributionInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/cloudfront-streaming-distribution_test.go b/aws-source/adapters/cloudfront-streaming-distribution_test.go index d5089cd4..eaeb0502 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution_test.go +++ b/aws-source/adapters/cloudfront-streaming-distribution_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, params *cloudfront.GetStreamingDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetStreamingDistributionOutput, error) { diff --git a/aws-source/adapters/cloudwatch-alarm.go b/aws-source/adapters/cloudwatch-alarm.go index 72331ed0..1b0d4f8d 100644 --- a/aws-source/adapters/cloudwatch-alarm.go +++ b/aws-source/adapters/cloudwatch-alarm.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type CloudwatchClient interface { diff --git a/aws-source/adapters/cloudwatch-alarm_test.go b/aws-source/adapters/cloudwatch-alarm_test.go index 85968b54..be179f7e 100644 --- a/aws-source/adapters/cloudwatch-alarm_test.go +++ b/aws-source/adapters/cloudwatch-alarm_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type testCloudwatchClient struct{} diff --git a/aws-source/adapters/cloudwatch-instance-metric.go b/aws-source/adapters/cloudwatch-instance-metric.go index 63de89c4..ef9a0e6a 100644 --- a/aws-source/adapters/cloudwatch-instance-metric.go +++ b/aws-source/adapters/cloudwatch-instance-metric.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // CloudwatchMetricClient defines the CloudWatch client interface for metrics diff --git a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go index 000ca15c..51208020 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_integration_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) // TestCloudwatchInstanceMetricIntegration fetches real CloudWatch metrics for an EC2 instance diff --git a/aws-source/adapters/cloudwatch-instance-metric_test.go b/aws-source/adapters/cloudwatch-instance-metric_test.go index d802a13f..ea7169c2 100644 --- a/aws-source/adapters/cloudwatch-instance-metric_test.go +++ b/aws-source/adapters/cloudwatch-instance-metric_test.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) // testCloudwatchMetricClient is a mock client for testing GetMetricData diff --git a/aws-source/adapters/cloudwatch_metric_links.go b/aws-source/adapters/cloudwatch_metric_links.go index b88b805a..4cf460a0 100644 --- a/aws-source/adapters/cloudwatch_metric_links.go +++ b/aws-source/adapters/cloudwatch_metric_links.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) var ErrNoQuery = errors.New("no query found") diff --git a/aws-source/adapters/directconnect-connection.go b/aws-source/adapters/directconnect-connection.go index f3e63a7e..f1303c1a 100644 --- a/aws-source/adapters/directconnect-connection.go +++ b/aws-source/adapters/directconnect-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeConnectionsInput, output *directconnect.DescribeConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-connection_test.go b/aws-source/adapters/directconnect-connection_test.go index bfbdc31a..86dc2d8a 100644 --- a/aws-source/adapters/directconnect-connection_test.go +++ b/aws-source/adapters/directconnect-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectconnectConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-customer-metadata.go b/aws-source/adapters/directconnect-customer-metadata.go index 9cf83252..187903c9 100644 --- a/aws-source/adapters/directconnect-customer-metadata.go +++ b/aws-source/adapters/directconnect-customer-metadata.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func customerMetadataOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeCustomerMetadataInput, output *directconnect.DescribeCustomerMetadataOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-customer-metadata_test.go b/aws-source/adapters/directconnect-customer-metadata_test.go index 8179344d..f2375df6 100644 --- a/aws-source/adapters/directconnect-customer-metadata_test.go +++ b/aws-source/adapters/directconnect-customer-metadata_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCustomerMetadataOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go index 4b01bcea..c92bf20f 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayAssociationProposalOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAssociationProposalsInput, output *directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go index 327ec577..942eff81 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAssociationProposalOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association.go b/aws-source/adapters/directconnect-direct-connect-gateway-association.go index 916219e3..95efa3cd 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const ( diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go index 833afdc6..7fc04408 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAssociationOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go index f1c7d2b4..22d7bdab 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewayAttachmentsInput, output *directconnect.DescribeDirectConnectGatewayAttachmentsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go index 68752a29..096629a3 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayAttachmentOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway.go b/aws-source/adapters/directconnect-direct-connect-gateway.go index 8a03d217..1d73ea81 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func directConnectGatewayOutputMapper(ctx context.Context, cli *directconnect.Client, scope string, _ *directconnect.DescribeDirectConnectGatewaysInput, output *directconnect.DescribeDirectConnectGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-direct-connect-gateway_test.go b/aws-source/adapters/directconnect-direct-connect-gateway_test.go index b89f382f..32ed2630 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDirectConnectGatewayOutputMapper_Health_OK(t *testing.T) { diff --git a/aws-source/adapters/directconnect-hosted-connection.go b/aws-source/adapters/directconnect-hosted-connection.go index 7ed66f52..c64fbb1b 100644 --- a/aws-source/adapters/directconnect-hosted-connection.go +++ b/aws-source/adapters/directconnect-hosted-connection.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeHostedConnectionsInput, output *directconnect.DescribeHostedConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-hosted-connection_test.go b/aws-source/adapters/directconnect-hosted-connection_test.go index 4921f61d..6e869b45 100644 --- a/aws-source/adapters/directconnect-hosted-connection_test.go +++ b/aws-source/adapters/directconnect-hosted-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHostedConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-interconnect.go b/aws-source/adapters/directconnect-interconnect.go index 26d5cbbd..161dfd3d 100644 --- a/aws-source/adapters/directconnect-interconnect.go +++ b/aws-source/adapters/directconnect-interconnect.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeInterconnectsInput, output *directconnect.DescribeInterconnectsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-interconnect_test.go b/aws-source/adapters/directconnect-interconnect_test.go index 8c0d4f55..84f09a63 100644 --- a/aws-source/adapters/directconnect-interconnect_test.go +++ b/aws-source/adapters/directconnect-interconnect_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInterconnectOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-lag.go b/aws-source/adapters/directconnect-lag.go index fb64f1f6..9b3831e1 100644 --- a/aws-source/adapters/directconnect-lag.go +++ b/aws-source/adapters/directconnect-lag.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLagsInput, output *directconnect.DescribeLagsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-lag_test.go b/aws-source/adapters/directconnect-lag_test.go index 10da521f..6632482b 100644 --- a/aws-source/adapters/directconnect-lag_test.go +++ b/aws-source/adapters/directconnect-lag_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" diff --git a/aws-source/adapters/directconnect-location.go b/aws-source/adapters/directconnect-location.go index 1c620f87..f514f884 100644 --- a/aws-source/adapters/directconnect-location.go +++ b/aws-source/adapters/directconnect-location.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func locationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeLocationsInput, output *directconnect.DescribeLocationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-location_test.go b/aws-source/adapters/directconnect-location_test.go index a714e5a1..9c18c9f6 100644 --- a/aws-source/adapters/directconnect-location_test.go +++ b/aws-source/adapters/directconnect-location_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLocationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-router-configuration.go b/aws-source/adapters/directconnect-router-configuration.go index 75939f66..4a8e4d83 100644 --- a/aws-source/adapters/directconnect-router-configuration.go +++ b/aws-source/adapters/directconnect-router-configuration.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func routerConfigurationOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeRouterConfigurationInput, output *directconnect.DescribeRouterConfigurationOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-router-configuration_test.go b/aws-source/adapters/directconnect-router-configuration_test.go index 9bee05df..6a151e2d 100644 --- a/aws-source/adapters/directconnect-router-configuration_test.go +++ b/aws-source/adapters/directconnect-router-configuration_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRouterConfigurationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-gateway.go b/aws-source/adapters/directconnect-virtual-gateway.go index 47bb351a..36ed2c06 100644 --- a/aws-source/adapters/directconnect-virtual-gateway.go +++ b/aws-source/adapters/directconnect-virtual-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func virtualGatewayOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ *directconnect.DescribeVirtualGatewaysInput, output *directconnect.DescribeVirtualGatewaysOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/directconnect-virtual-gateway_test.go b/aws-source/adapters/directconnect-virtual-gateway_test.go index 49dd6a35..c4003bb5 100644 --- a/aws-source/adapters/directconnect-virtual-gateway_test.go +++ b/aws-source/adapters/directconnect-virtual-gateway_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVirtualGatewayOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/directconnect-virtual-interface.go b/aws-source/adapters/directconnect-virtual-interface.go index 8e885617..906fa032 100644 --- a/aws-source/adapters/directconnect-virtual-interface.go +++ b/aws-source/adapters/directconnect-virtual-interface.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const gatewayIDVirtualInterfaceIDFormat = "gateway_id/virtual_interface_id" diff --git a/aws-source/adapters/directconnect-virtual-interface_test.go b/aws-source/adapters/directconnect-virtual-interface_test.go index 0c6b9c32..396737b2 100644 --- a/aws-source/adapters/directconnect-virtual-interface_test.go +++ b/aws-source/adapters/directconnect-virtual-interface_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/directconnect" "github.com/aws/aws-sdk-go-v2/service/directconnect/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVirtualInterfaceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/dynamodb-backup.go b/aws-source/adapters/dynamodb-backup.go index 0528cf65..9d1c599c 100644 --- a/aws-source/adapters/dynamodb-backup.go +++ b/aws-source/adapters/dynamodb-backup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func backupGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeBackupInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/dynamodb-backup_test.go b/aws-source/adapters/dynamodb-backup_test.go index 5750d0ff..4b924a28 100644 --- a/aws-source/adapters/dynamodb-backup_test.go +++ b/aws-source/adapters/dynamodb-backup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *DynamoDBTestClient) DescribeBackup(ctx context.Context, params *dynamodb.DescribeBackupInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DescribeBackupOutput, error) { diff --git a/aws-source/adapters/dynamodb-table.go b/aws-source/adapters/dynamodb-table.go index c934e388..2bb55dfe 100644 --- a/aws-source/adapters/dynamodb-table.go +++ b/aws-source/adapters/dynamodb-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func tableGetFunc(ctx context.Context, client Client, scope string, input *dynamodb.DescribeTableInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/dynamodb-table_test.go b/aws-source/adapters/dynamodb-table_test.go index ba5dc94f..ecbbfadd 100644 --- a/aws-source/adapters/dynamodb-table_test.go +++ b/aws-source/adapters/dynamodb-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) { diff --git a/aws-source/adapters/ec2-address.go b/aws-source/adapters/ec2-address.go index bdaaccb6..473b0422 100644 --- a/aws-source/adapters/ec2-address.go +++ b/aws-source/adapters/ec2-address.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // AddressInputMapperGet Maps adapter calls to the correct input for the AZ API diff --git a/aws-source/adapters/ec2-address_test.go b/aws-source/adapters/ec2-address_test.go index edd03592..59262cc9 100644 --- a/aws-source/adapters/ec2-address_test.go +++ b/aws-source/adapters/ec2-address_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAddressInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet.go b/aws-source/adapters/ec2-capacity-reservation-fleet.go index 4c844c41..75898c34 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func capacityReservationFleetOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationFleetsInput, output *ec2.DescribeCapacityReservationFleetsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go index d72391a8..cb81a86f 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCapacityReservationFleetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-capacity-reservation.go b/aws-source/adapters/ec2-capacity-reservation.go index 5081c15c..5585deb1 100644 --- a/aws-source/adapters/ec2-capacity-reservation.go +++ b/aws-source/adapters/ec2-capacity-reservation.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeCapacityReservationsInput, output *ec2.DescribeCapacityReservationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-capacity-reservation_test.go b/aws-source/adapters/ec2-capacity-reservation_test.go index c43dd0ef..94394fec 100644 --- a/aws-source/adapters/ec2-capacity-reservation_test.go +++ b/aws-source/adapters/ec2-capacity-reservation_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCapacityReservationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway.go b/aws-source/adapters/ec2-egress-only-internet-gateway.go index a15f6f0e..703181f8 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func egressOnlyInternetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeEgressOnlyInternetGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go index daa57359..4f1362d0 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestEgressOnlyInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-iam-instance-profile-association.go b/aws-source/adapters/ec2-iam-instance-profile-association.go index 4fbef64e..ded6b4c2 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeIamInstanceProfileAssociationsInput, output *ec2.DescribeIamInstanceProfileAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-iam-instance-profile-association_test.go b/aws-source/adapters/ec2-iam-instance-profile-association_test.go index 0403ffbc..a0573570 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association_test.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestIamInstanceProfileAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-image.go b/aws-source/adapters/ec2-image.go index dc5e77f0..c1052730 100644 --- a/aws-source/adapters/ec2-image.go +++ b/aws-source/adapters/ec2-image.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ImageInputMapperGet Gets a given image. As opposed to list, get will get diff --git a/aws-source/adapters/ec2-image_test.go b/aws-source/adapters/ec2-image_test.go index c9221f37..eadd9d08 100644 --- a/aws-source/adapters/ec2-image_test.go +++ b/aws-source/adapters/ec2-image_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestImageInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-event-window.go b/aws-source/adapters/ec2-instance-event-window.go index 2f1223a2..ce71fd37 100644 --- a/aws-source/adapters/ec2-instance-event-window.go +++ b/aws-source/adapters/ec2-instance-event-window.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceEventWindowInputMapperGet(scope, query string) (*ec2.DescribeInstanceEventWindowsInput, error) { diff --git a/aws-source/adapters/ec2-instance-event-window_test.go b/aws-source/adapters/ec2-instance-event-window_test.go index 5046efe2..e0826d43 100644 --- a/aws-source/adapters/ec2-instance-event-window_test.go +++ b/aws-source/adapters/ec2-instance-event-window_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceEventWindowInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance-status.go b/aws-source/adapters/ec2-instance-status.go index b3ab2e78..ef7ee70a 100644 --- a/aws-source/adapters/ec2-instance-status.go +++ b/aws-source/adapters/ec2-instance-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceStatusInputMapperGet(scope, query string) (*ec2.DescribeInstanceStatusInput, error) { diff --git a/aws-source/adapters/ec2-instance-status_test.go b/aws-source/adapters/ec2-instance-status_test.go index 4a6868d9..f4ff907e 100644 --- a/aws-source/adapters/ec2-instance-status_test.go +++ b/aws-source/adapters/ec2-instance-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-instance.go b/aws-source/adapters/ec2-instance.go index 716cf95a..71a88f96 100644 --- a/aws-source/adapters/ec2-instance.go +++ b/aws-source/adapters/ec2-instance.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ( diff --git a/aws-source/adapters/ec2-instance_test.go b/aws-source/adapters/ec2-instance_test.go index accbbc5c..6fc7f5e9 100644 --- a/aws-source/adapters/ec2-instance_test.go +++ b/aws-source/adapters/ec2-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-internet-gateway.go b/aws-source/adapters/ec2-internet-gateway.go index d5e15e6e..58b6adec 100644 --- a/aws-source/adapters/ec2-internet-gateway.go +++ b/aws-source/adapters/ec2-internet-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func internetGatewayInputMapperGet(scope string, query string) (*ec2.DescribeInternetGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-internet-gateway_test.go b/aws-source/adapters/ec2-internet-gateway_test.go index 0db2bd43..4c6888d5 100644 --- a/aws-source/adapters/ec2-internet-gateway_test.go +++ b/aws-source/adapters/ec2-internet-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInternetGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-key-pair.go b/aws-source/adapters/ec2-key-pair.go index bae9dda5..b1ef963c 100644 --- a/aws-source/adapters/ec2-key-pair.go +++ b/aws-source/adapters/ec2-key-pair.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func keyPairInputMapperGet(scope string, query string) (*ec2.DescribeKeyPairsInput, error) { diff --git a/aws-source/adapters/ec2-key-pair_test.go b/aws-source/adapters/ec2-key-pair_test.go index 100302bf..780daf80 100644 --- a/aws-source/adapters/ec2-key-pair_test.go +++ b/aws-source/adapters/ec2-key-pair_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestKeyPairInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template-version.go b/aws-source/adapters/ec2-launch-template-version.go index a8736734..3f0cfbc9 100644 --- a/aws-source/adapters/ec2-launch-template-version.go +++ b/aws-source/adapters/ec2-launch-template-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func launchTemplateVersionInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplateVersionsInput, error) { diff --git a/aws-source/adapters/ec2-launch-template-version_test.go b/aws-source/adapters/ec2-launch-template-version_test.go index b2a5d305..32ac3182 100644 --- a/aws-source/adapters/ec2-launch-template-version_test.go +++ b/aws-source/adapters/ec2-launch-template-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLaunchTemplateVersionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-launch-template.go b/aws-source/adapters/ec2-launch-template.go index b3653a2b..69619df5 100644 --- a/aws-source/adapters/ec2-launch-template.go +++ b/aws-source/adapters/ec2-launch-template.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func launchTemplateInputMapperGet(scope string, query string) (*ec2.DescribeLaunchTemplatesInput, error) { diff --git a/aws-source/adapters/ec2-launch-template_test.go b/aws-source/adapters/ec2-launch-template_test.go index e67d0d28..afa43420 100644 --- a/aws-source/adapters/ec2-launch-template_test.go +++ b/aws-source/adapters/ec2-launch-template_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLaunchTemplateInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-nat-gateway.go b/aws-source/adapters/ec2-nat-gateway.go index df8b4a57..9738e4c0 100644 --- a/aws-source/adapters/ec2-nat-gateway.go +++ b/aws-source/adapters/ec2-nat-gateway.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func natGatewayInputMapperGet(scope string, query string) (*ec2.DescribeNatGatewaysInput, error) { diff --git a/aws-source/adapters/ec2-nat-gateway_test.go b/aws-source/adapters/ec2-nat-gateway_test.go index 7dffc6f6..88e739e5 100644 --- a/aws-source/adapters/ec2-nat-gateway_test.go +++ b/aws-source/adapters/ec2-nat-gateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNatGatewayInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-acl.go b/aws-source/adapters/ec2-network-acl.go index 46da380b..ec35b197 100644 --- a/aws-source/adapters/ec2-network-acl.go +++ b/aws-source/adapters/ec2-network-acl.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkAclInputMapperGet(scope string, query string) (*ec2.DescribeNetworkAclsInput, error) { diff --git a/aws-source/adapters/ec2-network-acl_test.go b/aws-source/adapters/ec2-network-acl_test.go index 3c4b88ba..fba190e0 100644 --- a/aws-source/adapters/ec2-network-acl_test.go +++ b/aws-source/adapters/ec2-network-acl_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkAclInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface-permission.go b/aws-source/adapters/ec2-network-interface-permission.go index 9023762b..e914ea0b 100644 --- a/aws-source/adapters/ec2-network-interface-permission.go +++ b/aws-source/adapters/ec2-network-interface-permission.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkInterfacePermissionInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacePermissionsInput, error) { diff --git a/aws-source/adapters/ec2-network-interface-permission_test.go b/aws-source/adapters/ec2-network-interface-permission_test.go index 72c0d564..93104d99 100644 --- a/aws-source/adapters/ec2-network-interface-permission_test.go +++ b/aws-source/adapters/ec2-network-interface-permission_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkInterfacePermissionInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-network-interface.go b/aws-source/adapters/ec2-network-interface.go index fc795ee3..73c28e20 100644 --- a/aws-source/adapters/ec2-network-interface.go +++ b/aws-source/adapters/ec2-network-interface.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkInterfaceInputMapperGet(scope string, query string) (*ec2.DescribeNetworkInterfacesInput, error) { diff --git a/aws-source/adapters/ec2-network-interface_test.go b/aws-source/adapters/ec2-network-interface_test.go index 861cbd79..2a03331f 100644 --- a/aws-source/adapters/ec2-network-interface_test.go +++ b/aws-source/adapters/ec2-network-interface_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNetworkInterfaceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-placement-group.go b/aws-source/adapters/ec2-placement-group.go index c03701f5..9685f2c2 100644 --- a/aws-source/adapters/ec2-placement-group.go +++ b/aws-source/adapters/ec2-placement-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func placementGroupInputMapperGet(scope string, query string) (*ec2.DescribePlacementGroupsInput, error) { diff --git a/aws-source/adapters/ec2-placement-group_test.go b/aws-source/adapters/ec2-placement-group_test.go index db743000..ca79deb5 100644 --- a/aws-source/adapters/ec2-placement-group_test.go +++ b/aws-source/adapters/ec2-placement-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestPlacementGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-reserved-instance.go b/aws-source/adapters/ec2-reserved-instance.go index 3e8a3472..c6294793 100644 --- a/aws-source/adapters/ec2-reserved-instance.go +++ b/aws-source/adapters/ec2-reserved-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func reservedInstanceInputMapperGet(scope, query string) (*ec2.DescribeReservedInstancesInput, error) { diff --git a/aws-source/adapters/ec2-reserved-instance_test.go b/aws-source/adapters/ec2-reserved-instance_test.go index 9ef700da..9a381de8 100644 --- a/aws-source/adapters/ec2-reserved-instance_test.go +++ b/aws-source/adapters/ec2-reserved-instance_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestReservedInstanceInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-route-table.go b/aws-source/adapters/ec2-route-table.go index 1e12f1cf..6dc36780 100644 --- a/aws-source/adapters/ec2-route-table.go +++ b/aws-source/adapters/ec2-route-table.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func routeTableInputMapperGet(scope string, query string) (*ec2.DescribeRouteTablesInput, error) { diff --git a/aws-source/adapters/ec2-route-table_test.go b/aws-source/adapters/ec2-route-table_test.go index 867dc678..7d5d1875 100644 --- a/aws-source/adapters/ec2-route-table_test.go +++ b/aws-source/adapters/ec2-route-table_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRouteTableInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group-rule.go b/aws-source/adapters/ec2-security-group-rule.go index f51ec404..7c3261b9 100644 --- a/aws-source/adapters/ec2-security-group-rule.go +++ b/aws-source/adapters/ec2-security-group-rule.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func securityGroupRuleInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupRulesInput, error) { diff --git a/aws-source/adapters/ec2-security-group-rule_test.go b/aws-source/adapters/ec2-security-group-rule_test.go index a34989ca..95465917 100644 --- a/aws-source/adapters/ec2-security-group-rule_test.go +++ b/aws-source/adapters/ec2-security-group-rule_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSecurityGroupRuleInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-security-group.go b/aws-source/adapters/ec2-security-group.go index de801c34..e4967640 100644 --- a/aws-source/adapters/ec2-security-group.go +++ b/aws-source/adapters/ec2-security-group.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func securityGroupInputMapperGet(scope string, query string) (*ec2.DescribeSecurityGroupsInput, error) { diff --git a/aws-source/adapters/ec2-security-group_test.go b/aws-source/adapters/ec2-security-group_test.go index ae9ddbac..ca1379a4 100644 --- a/aws-source/adapters/ec2-security-group_test.go +++ b/aws-source/adapters/ec2-security-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSecurityGroupInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-snapshot.go b/aws-source/adapters/ec2-snapshot.go index 445c815c..c94536ac 100644 --- a/aws-source/adapters/ec2-snapshot.go +++ b/aws-source/adapters/ec2-snapshot.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func snapshotInputMapperGet(scope string, query string) (*ec2.DescribeSnapshotsInput, error) { diff --git a/aws-source/adapters/ec2-snapshot_test.go b/aws-source/adapters/ec2-snapshot_test.go index 732e96ab..bf485224 100644 --- a/aws-source/adapters/ec2-snapshot_test.go +++ b/aws-source/adapters/ec2-snapshot_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSnapshotInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-subnet.go b/aws-source/adapters/ec2-subnet.go index 7ef9f311..b34598f5 100644 --- a/aws-source/adapters/ec2-subnet.go +++ b/aws-source/adapters/ec2-subnet.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func subnetInputMapperGet(scope string, query string) (*ec2.DescribeSubnetsInput, error) { diff --git a/aws-source/adapters/ec2-subnet_test.go b/aws-source/adapters/ec2-subnet_test.go index 1f78abab..693afacd 100644 --- a/aws-source/adapters/ec2-subnet_test.go +++ b/aws-source/adapters/ec2-subnet_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSubnetInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-volume-status.go b/aws-source/adapters/ec2-volume-status.go index 476096db..227c8bc9 100644 --- a/aws-source/adapters/ec2-volume-status.go +++ b/aws-source/adapters/ec2-volume-status.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func volumeStatusInputMapperGet(scope string, query string) (*ec2.DescribeVolumeStatusInput, error) { diff --git a/aws-source/adapters/ec2-volume-status_test.go b/aws-source/adapters/ec2-volume-status_test.go index d3b5b9d6..a5ac6cae 100644 --- a/aws-source/adapters/ec2-volume-status_test.go +++ b/aws-source/adapters/ec2-volume-status_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVolumeStatusInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-volume.go b/aws-source/adapters/ec2-volume.go index e1dc1a3a..18360be3 100644 --- a/aws-source/adapters/ec2-volume.go +++ b/aws-source/adapters/ec2-volume.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func volumeInputMapperGet(scope string, query string) (*ec2.DescribeVolumesInput, error) { diff --git a/aws-source/adapters/ec2-volume_test.go b/aws-source/adapters/ec2-volume_test.go index 45014b13..277c8dbb 100644 --- a/aws-source/adapters/ec2-volume_test.go +++ b/aws-source/adapters/ec2-volume_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVolumeInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-endpoint.go b/aws-source/adapters/ec2-vpc-endpoint.go index d0fd94a0..05c7a156 100644 --- a/aws-source/adapters/ec2-vpc-endpoint.go +++ b/aws-source/adapters/ec2-vpc-endpoint.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcEndpointInputMapperGet(scope string, query string) (*ec2.DescribeVpcEndpointsInput, error) { diff --git a/aws-source/adapters/ec2-vpc-endpoint_test.go b/aws-source/adapters/ec2-vpc-endpoint_test.go index 41436e97..8a471d13 100644 --- a/aws-source/adapters/ec2-vpc-endpoint_test.go +++ b/aws-source/adapters/ec2-vpc-endpoint_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcEndpointInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc-peering-connection.go b/aws-source/adapters/ec2-vpc-peering-connection.go index 9aaadbdb..90149772 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection.go +++ b/aws-source/adapters/ec2-vpc-peering-connection.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope string, input *ec2.DescribeVpcPeeringConnectionsInput, output *ec2.DescribeVpcPeeringConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/ec2-vpc-peering-connection_test.go b/aws-source/adapters/ec2-vpc-peering-connection_test.go index 770234fa..6b2b6da3 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection_test.go +++ b/aws-source/adapters/ec2-vpc-peering-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcPeeringConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/ec2-vpc.go b/aws-source/adapters/ec2-vpc.go index 5530b0c8..4c7cb989 100644 --- a/aws-source/adapters/ec2-vpc.go +++ b/aws-source/adapters/ec2-vpc.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcInputMapperGet(scope string, query string) (*ec2.DescribeVpcsInput, error) { diff --git a/aws-source/adapters/ec2-vpc_test.go b/aws-source/adapters/ec2-vpc_test.go index b4032a3c..fea243cd 100644 --- a/aws-source/adapters/ec2-vpc_test.go +++ b/aws-source/adapters/ec2-vpc_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestVpcInputMapperGet(t *testing.T) { diff --git a/aws-source/adapters/ecs-capacity-provider.go b/aws-source/adapters/ecs-capacity-provider.go index b963a199..a9e234c1 100644 --- a/aws-source/adapters/ecs-capacity-provider.go +++ b/aws-source/adapters/ecs-capacity-provider.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var CapacityProviderIncludeFields = []types.CapacityProviderField{ diff --git a/aws-source/adapters/ecs-capacity-provider_test.go b/aws-source/adapters/ecs-capacity-provider_test.go index 41024f74..77724230 100644 --- a/aws-source/adapters/ecs-capacity-provider_test.go +++ b/aws-source/adapters/ecs-capacity-provider_test.go @@ -7,9 +7,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeCapacityProviders(ctx context.Context, params *ecs.DescribeCapacityProvidersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeCapacityProvidersOutput, error) { diff --git a/aws-source/adapters/ecs-cluster.go b/aws-source/adapters/ecs-cluster.go index b19ba099..e7248788 100644 --- a/aws-source/adapters/ecs-cluster.go +++ b/aws-source/adapters/ecs-cluster.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ClusterIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-cluster_test.go b/aws-source/adapters/ecs-cluster_test.go index 97a88b5f..3ee97b51 100644 --- a/aws-source/adapters/ecs-cluster_test.go +++ b/aws-source/adapters/ecs-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeClusters(ctx context.Context, params *ecs.DescribeClustersInput, optFns ...func(*ecs.Options)) (*ecs.DescribeClustersOutput, error) { diff --git a/aws-source/adapters/ecs-container-instance.go b/aws-source/adapters/ecs-container-instance.go index e212d9f1..ec144590 100644 --- a/aws-source/adapters/ecs-container-instance.go +++ b/aws-source/adapters/ecs-container-instance.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ContainerInstanceIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-container-instance_test.go b/aws-source/adapters/ecs-container-instance_test.go index f547cae4..6a9b8632 100644 --- a/aws-source/adapters/ecs-container-instance_test.go +++ b/aws-source/adapters/ecs-container-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params *ecs.DescribeContainerInstancesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeContainerInstancesOutput, error) { diff --git a/aws-source/adapters/ecs-service.go b/aws-source/adapters/ecs-service.go index 327cc1fd..58ef6325 100644 --- a/aws-source/adapters/ecs-service.go +++ b/aws-source/adapters/ecs-service.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // ServiceIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-service_test.go b/aws-source/adapters/ecs-service_test.go index f9cc70ed..b7e6683d 100644 --- a/aws-source/adapters/ecs-service_test.go +++ b/aws-source/adapters/ecs-service_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error) { diff --git a/aws-source/adapters/ecs-task-definition.go b/aws-source/adapters/ecs-task-definition.go index 8ebcf4e3..531bf167 100644 --- a/aws-source/adapters/ecs-task-definition.go +++ b/aws-source/adapters/ecs-task-definition.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TaskDefinitionIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-task-definition_test.go b/aws-source/adapters/ecs-task-definition_test.go index 0f4c97aa..4b7ce3cb 100644 --- a/aws-source/adapters/ecs-task-definition_test.go +++ b/aws-source/adapters/ecs-task-definition_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { diff --git a/aws-source/adapters/ecs-task.go b/aws-source/adapters/ecs-task.go index 72ec8e65..da3852cd 100644 --- a/aws-source/adapters/ecs-task.go +++ b/aws-source/adapters/ecs-task.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // TaskIncludeFields Fields that we want included by default diff --git a/aws-source/adapters/ecs-task_test.go b/aws-source/adapters/ecs-task_test.go index cb137acf..e0e8b1b0 100644 --- a/aws-source/adapters/ecs-task_test.go +++ b/aws-source/adapters/ecs-task_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs" "github.com/aws/aws-sdk-go-v2/service/ecs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *ecsTestClient) DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { diff --git a/aws-source/adapters/efs-access-point.go b/aws-source/adapters/efs-access-point.go index d747ae01..d9614a4e 100644 --- a/aws-source/adapters/efs-access-point.go +++ b/aws-source/adapters/efs-access-point.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func AccessPointOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeAccessPointsInput, output *efs.DescribeAccessPointsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-access-point_test.go b/aws-source/adapters/efs-access-point_test.go index 561c04ba..2a47b2a6 100644 --- a/aws-source/adapters/efs-access-point_test.go +++ b/aws-source/adapters/efs-access-point_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAccessPointOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-backup-policy.go b/aws-source/adapters/efs-backup-policy.go index 5c6c64d2..cbb43401 100644 --- a/aws-source/adapters/efs-backup-policy.go +++ b/aws-source/adapters/efs-backup-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func BackupPolicyOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeBackupPolicyInput, output *efs.DescribeBackupPolicyOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-file-system.go b/aws-source/adapters/efs-file-system.go index 41f5dbc3..2721dfae 100644 --- a/aws-source/adapters/efs-file-system.go +++ b/aws-source/adapters/efs-file-system.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeFileSystemsInput, output *efs.DescribeFileSystemsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-file-system_test.go b/aws-source/adapters/efs-file-system_test.go index fd7ff8f6..6b0831dd 100644 --- a/aws-source/adapters/efs-file-system_test.go +++ b/aws-source/adapters/efs-file-system_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestFileSystemOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-mount-target.go b/aws-source/adapters/efs-mount-target.go index dc364cfe..d8a40bb3 100644 --- a/aws-source/adapters/efs-mount-target.go +++ b/aws-source/adapters/efs-mount-target.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeMountTargetsInput, output *efs.DescribeMountTargetsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-mount-target_test.go b/aws-source/adapters/efs-mount-target_test.go index 5344f8a2..30885f3b 100644 --- a/aws-source/adapters/efs-mount-target_test.go +++ b/aws-source/adapters/efs-mount-target_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestMountTargetOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/efs-replication-configuration.go b/aws-source/adapters/efs-replication-configuration.go index e48c0211..e70457cc 100644 --- a/aws-source/adapters/efs-replication-configuration.go +++ b/aws-source/adapters/efs-replication-configuration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scope string, input *efs.DescribeReplicationConfigurationsInput, output *efs.DescribeReplicationConfigurationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/efs-replication-configuration_test.go b/aws-source/adapters/efs-replication-configuration_test.go index a98bb31c..d3cf233c 100644 --- a/aws-source/adapters/efs-replication-configuration_test.go +++ b/aws-source/adapters/efs-replication-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/efs.go b/aws-source/adapters/efs.go index b2d81f00..c6d9c89e 100644 --- a/aws-source/adapters/efs.go +++ b/aws-source/adapters/efs.go @@ -2,7 +2,7 @@ package adapters import ( "github.com/aws/aws-sdk-go-v2/service/efs/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // lifeCycleStateToHealth Converts a lifecycle state to a health state diff --git a/aws-source/adapters/eks-addon.go b/aws-source/adapters/eks-addon.go index 2f73e13d..163a1b3a 100644 --- a/aws-source/adapters/eks-addon.go +++ b/aws-source/adapters/eks-addon.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func addonGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeAddonInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-addon_test.go b/aws-source/adapters/eks-addon_test.go index 5432416a..dacefe45 100644 --- a/aws-source/adapters/eks-addon_test.go +++ b/aws-source/adapters/eks-addon_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var AddonTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-cluster.go b/aws-source/adapters/eks-cluster.go index 58ac7b89..806bdb0d 100644 --- a/aws-source/adapters/eks-cluster.go +++ b/aws-source/adapters/eks-cluster.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeClusterInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-cluster_test.go b/aws-source/adapters/eks-cluster_test.go index 4920a91d..80dd9d3f 100644 --- a/aws-source/adapters/eks-cluster_test.go +++ b/aws-source/adapters/eks-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ClusterClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-fargate-profile.go b/aws-source/adapters/eks-fargate-profile.go index 4eae17e7..cd54ab90 100644 --- a/aws-source/adapters/eks-fargate-profile.go +++ b/aws-source/adapters/eks-fargate-profile.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeFargateProfileInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-fargate-profile_test.go b/aws-source/adapters/eks-fargate-profile_test.go index 3f830c73..6d6cf7db 100644 --- a/aws-source/adapters/eks-fargate-profile_test.go +++ b/aws-source/adapters/eks-fargate-profile_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var FargateTestClient = EKSTestClient{ diff --git a/aws-source/adapters/eks-nodegroup.go b/aws-source/adapters/eks-nodegroup.go index c79f7097..1df7c710 100644 --- a/aws-source/adapters/eks-nodegroup.go +++ b/aws-source/adapters/eks-nodegroup.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input *eks.DescribeNodegroupInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/eks-nodegroup_test.go b/aws-source/adapters/eks-nodegroup_test.go index 048db1b4..c630f329 100644 --- a/aws-source/adapters/eks-nodegroup_test.go +++ b/aws-source/adapters/eks-nodegroup_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var NodeGroupClient = EKSTestClient{ diff --git a/aws-source/adapters/elb-instance-health.go b/aws-source/adapters/elb-instance-health.go index ccf8800d..bd03ff61 100644 --- a/aws-source/adapters/elb-instance-health.go +++ b/aws-source/adapters/elb-instance-health.go @@ -9,8 +9,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) // InstanceHealthName Structured representation of an instance health's unique diff --git a/aws-source/adapters/elb-instance-health_test.go b/aws-source/adapters/elb-instance-health_test.go index aa1418fb..de83a9cc 100644 --- a/aws-source/adapters/elb-instance-health_test.go +++ b/aws-source/adapters/elb-instance-health_test.go @@ -7,7 +7,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestInstanceHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elb-load-balancer.go b/aws-source/adapters/elb-load-balancer.go index 077a9523..f7c39e9d 100644 --- a/aws-source/adapters/elb-load-balancer.go +++ b/aws-source/adapters/elb-load-balancer.go @@ -6,8 +6,8 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type elbClient interface { diff --git a/aws-source/adapters/elb-load-balancer_test.go b/aws-source/adapters/elb-load-balancer_test.go index 6060fe6d..e54a56c9 100644 --- a/aws-source/adapters/elb-load-balancer_test.go +++ b/aws-source/adapters/elb-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type mockElbClient struct{} diff --git a/aws-source/adapters/elbv2-listener.go b/aws-source/adapters/elbv2-listener.go index 483d2de0..98e491c4 100644 --- a/aws-source/adapters/elbv2-listener.go +++ b/aws-source/adapters/elbv2-listener.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeListenersInput, output *elbv2.DescribeListenersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-listener_test.go b/aws-source/adapters/elbv2-listener_test.go index c30fa8ee..01a3eb6c 100644 --- a/aws-source/adapters/elbv2-listener_test.go +++ b/aws-source/adapters/elbv2-listener_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestListenerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-load-balancer.go b/aws-source/adapters/elbv2-load-balancer.go index d330d0aa..5876349f 100644 --- a/aws-source/adapters/elbv2-load-balancer.go +++ b/aws-source/adapters/elbv2-load-balancer.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeLoadBalancersInput, output *elbv2.DescribeLoadBalancersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-load-balancer_test.go b/aws-source/adapters/elbv2-load-balancer_test.go index 4a5ee666..ed37a0a7 100644 --- a/aws-source/adapters/elbv2-load-balancer_test.go +++ b/aws-source/adapters/elbv2-load-balancer_test.go @@ -8,7 +8,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestLoadBalancerOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-rule.go b/aws-source/adapters/elbv2-rule.go index 913030f8..03ed6af0 100644 --- a/aws-source/adapters/elbv2-rule.go +++ b/aws-source/adapters/elbv2-rule.go @@ -5,8 +5,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func ruleOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeRulesInput, output *elbv2.DescribeRulesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-rule_test.go b/aws-source/adapters/elbv2-rule_test.go index 9fdd508f..1cdd7024 100644 --- a/aws-source/adapters/elbv2-rule_test.go +++ b/aws-source/adapters/elbv2-rule_test.go @@ -9,9 +9,9 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestRuleOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-group.go b/aws-source/adapters/elbv2-target-group.go index 9d7789d0..e6f2da38 100644 --- a/aws-source/adapters/elbv2-target-group.go +++ b/aws-source/adapters/elbv2-target-group.go @@ -6,8 +6,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope string, _ *elbv2.DescribeTargetGroupsInput, output *elbv2.DescribeTargetGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/elbv2-target-group_test.go b/aws-source/adapters/elbv2-target-group_test.go index b296a9d9..44ec62ba 100644 --- a/aws-source/adapters/elbv2-target-group_test.go +++ b/aws-source/adapters/elbv2-target-group_test.go @@ -8,8 +8,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestTargetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2-target-health.go b/aws-source/adapters/elbv2-target-health.go index e4c129f2..851e834a 100644 --- a/aws-source/adapters/elbv2-target-health.go +++ b/aws-source/adapters/elbv2-target-health.go @@ -10,8 +10,8 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type TargetHealthUniqueID struct { diff --git a/aws-source/adapters/elbv2-target-health_test.go b/aws-source/adapters/elbv2-target-health_test.go index 72810f38..c15e8a62 100644 --- a/aws-source/adapters/elbv2-target-health_test.go +++ b/aws-source/adapters/elbv2-target-health_test.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTargetHealthOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/elbv2.go b/aws-source/adapters/elbv2.go index d083a106..3152431c 100644 --- a/aws-source/adapters/elbv2.go +++ b/aws-source/adapters/elbv2.go @@ -7,7 +7,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type elbv2Client interface { diff --git a/aws-source/adapters/elbv2_test.go b/aws-source/adapters/elbv2_test.go index 7be9fd0b..6079768a 100644 --- a/aws-source/adapters/elbv2_test.go +++ b/aws-source/adapters/elbv2_test.go @@ -6,7 +6,7 @@ import ( elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type mockElbv2Client struct{} diff --git a/aws-source/adapters/iam-group.go b/aws-source/adapters/iam-group.go index f8bae2f2..966eae40 100644 --- a/aws-source/adapters/iam-group.go +++ b/aws-source/adapters/iam-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func groupGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.Group, error) { diff --git a/aws-source/adapters/iam-group_test.go b/aws-source/adapters/iam-group_test.go index 20872517..2ad67320 100644 --- a/aws-source/adapters/iam-group_test.go +++ b/aws-source/adapters/iam-group_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestGroupItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-instance-profile.go b/aws-source/adapters/iam-instance-profile.go index b6fff4d2..4d691449 100644 --- a/aws-source/adapters/iam-instance-profile.go +++ b/aws-source/adapters/iam-instance-profile.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func instanceProfileGetFunc(ctx context.Context, client *iam.Client, _, query string) (*types.InstanceProfile, error) { diff --git a/aws-source/adapters/iam-instance-profile_test.go b/aws-source/adapters/iam-instance-profile_test.go index 2b86d004..50ade39e 100644 --- a/aws-source/adapters/iam-instance-profile_test.go +++ b/aws-source/adapters/iam-instance-profile_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestInstanceProfileItemMapper(t *testing.T) { diff --git a/aws-source/adapters/iam-policy.go b/aws-source/adapters/iam-policy.go index 16bd0713..15bbe465 100644 --- a/aws-source/adapters/iam-policy.go +++ b/aws-source/adapters/iam-policy.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/iam-policy_test.go b/aws-source/adapters/iam-policy_test.go index 6ce52a03..a9b85490 100644 --- a/aws-source/adapters/iam-policy_test.go +++ b/aws-source/adapters/iam-policy_test.go @@ -9,9 +9,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) GetPolicy(ctx context.Context, params *iam.GetPolicyInput, optFns ...func(*iam.Options)) (*iam.GetPolicyOutput, error) { diff --git a/aws-source/adapters/iam-role.go b/aws-source/adapters/iam-role.go index ed72506e..2fefdc11 100644 --- a/aws-source/adapters/iam-role.go +++ b/aws-source/adapters/iam-role.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/iter" ) diff --git a/aws-source/adapters/iam-role_test.go b/aws-source/adapters/iam-role_test.go index ee92d961..e08d1456 100644 --- a/aws-source/adapters/iam-role_test.go +++ b/aws-source/adapters/iam-role_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) { diff --git a/aws-source/adapters/iam-user.go b/aws-source/adapters/iam-user.go index 47c49fa1..cc74d326 100644 --- a/aws-source/adapters/iam-user.go +++ b/aws-source/adapters/iam-user.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type UserDetails struct { diff --git a/aws-source/adapters/iam-user_test.go b/aws-source/adapters/iam-user_test.go index bd432c76..66c6c8af 100644 --- a/aws-source/adapters/iam-user_test.go +++ b/aws-source/adapters/iam-user_test.go @@ -11,9 +11,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/iam/types" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListGroupsForUserInput, optFns ...func(*iam.Options)) (*iam.ListGroupsForUserOutput, error) { diff --git a/aws-source/adapters/iam.go b/aws-source/adapters/iam.go index 78128364..465daebc 100644 --- a/aws-source/adapters/iam.go +++ b/aws-source/adapters/iam.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type IAMClient interface { diff --git a/aws-source/adapters/iam_test.go b/aws-source/adapters/iam_test.go index f345b877..95a2aa25 100644 --- a/aws-source/adapters/iam_test.go +++ b/aws-source/adapters/iam_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/tracing" ) // TestIAMClient Test client that returns three pages diff --git a/aws-source/adapters/integration/apigateway/apigateway_test.go b/aws-source/adapters/integration/apigateway/apigateway_test.go index 1258ad25..79a7b7c4 100644 --- a/aws-source/adapters/integration/apigateway/apigateway_test.go +++ b/aws-source/adapters/integration/apigateway/apigateway_test.go @@ -7,8 +7,8 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func APIGateway(t *testing.T) { diff --git a/aws-source/adapters/integration/ec2/instance_test.go b/aws-source/adapters/integration/ec2/instance_test.go index d40a2774..56b1d241 100644 --- a/aws-source/adapters/integration/ec2/instance_test.go +++ b/aws-source/adapters/integration/ec2/instance_test.go @@ -7,9 +7,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/kms/kms_test.go b/aws-source/adapters/integration/kms/kms_test.go index 8669d281..750a7730 100644 --- a/aws-source/adapters/integration/kms/kms_test.go +++ b/aws-source/adapters/integration/kms/kms_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/networkmanager/networkmanager_test.go b/aws-source/adapters/integration/networkmanager/networkmanager_test.go index e224c985..712f1ad2 100644 --- a/aws-source/adapters/integration/networkmanager/networkmanager_test.go +++ b/aws-source/adapters/integration/networkmanager/networkmanager_test.go @@ -8,9 +8,9 @@ import ( "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/integration/ssm/main_test.go b/aws-source/adapters/integration/ssm/main_test.go index d8cab0c9..ab13915c 100644 --- a/aws-source/adapters/integration/ssm/main_test.go +++ b/aws-source/adapters/integration/ssm/main_test.go @@ -14,9 +14,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm/types" "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) diff --git a/aws-source/adapters/integration/util.go b/aws-source/adapters/integration/util.go index 67067b05..1b2b1b4a 100644 --- a/aws-source/adapters/integration/util.go +++ b/aws-source/adapters/integration/util.go @@ -13,8 +13,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" ) const ( diff --git a/aws-source/adapters/kms-alias.go b/aws-source/adapters/kms-alias.go index ac114c0d..fbd810b9 100644 --- a/aws-source/adapters/kms-alias.go +++ b/aws-source/adapters/kms-alias.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func aliasOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.ListAliasesInput, output *kms.ListAliasesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/kms-alias_test.go b/aws-source/adapters/kms-alias_test.go index de6566e1..3e99443a 100644 --- a/aws-source/adapters/kms-alias_test.go +++ b/aws-source/adapters/kms-alias_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestAliasOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-custom-key-store.go b/aws-source/adapters/kms-custom-key-store.go index 74fb2ce5..ad5673dc 100644 --- a/aws-source/adapters/kms-custom-key-store.go +++ b/aws-source/adapters/kms-custom-key-store.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.DescribeCustomKeyStoresInput, output *kms.DescribeCustomKeyStoresOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/kms-custom-key-store_test.go b/aws-source/adapters/kms-custom-key-store_test.go index 1cdcfaf3..19c2d834 100644 --- a/aws-source/adapters/kms-custom-key-store_test.go +++ b/aws-source/adapters/kms-custom-key-store_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestCustomKeyStoreOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/kms-grant.go b/aws-source/adapters/kms-grant.go index 2e638039..11a92659 100644 --- a/aws-source/adapters/kms-grant.go +++ b/aws-source/adapters/kms-grant.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/kms-grant_test.go b/aws-source/adapters/kms-grant_test.go index bef28adc..c2f8dbf9 100644 --- a/aws-source/adapters/kms-grant_test.go +++ b/aws-source/adapters/kms-grant_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key-policy.go b/aws-source/adapters/kms-key-policy.go index be21fd5e..6fbcb5d1 100644 --- a/aws-source/adapters/kms-key-policy.go +++ b/aws-source/adapters/kms-key-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/micahhausler/aws-iam-policy/policy" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" ) diff --git a/aws-source/adapters/kms-key-policy_test.go b/aws-source/adapters/kms-key-policy_test.go index 0aebd1c3..be46f433 100644 --- a/aws-source/adapters/kms-key-policy_test.go +++ b/aws-source/adapters/kms-key-policy_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) /* diff --git a/aws-source/adapters/kms-key.go b/aws-source/adapters/kms-key.go index 1c20b12e..a066927a 100644 --- a/aws-source/adapters/kms-key.go +++ b/aws-source/adapters/kms-key.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type kmsClient interface { diff --git a/aws-source/adapters/kms-key_test.go b/aws-source/adapters/kms-key_test.go index 1e135a51..9927110e 100644 --- a/aws-source/adapters/kms-key_test.go +++ b/aws-source/adapters/kms-key_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type kmsTestClient struct{} diff --git a/aws-source/adapters/lambda-event-source-mapping.go b/aws-source/adapters/lambda-event-source-mapping.go index ebb16c41..f97aeea3 100644 --- a/aws-source/adapters/lambda-event-source-mapping.go +++ b/aws-source/adapters/lambda-event-source-mapping.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type lambdaEventSourceMappingClient interface { diff --git a/aws-source/adapters/lambda-event-source-mapping_test.go b/aws-source/adapters/lambda-event-source-mapping_test.go index 63c170dd..9a9206ad 100644 --- a/aws-source/adapters/lambda-event-source-mapping_test.go +++ b/aws-source/adapters/lambda-event-source-mapping_test.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type TestLambdaEventSourceMappingClient struct{} diff --git a/aws-source/adapters/lambda-function.go b/aws-source/adapters/lambda-function.go index e63a200f..c118cd20 100644 --- a/aws-source/adapters/lambda-function.go +++ b/aws-source/adapters/lambda-function.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type FunctionDetails struct { diff --git a/aws-source/adapters/lambda-function_test.go b/aws-source/adapters/lambda-function_test.go index 239ec1d1..8a444c81 100644 --- a/aws-source/adapters/lambda-function_test.go +++ b/aws-source/adapters/lambda-function_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var testFuncConfig = &types.FunctionConfiguration{ diff --git a/aws-source/adapters/lambda-layer-version.go b/aws-source/adapters/lambda-layer-version.go index f3d6dcdd..6eb374b0 100644 --- a/aws-source/adapters/lambda-layer-version.go +++ b/aws-source/adapters/lambda-layer-version.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func layerVersionGetInputMapper(scope, query string) *lambda.GetLayerVersionInput { diff --git a/aws-source/adapters/lambda-layer-version_test.go b/aws-source/adapters/lambda-layer-version_test.go index 02da971e..15d1f253 100644 --- a/aws-source/adapters/lambda-layer-version_test.go +++ b/aws-source/adapters/lambda-layer-version_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLayerVersionGetInputMapper(t *testing.T) { diff --git a/aws-source/adapters/lambda-layer.go b/aws-source/adapters/lambda-layer.go index ebeb741b..c7b73e46 100644 --- a/aws-source/adapters/lambda-layer.go +++ b/aws-source/adapters/lambda-layer.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func layerListFunc(ctx context.Context, client *lambda.Client, scope string) ([]*types.LayersListItem, error) { diff --git a/aws-source/adapters/lambda-layer_test.go b/aws-source/adapters/lambda-layer_test.go index a1d2e0c5..d402bdd1 100644 --- a/aws-source/adapters/lambda-layer_test.go +++ b/aws-source/adapters/lambda-layer_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/lambda/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLayerItemMapper(t *testing.T) { diff --git a/aws-source/adapters/main.go b/aws-source/adapters/main.go index 08905ff3..856e1a97 100644 --- a/aws-source/adapters/main.go +++ b/aws-source/adapters/main.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) var Metadata = sdp.AdapterMetadataList{} diff --git a/aws-source/adapters/network-firewall-firewall-policy.go b/aws-source/adapters/network-firewall-firewall-policy.go index ca914622..7c427195 100644 --- a/aws-source/adapters/network-firewall-firewall-policy.go +++ b/aws-source/adapters/network-firewall-firewall-policy.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedFirewallPolicy struct { diff --git a/aws-source/adapters/network-firewall-firewall-policy_test.go b/aws-source/adapters/network-firewall-firewall-policy_test.go index 80728ae6..092aab28 100644 --- a/aws-source/adapters/network-firewall-firewall-policy_test.go +++ b/aws-source/adapters/network-firewall-firewall-policy_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-firewall.go b/aws-source/adapters/network-firewall-firewall.go index f3fb301a..d0074735 100644 --- a/aws-source/adapters/network-firewall-firewall.go +++ b/aws-source/adapters/network-firewall-firewall.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedFirewall struct { diff --git a/aws-source/adapters/network-firewall-firewall_test.go b/aws-source/adapters/network-firewall-firewall_test.go index 68bb239a..1092e319 100644 --- a/aws-source/adapters/network-firewall-firewall_test.go +++ b/aws-source/adapters/network-firewall-firewall_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params *networkfirewall.DescribeFirewallInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeFirewallOutput, error) { diff --git a/aws-source/adapters/network-firewall-rule-group.go b/aws-source/adapters/network-firewall-rule-group.go index d59ab06c..4e0c994b 100644 --- a/aws-source/adapters/network-firewall-rule-group.go +++ b/aws-source/adapters/network-firewall-rule-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedRuleGroup struct { diff --git a/aws-source/adapters/network-firewall-rule-group_test.go b/aws-source/adapters/network-firewall-rule-group_test.go index b8dd06b5..c5dd65a6 100644 --- a/aws-source/adapters/network-firewall-rule-group_test.go +++ b/aws-source/adapters/network-firewall-rule-group_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration.go b/aws-source/adapters/network-firewall-tls-inspection-configuration.go index 5892d208..460f45e4 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type unifiedTLSInspectionConfiguration struct { diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go index 3e2ee831..1ff37cdb 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "testing" "time" ) diff --git a/aws-source/adapters/networkfirewall.go b/aws-source/adapters/networkfirewall.go index d23c7860..c0c91b9f 100644 --- a/aws-source/adapters/networkfirewall.go +++ b/aws-source/adapters/networkfirewall.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkfirewall" "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) type networkFirewallClient interface { diff --git a/aws-source/adapters/networkmanager-connect-attachment.go b/aws-source/adapters/networkmanager-connect-attachment.go index 2dac9b47..f55bd4cc 100644 --- a/aws-source/adapters/networkmanager-connect-attachment.go +++ b/aws-source/adapters/networkmanager-connect-attachment.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.ConnectAttachment, error) { diff --git a/aws-source/adapters/networkmanager-connect-attachment_test.go b/aws-source/adapters/networkmanager-connect-attachment_test.go index 4a3b55ba..7e9479fe 100644 --- a/aws-source/adapters/networkmanager-connect-attachment_test.go +++ b/aws-source/adapters/networkmanager-connect-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestConnectAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer-association.go b/aws-source/adapters/networkmanager-connect-peer-association.go index 3a6631aa..5e59589b 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-connect-peer-association.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectPeerAssociationsInput, output *networkmanager.GetConnectPeerAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connect-peer-association_test.go b/aws-source/adapters/networkmanager-connect-peer-association_test.go index 25c4dbda..501a4c56 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-connect-peer.go b/aws-source/adapters/networkmanager-connect-peer.go index 7195086a..2d178120 100644 --- a/aws-source/adapters/networkmanager-connect-peer.go +++ b/aws-source/adapters/networkmanager-connect-peer.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetConnectPeerInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connect-peer_test.go b/aws-source/adapters/networkmanager-connect-peer_test.go index 22f2b127..c653214d 100644 --- a/aws-source/adapters/networkmanager-connect-peer_test.go +++ b/aws-source/adapters/networkmanager-connect-peer_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (n NetworkManagerTestClient) GetConnectPeer(ctx context.Context, params *networkmanager.GetConnectPeerInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetConnectPeerOutput, error) { diff --git a/aws-source/adapters/networkmanager-connection.go b/aws-source/adapters/networkmanager-connection.go index 37ba42f0..653e79da 100644 --- a/aws-source/adapters/networkmanager-connection.go +++ b/aws-source/adapters/networkmanager-connection.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetConnectionsInput, output *networkmanager.GetConnectionsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-connection_test.go b/aws-source/adapters/networkmanager-connection_test.go index 8e633c7b..a2cf5f1d 100644 --- a/aws-source/adapters/networkmanager-connection_test.go +++ b/aws-source/adapters/networkmanager-connection_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestConnectionOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network-policy.go b/aws-source/adapters/networkmanager-core-network-policy.go index 46c61af1..6b016a46 100644 --- a/aws-source/adapters/networkmanager-core-network-policy.go +++ b/aws-source/adapters/networkmanager-core-network-policy.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func coreNetworkPolicyGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.CoreNetworkPolicy, error) { diff --git a/aws-source/adapters/networkmanager-core-network-policy_test.go b/aws-source/adapters/networkmanager-core-network-policy_test.go index 25a3667a..b0bd39c5 100644 --- a/aws-source/adapters/networkmanager-core-network-policy_test.go +++ b/aws-source/adapters/networkmanager-core-network-policy_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestCoreNetworkPolicyItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-core-network.go b/aws-source/adapters/networkmanager-core-network.go index f11fc0ca..accaadd8 100644 --- a/aws-source/adapters/networkmanager-core-network.go +++ b/aws-source/adapters/networkmanager-core-network.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope string, input *networkmanager.GetCoreNetworkInput) (*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-core-network_test.go b/aws-source/adapters/networkmanager-core-network_test.go index 88d9fede..9283b49d 100644 --- a/aws-source/adapters/networkmanager-core-network_test.go +++ b/aws-source/adapters/networkmanager-core-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func (n NetworkManagerTestClient) GetCoreNetwork(ctx context.Context, params *networkmanager.GetCoreNetworkInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetCoreNetworkOutput, error) { diff --git a/aws-source/adapters/networkmanager-device.go b/aws-source/adapters/networkmanager-device.go index 308b774c..ecbf784e 100644 --- a/aws-source/adapters/networkmanager-device.go +++ b/aws-source/adapters/networkmanager-device.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetDevicesInput, output *networkmanager.GetDevicesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-device_test.go b/aws-source/adapters/networkmanager-device_test.go index 79deae1a..190278c3 100644 --- a/aws-source/adapters/networkmanager-device_test.go +++ b/aws-source/adapters/networkmanager-device_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDeviceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-global-network.go b/aws-source/adapters/networkmanager-global-network.go index c561f511..71d4d125 100644 --- a/aws-source/adapters/networkmanager-global-network.go +++ b/aws-source/adapters/networkmanager-global-network.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, scope string, _ *networkmanager.DescribeGlobalNetworksInput, output *networkmanager.DescribeGlobalNetworksOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-global-network_test.go b/aws-source/adapters/networkmanager-global-network_test.go index 02e8a8d4..8407c7b7 100644 --- a/aws-source/adapters/networkmanager-global-network_test.go +++ b/aws-source/adapters/networkmanager-global-network_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestGlobalNetworkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link-association.go b/aws-source/adapters/networkmanager-link-association.go index df3cebc2..57c240e3 100644 --- a/aws-source/adapters/networkmanager-link-association.go +++ b/aws-source/adapters/networkmanager-link-association.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinkAssociationsInput, output *networkmanager.GetLinkAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-link-association_test.go b/aws-source/adapters/networkmanager-link-association_test.go index f06f11bd..cb74c060 100644 --- a/aws-source/adapters/networkmanager-link-association_test.go +++ b/aws-source/adapters/networkmanager-link-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestLinkAssociationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-link.go b/aws-source/adapters/networkmanager-link.go index 87c87739..48b93e38 100644 --- a/aws-source/adapters/networkmanager-link.go +++ b/aws-source/adapters/networkmanager-link.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetLinksInput, output *networkmanager.GetLinksOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-link_test.go b/aws-source/adapters/networkmanager-link_test.go index 3468dc75..363d6838 100644 --- a/aws-source/adapters/networkmanager-link_test.go +++ b/aws-source/adapters/networkmanager-link_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestLinkOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-network-resource-relationship.go b/aws-source/adapters/networkmanager-network-resource-relationship.go index 24a17008..30adbd9f 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, input *networkmanager.GetNetworkResourceRelationshipsInput, output *networkmanager.GetNetworkResourceRelationshipsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-network-resource-relationship_test.go b/aws-source/adapters/networkmanager-network-resource-relationship_test.go index 464a1716..78dd910c 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship_test.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestNetworkResourceRelationshipOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go index 961d7439..9c0b0375 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getSiteToSiteVpnAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.SiteToSiteVpnAttachment, error) { diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go index a1c0ba85..473d8e11 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestSiteToSiteVpnAttachmentOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-site.go b/aws-source/adapters/networkmanager-site.go index 19fd7b06..1459c70d 100644 --- a/aws-source/adapters/networkmanager-site.go +++ b/aws-source/adapters/networkmanager-site.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetSitesInput, output *networkmanager.GetSitesOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-site_test.go b/aws-source/adapters/networkmanager-site_test.go index 4ce76b04..9c2e885f 100644 --- a/aws-source/adapters/networkmanager-site_test.go +++ b/aws-source/adapters/networkmanager-site_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSiteOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go index 1588ecb5..7cee9d11 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayConnectPeerAssociationsInput, output *networkmanager.GetTransitGatewayConnectPeerAssociationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go index d63b5bd7..6db8a6c8 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayConnectPeerAssociationsOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering.go b/aws-source/adapters/networkmanager-transit-gateway-peering.go index 5e1c140a..2c5b38d7 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getTransitGatewayPeeringGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayPeering, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go index f7552bde..03c8fd28 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayPeeringOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration.go b/aws-source/adapters/networkmanager-transit-gateway-registration.go index 7a9d9b2b..1e81be97 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, _ *networkmanager.GetTransitGatewayRegistrationsInput, output *networkmanager.GetTransitGatewayRegistrationsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go index 4de15a86..2b0fbeed 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayRegistrationOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go index 9653b0b4..248aab5f 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func getTransitGatewayRouteTableAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.TransitGatewayRouteTableAttachment, error) { diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go index 8d775e75..92ada429 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/networkmanager-vpc-attachment.go b/aws-source/adapters/networkmanager-vpc-attachment.go index 92cdcba7..764effec 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment.go +++ b/aws-source/adapters/networkmanager-vpc-attachment.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager" "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func vpcAttachmentGetFunc(ctx context.Context, client *networkmanager.Client, _, query string) (*types.VpcAttachment, error) { diff --git a/aws-source/adapters/networkmanager-vpc-attachment_test.go b/aws-source/adapters/networkmanager-vpc-attachment_test.go index bdcfcdf4..0e9baa94 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment_test.go +++ b/aws-source/adapters/networkmanager-vpc-attachment_test.go @@ -4,7 +4,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/networkmanager/types" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestVPCAttachmentItemMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group.go b/aws-source/adapters/rds-db-cluster-parameter-group.go index 8dd1689f..967ad11f 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type ClusterParameterGroup struct { diff --git a/aws-source/adapters/rds-db-cluster-parameter-group_test.go b/aws-source/adapters/rds-db-cluster-parameter-group_test.go index 3c79d2ea..e02ff713 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group_test.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBClusterParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-cluster.go b/aws-source/adapters/rds-db-cluster.go index 59db9fbe..418acd27 100644 --- a/aws-source/adapters/rds-db-cluster.go +++ b/aws-source/adapters/rds-db-cluster.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBClustersInput, output *rds.DescribeDBClustersOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/rds-db-cluster_test.go b/aws-source/adapters/rds-db-cluster_test.go index 7fbd2f8e..898fdfdc 100644 --- a/aws-source/adapters/rds-db-cluster_test.go +++ b/aws-source/adapters/rds-db-cluster_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBClusterOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-instance.go b/aws-source/adapters/rds-db-instance.go index 5dc63163..fb120f75 100644 --- a/aws-source/adapters/rds-db-instance.go +++ b/aws-source/adapters/rds-db-instance.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func statusToHealth(status string) *sdp.Health { diff --git a/aws-source/adapters/rds-db-instance_test.go b/aws-source/adapters/rds-db-instance_test.go index 94b2773d..8ccd1176 100644 --- a/aws-source/adapters/rds-db-instance_test.go +++ b/aws-source/adapters/rds-db-instance_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBInstanceOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-parameter-group.go b/aws-source/adapters/rds-db-parameter-group.go index 4d8e90e1..ea0d98ea 100644 --- a/aws-source/adapters/rds-db-parameter-group.go +++ b/aws-source/adapters/rds-db-parameter-group.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type ParameterGroup struct { diff --git a/aws-source/adapters/rds-db-parameter-group_test.go b/aws-source/adapters/rds-db-parameter-group_test.go index d403a519..e07c9cc9 100644 --- a/aws-source/adapters/rds-db-parameter-group_test.go +++ b/aws-source/adapters/rds-db-parameter-group_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBParameterGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-db-subnet-group.go b/aws-source/adapters/rds-db-subnet-group.go index 0cf96777..9cc43031 100644 --- a/aws-source/adapters/rds-db-subnet-group.go +++ b/aws-source/adapters/rds-db-subnet-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeDBSubnetGroupsInput, output *rds.DescribeDBSubnetGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/rds-db-subnet-group_test.go b/aws-source/adapters/rds-db-subnet-group_test.go index 4bf62d79..7b7db1dc 100644 --- a/aws-source/adapters/rds-db-subnet-group_test.go +++ b/aws-source/adapters/rds-db-subnet-group_test.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDBSubnetGroupOutputMapper(t *testing.T) { diff --git a/aws-source/adapters/rds-option-group.go b/aws-source/adapters/rds-option-group.go index af984c82..37787cd3 100644 --- a/aws-source/adapters/rds-option-group.go +++ b/aws-source/adapters/rds-option-group.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func optionGroupOutputMapper(ctx context.Context, client rdsClient, scope string, _ *rds.DescribeOptionGroupsInput, output *rds.DescribeOptionGroupsOutput) ([]*sdp.Item, error) { diff --git a/aws-source/adapters/route53-health-check.go b/aws-source/adapters/route53-health-check.go index 9f98fafb..ea78fcad 100644 --- a/aws-source/adapters/route53-health-check.go +++ b/aws-source/adapters/route53-health-check.go @@ -10,8 +10,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type HealthCheck struct { diff --git a/aws-source/adapters/route53-health-check_test.go b/aws-source/adapters/route53-health-check_test.go index af72f1b0..35939600 100644 --- a/aws-source/adapters/route53-health-check_test.go +++ b/aws-source/adapters/route53-health-check_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHealthCheckItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-hosted-zone.go b/aws-source/adapters/route53-hosted-zone.go index 63df5f28..2af9708f 100644 --- a/aws-source/adapters/route53-hosted-zone.go +++ b/aws-source/adapters/route53-hosted-zone.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func hostedZoneGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.HostedZone, error) { diff --git a/aws-source/adapters/route53-hosted-zone_test.go b/aws-source/adapters/route53-hosted-zone_test.go index abed7dc5..da48d7de 100644 --- a/aws-source/adapters/route53-hosted-zone_test.go +++ b/aws-source/adapters/route53-hosted-zone_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestHostedZoneItemMapper(t *testing.T) { diff --git a/aws-source/adapters/route53-resource-record-set.go b/aws-source/adapters/route53-resource-record-set.go index cc36042b..8fd99151 100644 --- a/aws-source/adapters/route53-resource-record-set.go +++ b/aws-source/adapters/route53-resource-record-set.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func resourceRecordSetGetFunc(ctx context.Context, client *route53.Client, scope, query string) (*types.ResourceRecordSet, error) { diff --git a/aws-source/adapters/route53-resource-record-set_test.go b/aws-source/adapters/route53-resource-record-set_test.go index 9559fd38..d18c15a9 100644 --- a/aws-source/adapters/route53-resource-record-set_test.go +++ b/aws-source/adapters/route53-resource-record-set_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/route53/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestResourceRecordSetItemMapper(t *testing.T) { diff --git a/aws-source/adapters/s3.go b/aws-source/adapters/s3.go index 9c3b21d6..7e0665ed 100644 --- a/aws-source/adapters/s3.go +++ b/aws-source/adapters/s3.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const CacheDuration = 10 * time.Minute diff --git a/aws-source/adapters/s3_test.go b/aws-source/adapters/s3_test.go index 65ae8fdd..574c87be 100644 --- a/aws-source/adapters/s3_test.go +++ b/aws-source/adapters/s3_test.go @@ -9,8 +9,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestS3SearchImpl(t *testing.T) { diff --git a/aws-source/adapters/sns-data-protection-policy.go b/aws-source/adapters/sns-data-protection-policy.go index 7f566cb5..bc3fdae3 100644 --- a/aws-source/adapters/sns-data-protection-policy.go +++ b/aws-source/adapters/sns-data-protection-policy.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type dataProtectionPolicyClient interface { diff --git a/aws-source/adapters/sns-data-protection-policy_test.go b/aws-source/adapters/sns-data-protection-policy_test.go index c9283234..3e3fe366 100644 --- a/aws-source/adapters/sns-data-protection-policy_test.go +++ b/aws-source/adapters/sns-data-protection-policy_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockDataProtectionPolicyClient struct{} diff --git a/aws-source/adapters/sns-endpoint.go b/aws-source/adapters/sns-endpoint.go index afbd1c2e..0b3b56cf 100644 --- a/aws-source/adapters/sns-endpoint.go +++ b/aws-source/adapters/sns-endpoint.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type endpointClient interface { diff --git a/aws-source/adapters/sns-endpoint_test.go b/aws-source/adapters/sns-endpoint_test.go index a71e638c..b26f5b65 100644 --- a/aws-source/adapters/sns-endpoint_test.go +++ b/aws-source/adapters/sns-endpoint_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockEndpointClient struct{} diff --git a/aws-source/adapters/sns-platform-application.go b/aws-source/adapters/sns-platform-application.go index 3750a65d..89893c5a 100644 --- a/aws-source/adapters/sns-platform-application.go +++ b/aws-source/adapters/sns-platform-application.go @@ -5,8 +5,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type platformApplicationClient interface { diff --git a/aws-source/adapters/sns-platform-application_test.go b/aws-source/adapters/sns-platform-application_test.go index a493b98c..f7fffbd6 100644 --- a/aws-source/adapters/sns-platform-application_test.go +++ b/aws-source/adapters/sns-platform-application_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type mockPlatformApplicationClient struct{} diff --git a/aws-source/adapters/sns-subscription.go b/aws-source/adapters/sns-subscription.go index 7fb609ad..f134ffae 100644 --- a/aws-source/adapters/sns-subscription.go +++ b/aws-source/adapters/sns-subscription.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type subsCli interface { diff --git a/aws-source/adapters/sns-subscription_test.go b/aws-source/adapters/sns-subscription_test.go index dc39df2c..d83d419f 100644 --- a/aws-source/adapters/sns-subscription_test.go +++ b/aws-source/adapters/sns-subscription_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type snsTestClient struct{} diff --git a/aws-source/adapters/sns-topic.go b/aws-source/adapters/sns-topic.go index 0eebb622..fac839d3 100644 --- a/aws-source/adapters/sns-topic.go +++ b/aws-source/adapters/sns-topic.go @@ -6,8 +6,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type topicClient interface { diff --git a/aws-source/adapters/sns-topic_test.go b/aws-source/adapters/sns-topic_test.go index e1a4f216..57f344de 100644 --- a/aws-source/adapters/sns-topic_test.go +++ b/aws-source/adapters/sns-topic_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sns" "github.com/aws/aws-sdk-go-v2/service/sns/types" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) type testTopicClient struct{} diff --git a/aws-source/adapters/sqs-queue.go b/aws-source/adapters/sqs-queue.go index d1f8713e..ee628ef4 100644 --- a/aws-source/adapters/sqs-queue.go +++ b/aws-source/adapters/sqs-queue.go @@ -7,8 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type sqsClient interface { diff --git a/aws-source/adapters/sqs-queue_test.go b/aws-source/adapters/sqs-queue_test.go index 42de525d..1500b11e 100644 --- a/aws-source/adapters/sqs-queue_test.go +++ b/aws-source/adapters/sqs-queue_test.go @@ -6,8 +6,8 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/sqs" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type testClient struct{} diff --git a/aws-source/adapters/ssm-parameter.go b/aws-source/adapters/ssm-parameter.go index 61e5e3ab..0294d5a3 100644 --- a/aws-source/adapters/ssm-parameter.go +++ b/aws-source/adapters/ssm-parameter.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/sourcegraph/conc/iter" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" diff --git a/aws-source/adapters/ssm-parameter_test.go b/aws-source/adapters/ssm-parameter_test.go index 6d199c9f..97e102f8 100644 --- a/aws-source/adapters/ssm-parameter_test.go +++ b/aws-source/adapters/ssm-parameter_test.go @@ -8,8 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/ssm/types" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" ) type mockSSMClient struct{} diff --git a/aws-source/build/package/Dockerfile b/aws-source/build/package/Dockerfile index a5b103da..5ea83d03 100644 --- a/aws-source/build/package/Dockerfile +++ b/aws-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source aws-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/aws-source/cmd/root.go b/aws-source/cmd/root.go index 07231afd..59f6e1d5 100644 --- a/aws-source/cmd/root.go +++ b/aws-source/cmd/root.go @@ -10,9 +10,9 @@ import ( "github.com/getsentry/sentry-go" "github.com/overmindtech/cli/aws-source/proc" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/logging" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/aws-source/proc/proc.go b/aws-source/proc/proc.go index e8881fe4..9a9c6c3c 100644 --- a/aws-source/proc/proc.go +++ b/aws-source/proc/proc.go @@ -41,8 +41,8 @@ import ( stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" diff --git a/aws-source/proc/proc_test.go b/aws-source/proc/proc_test.go index 097e6020..69c1f7d3 100644 --- a/aws-source/proc/proc_test.go +++ b/aws-source/proc/proc_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/aws/smithy-go" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cmd/auth_client.go b/cmd/auth_client.go index 2efef5be..3666f44d 100644 --- a/cmd/auth_client.go +++ b/cmd/auth_client.go @@ -7,10 +7,10 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/workspace/auth" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpconnect" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" ) diff --git a/cmd/auth_client_test.go b/cmd/auth_client_test.go index 7fa130b6..ec4516fc 100644 --- a/cmd/auth_client_test.go +++ b/cmd/auth_client_test.go @@ -14,8 +14,8 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/overmindtech/workspace/auth" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/tracing" ) // testProxyServer is a simple HTTP proxy server for testing diff --git a/cmd/bookmarks_create_bookmark.go b/cmd/bookmarks_create_bookmark.go index 4717c8bc..1e89d038 100644 --- a/cmd/bookmarks_create_bookmark.go +++ b/cmd/bookmarks_create_bookmark.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_affected_bookmarks.go b/cmd/bookmarks_get_affected_bookmarks.go index c0523149..e6dc62ef 100644 --- a/cmd/bookmarks_get_affected_bookmarks.go +++ b/cmd/bookmarks_get_affected_bookmarks.go @@ -5,7 +5,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/bookmarks_get_bookmark.go b/cmd/bookmarks_get_bookmark.go index 62e8646e..46a0fe5d 100644 --- a/cmd/bookmarks_get_bookmark.go +++ b/cmd/bookmarks_get_bookmark.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_end_change.go b/cmd/changes_end_change.go index 8a4837bb..a01cf151 100644 --- a/cmd/changes_end_change.go +++ b/cmd/changes_end_change.go @@ -4,7 +4,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index bb370c26..93f50d10 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -8,7 +8,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_get_change_test.go b/cmd/changes_get_change_test.go index 6f201b3f..55353e56 100644 --- a/cmd/changes_get_change_test.go +++ b/cmd/changes_get_change_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) func TestValidateChangeStatus(t *testing.T) { diff --git a/cmd/changes_get_signals.go b/cmd/changes_get_signals.go index 6f7e1311..8edb9dd6 100644 --- a/cmd/changes_get_signals.go +++ b/cmd/changes_get_signals.go @@ -6,7 +6,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_list_changes.go b/cmd/changes_list_changes.go index 6824cf08..7809e340 100644 --- a/cmd/changes_list_changes.go +++ b/cmd/changes_list_changes.go @@ -8,7 +8,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 86425be2..56e93fae 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -5,7 +5,7 @@ import ( "time" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 94405321..f47647d4 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -12,7 +12,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/changes_submit_signal.go b/cmd/changes_submit_signal.go index dafd6642..7e18a3a4 100644 --- a/cmd/changes_submit_signal.go +++ b/cmd/changes_submit_signal.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/explore.go b/cmd/explore.go index 8164042f..743240ee 100644 --- a/cmd/explore.go +++ b/cmd/explore.go @@ -18,12 +18,12 @@ import ( "github.com/overmindtech/pterm" "github.com/overmindtech/cli/aws-source/proc" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" azureproc "github.com/overmindtech/cli/sources/azure/proc" gcpproc "github.com/overmindtech/cli/sources/gcp/proc" stdlibSource "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" diff --git a/cmd/flags.go b/cmd/flags.go index 357625fe..5a4f3b2d 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/spf13/cobra" "github.com/spf13/viper" ) diff --git a/cmd/flags_test.go b/cmd/flags_test.go index ad30a99a..5521757a 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/spf13/viper" ) diff --git a/cmd/integrations_tfc.go b/cmd/integrations_tfc.go index babd2a21..7305805f 100644 --- a/cmd/integrations_tfc.go +++ b/cmd/integrations_tfc.go @@ -5,7 +5,7 @@ import ( "fmt" "connectrpc.com/connect" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/cmd/invites_crud.go b/cmd/invites_crud.go index 8800ec69..f5839f70 100644 --- a/cmd/invites_crud.go +++ b/cmd/invites_crud.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/jedib0t/go-pretty/v6/table" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/pterm.go b/cmd/pterm.go index de9c9881..3818ec37 100644 --- a/cmd/pterm.go +++ b/cmd/pterm.go @@ -15,11 +15,11 @@ import ( "connectrpc.com/connect" "github.com/overmindtech/pterm" - "github.com/overmindtech/workspace/auth" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpconnect" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "github.com/spf13/cobra" diff --git a/cmd/request.go b/cmd/request.go index 3f0d5b98..7596d4b8 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -4,8 +4,8 @@ import ( "context" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpws" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_load.go b/cmd/request_load.go index 16de26d6..2ff6a1a1 100644 --- a/cmd/request_load.go +++ b/cmd/request_load.go @@ -6,9 +6,9 @@ import ( "os" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpws" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/request_query.go b/cmd/request_query.go index 43887b7a..cb7fa349 100644 --- a/cmd/request_query.go +++ b/cmd/request_query.go @@ -7,9 +7,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpws" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/root.go b/cmd/root.go index 6e09e45a..512f4f95 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,9 +22,9 @@ import ( josejwt "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/workspace/auth" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/cmd/root_test.go b/cmd/root_test.go index c0589e02..bac3a178 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/auth" + "github.com/overmindtech/cli/go/auth" "golang.org/x/oauth2" ) diff --git a/cmd/snapshots_create.go b/cmd/snapshots_create.go index 03244504..22253f86 100644 --- a/cmd/snapshots_create.go +++ b/cmd/snapshots_create.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdp-go/sdpws" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpws" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/snapshots_get_snapshot.go b/cmd/snapshots_get_snapshot.go index 3778ed75..10914241 100644 --- a/cmd/snapshots_get_snapshot.go +++ b/cmd/snapshots_get_snapshot.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_apply.go b/cmd/terraform_apply.go index 02266d15..43f72216 100644 --- a/cmd/terraform_apply.go +++ b/cmd/terraform_apply.go @@ -11,7 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" "github.com/overmindtech/pterm" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index ad4643c6..b6e10be1 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -17,7 +17,7 @@ import ( "github.com/muesli/reflow/wordwrap" "github.com/overmindtech/pterm" "github.com/overmindtech/cli/tfutils" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/go/auth/auth.go b/go/auth/auth.go new file mode 100644 index 00000000..9b7e2ce2 --- /dev/null +++ b/go/auth/auth.go @@ -0,0 +1,448 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "time" + + "connectrpc.com/connect" + jose "github.com/go-jose/go-jose/v4" + josejwt "github.com/go-jose/go-jose/v4/jwt" + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nkeys" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/codes" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +const UserAgentVersion = "0.1" + +// TokenClient Represents something that is capable of getting NATS JWT tokens +// for a given set of NKeys +type TokenClient interface { + // Returns a NATS token that can be used to connect + GetJWT() (string, error) + + // Uses the NKeys associated with the token to sign some binary data + Sign([]byte) ([]byte, error) +} + +// BasicTokenClient stores a static token and returns it when called, ignoring +// any provided NKeys or context since it already has the token and doesn't need +// to make any requests +type BasicTokenClient struct { + staticToken string + staticKeys nkeys.KeyPair +} + +// assert interface implementation +var _ TokenClient = (*BasicTokenClient)(nil) + +// NewBasicTokenClient Creates a new basic token client that simply returns a static token +func NewBasicTokenClient(token string, keys nkeys.KeyPair) *BasicTokenClient { + return &BasicTokenClient{ + staticToken: token, + staticKeys: keys, + } +} + +func (b *BasicTokenClient) GetJWT() (string, error) { + return b.staticToken, nil +} + +func (b *BasicTokenClient) Sign(in []byte) ([]byte, error) { + return b.staticKeys.Sign(in) +} + +// ClientCredentialsConfig Authenticates to Overmind using the Client +// Credentials flow +// https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow +type ClientCredentialsConfig struct { + // The ClientID of the application that we'll be authenticating as + ClientID string + // ClientSecret that corresponds to the ClientID + ClientSecret string +} + +type TokenSourceOptionsFunc func(*clientcredentials.Config) + +// This option means that the token that is retrieved will have the following +// account embedded in it through impersonation. In order for this to work, the +// Auth0 ClientID must be added to workspace/deploy/auth0.tf. This will use +// deploy/auth0_embed_account_m2m.tftpl to update the Auth0 action that we use +// to allow impersonation. If this isn't done first you will get an error from +// Auth0. +func WithImpersonateAccount(account string) TokenSourceOptionsFunc { + return func(c *clientcredentials.Config) { + c.EndpointParams.Set("account_name", account) + } +} + +// TokenSource Returns a token source that can be used to get OAuth tokens. +// Cache this between invocations to avoid additional charges by Auth0 for M2M +// tokens. The oAuthTokenURL looks like this: +// https://somedomain.auth0.com/oauth/token +// +// The context that is passed to this function is used when getting new tokens, +// which will happen initially, and then subsequently when the token expires. +// This means that if this token source is going to be stored and used for many +// requests, it should not use the context of the request that created it, as +// this will be cancelled. Instead it should probably use `context.Background()` +// or similar. +func (flowConfig ClientCredentialsConfig) TokenSource(ctx context.Context, oAuthTokenURL, oAuthAudience string, opts ...TokenSourceOptionsFunc) oauth2.TokenSource { + // inject otel into oauth2 + ctx = context.WithValue(ctx, oauth2.HTTPClient, tracing.HTTPClient()) + + conf := &clientcredentials.Config{ + ClientID: flowConfig.ClientID, + ClientSecret: flowConfig.ClientSecret, + TokenURL: oAuthTokenURL, + EndpointParams: url.Values{ + "audience": []string{oAuthAudience}, + }, + } + + for _, opt := range opts { + opt(conf) + } + // this will be a `oauth2.ReuseTokenSource`, thus caching the M2M token. + // note that this token source is safe for concurrent use and will + // automatically refresh the token when it expires. Also note that this + // token source will use the passed in http client from otelhttp for all + // requests, but will not get the actual caller's context, so spans will not + // link up. + 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 +type natsTokenClient struct { + // The name of the account to impersonate. If this is omitted then the + // account will be determined based on the account included in the resulting + // token. + Account string + + // authenticated clients for the Overmind API + adminClient sdpconnect.AdminServiceClient + mgmtClient sdpconnect.ManagementServiceClient + + jwt string + keys nkeys.KeyPair +} + +// assert interface implementation +var _ TokenClient = (*natsTokenClient)(nil) + +// generateKeys Generates a new set of keys for the client +func (n *natsTokenClient) generateKeys() error { + var err error + + n.keys, err = nkeys.CreateUser() + + return err +} + +// generateJWT Gets a new JWT from the auth API +func (n *natsTokenClient) generateJWT(ctx context.Context) error { + if n.adminClient == nil || n.mgmtClient == nil { + return errors.New("no Overmind API client configured") + } + + // If we don't yet have keys generate them + if n.keys == nil { + err := n.generateKeys() + if err != nil { + return err + } + } + + pubKey, err := n.keys.PublicKey() + if err != nil { + return err + } + + hostname, err := os.Hostname() + if err != nil { + return err + } + + req := &sdp.CreateTokenRequest{ + UserPublicNkey: pubKey, + UserName: hostname, + } + + // Create the request for a NATS token + var response *connect.Response[sdp.CreateTokenResponse] + if n.Account == "" { + // Use the regular API and let the client authentication determine what our org should be + log.WithFields(log.Fields{ + "account": n.Account, + "publicNKey": req.GetUserPublicNkey(), + "UserName": req.GetUserName(), + }).Trace("Using regular API to get NATS token") + response, err = n.mgmtClient.CreateToken(ctx, connect.NewRequest(req)) + } else { + log.WithFields(log.Fields{ + "account": n.Account, + "publicNKey": req.GetUserPublicNkey(), + "UserName": req.GetUserName(), + }).Trace("Using admin API to get NATS token") + // Explicitly request an org + response, err = n.adminClient.CreateToken(ctx, connect.NewRequest(&sdp.AdminCreateTokenRequest{ + Account: n.Account, + Request: req, + })) + } + if err != nil { + return fmt.Errorf("getting NATS token failed: %w", err) + } + + n.jwt = response.Msg.GetToken() + + return nil +} + +func (n *natsTokenClient) GetJWT() (string, error) { + ctx, span := tracer.Start(context.Background(), "connect.GetJWT") + defer span.End() + + // If we don't yet have a JWT, generate one + if n.jwt == "" { + err := n.generateJWT(ctx) + if err != nil { + err = fmt.Errorf("error generating JWT: %w", err) + span.SetStatus(codes.Error, err.Error()) + return "", err + } + } + + claims, err := jwt.DecodeUserClaims(n.jwt) + if err != nil { + err = fmt.Errorf("error decoding JWT: %w", err) + span.SetStatus(codes.Error, err.Error()) + return n.jwt, err + } + + // Validate to make sure the JWT is valid. If it isn't we'll generate a new + // one + var vr jwt.ValidationResults + + claims.Validate(&vr) + + if vr.IsBlocking(true) { + // Regenerate the token + err := n.generateJWT(ctx) + if err != nil { + err = fmt.Errorf("error validating JWT: %w", err) + span.SetStatus(codes.Error, err.Error()) + return "", err + } + } + + span.SetStatus(codes.Ok, "Completed") + return n.jwt, nil +} + +func (n *natsTokenClient) Sign(in []byte) ([]byte, error) { + if n.keys == nil { + err := n.generateKeys() + if err != nil { + return []byte{}, err + } + } + + return n.keys.Sign(in) +} + +// An OAuth2 token source which uses an Overmind API token as a source for OAuth +// tokens +type APIKeyTokenSource struct { + // The API Key to use to authenticate to the Overmind API + ApiKey string + token *oauth2.Token + apiKeyClient sdpconnect.ApiKeyServiceClient +} + +func NewAPIKeyTokenSource(apiKey string, overmindAPIURL string) *APIKeyTokenSource { + httpClient := http.Client{ + Timeout: 10 * time.Second, + Transport: otelhttp.NewTransport(http.DefaultTransport), + } + + // Create a client that exchanges the API key for a JWT + apiKeyClient := sdpconnect.NewApiKeyServiceClient(&httpClient, overmindAPIURL) + + return &APIKeyTokenSource{ + ApiKey: apiKey, + apiKeyClient: apiKeyClient, + } +} + +// Exchange an API key for an OAuth token +func (ats *APIKeyTokenSource) Token() (*oauth2.Token, error) { + if ats.token != nil { + // If we already have a token, and it is valid, return it + if ats.token.Valid() { + return ats.token, nil + } + } + + // Get a new token + 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) + } + + if res.Msg.GetAccessToken() == "" { + return nil, errors.New("no access token returned") + } + + // 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) + } + + claims := josejwt.Claims{} + + err = token.UnsafeClaimsWithoutVerification(&claims) + if err != nil { + return nil, fmt.Errorf("error parsing JWT claims: %w", err) + } + + ats.token = &oauth2.Token{ + AccessToken: res.Msg.GetAccessToken(), + TokenType: "Bearer", + Expiry: claims.Expiry.Time(), + } + + return ats.token, nil +} + +// NewAPIKeyClient Creates a new token client that authenticates to Overmind +// using an API key. This is exchanged for an OAuth token, which is then used to +// get a NATS token. +// +// The provided `overmindAPIURL` parameter should be the root URL of the +// Overmind API, without the /api suffix e.g. https://api.app.overmind.tech +func NewAPIKeyClient(overmindAPIURL string, apiKey string) (*natsTokenClient, error) { + // Create a token source that exchanges the API key for an OAuth token + tokenSource := NewAPIKeyTokenSource(apiKey, overmindAPIURL) + transport := oauth2.Transport{ + Source: tokenSource, + Base: http.DefaultTransport, + } + httpClient := http.Client{ + Transport: otelhttp.NewTransport(&transport), + } + + return &natsTokenClient{ + adminClient: sdpconnect.NewAdminServiceClient(&httpClient, overmindAPIURL), + mgmtClient: sdpconnect.NewManagementServiceClient(&httpClient, overmindAPIURL), + }, nil +} + +// NewStaticTokenClient Creates a new token client that uses a static token +// The user must pass the Overmind API URL to configure the client to connect +// to, the raw JWT OAuth access token, and the type of token. This is almost +// always "Bearer" +func NewStaticTokenClient(overmindAPIURL, token, tokenType string) (*natsTokenClient, error) { + transport := oauth2.Transport{ + Source: oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token, + TokenType: tokenType, + }), + } + + httpClient := http.Client{ + Transport: otelhttp.NewTransport(&transport), + } + + return &natsTokenClient{ + adminClient: sdpconnect.NewAdminServiceClient(&httpClient, overmindAPIURL), + mgmtClient: sdpconnect.NewManagementServiceClient(&httpClient, overmindAPIURL), + }, nil +} + +// NewOAuthTokenClient creates a token client that uses the provided TokenSource +// to get a NATS token. `overmindAPIURL` is the root URL of the NATS token +// exchange API that will be used e.g. https://api.server.test/v1 +// +// Tokens will be minted under the specified account as long as the client has +// admin permissions, if not, the account that is attached to the client via +// Auth0 metadata will be used +func NewOAuthTokenClient(overmindAPIURL string, account string, ts oauth2.TokenSource) *natsTokenClient { + return NewOAuthTokenClientWithContext(context.Background(), overmindAPIURL, account, ts) +} + +// NewOAuthTokenClientWithContext creates a token client that uses the provided +// TokenSource to get a NATS token. `overmindAPIURL` is the root URL of the NATS +// token exchange API that will be used e.g. https://api.server.test/v1 +// +// Tokens will be minted under the specified account as long as the client has +// admin permissions, if not, the account that is attached to the client via +// Auth0 metadata will be used +// +// The provided context is used for cancellation and to lookup the HTTP client +// used by oauth2. See the oauth2.HTTPClient variable. +// +// Provide an account name and an admin token to create a token client for a +// foreign account. +func NewOAuthTokenClientWithContext(ctx context.Context, overmindAPIURL string, account string, ts oauth2.TokenSource) *natsTokenClient { + authenticatedClient := oauth2.NewClient(ctx, ts) + + // backwards compatibility: remove previously existing "/api" suffix from URL for connect + apiUrl, err := url.Parse(overmindAPIURL) + if err == nil { + apiUrl.Path = "" + overmindAPIURL = apiUrl.String() + } + + return &natsTokenClient{ + Account: account, + adminClient: sdpconnect.NewAdminServiceClient(authenticatedClient, overmindAPIURL), + mgmtClient: sdpconnect.NewManagementServiceClient(authenticatedClient, overmindAPIURL), + } +} diff --git a/go/auth/auth_client.go b/go/auth/auth_client.go new file mode 100644 index 00000000..f5be155f --- /dev/null +++ b/go/auth/auth_client.go @@ -0,0 +1,124 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" +) + +// AuthenticatedClient is a http.Client that will automatically add the required +// Authorization header to the request, which is taken from the context that it +// is created with. We also always set the X-overmind-interactive header to +// false to connect opentelemetry traces. +type AuthenticatedTransport struct { + from http.RoundTripper + token string +} + +// RoundTrip Adds the Authorization header to the request then call the +// underlying roundTripper +func (y *AuthenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // ask for otel trace linkup + req.Header.Set("X-Overmind-Interactive", "false") + + if y.token != "" { + bearer := fmt.Sprintf("Bearer %v", y.token) + req.Header.Set("Authorization", bearer) + } + + return y.from.RoundTrip(req) +} + +// NewAuthenticatedClient creates a new AuthenticatedClient from the given +// context and http.Client. +func NewAuthenticatedClient(ctx context.Context, from *http.Client) *http.Client { + token, ok := ctx.Value(UserTokenContextKey{}).(string) + if !ok { + token = "" + } + + return &http.Client{ + Transport: &AuthenticatedTransport{ + from: from.Transport, + token: token, + }, + CheckRedirect: from.CheckRedirect, + Jar: from.Jar, + Timeout: from.Timeout, + } +} + +// AuthenticatedAdminClient Returns a bookmark client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedAdminClient(ctx context.Context, apiUrl string) sdpconnect.AdminServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind admin API (pre-authenticated)") + return sdpconnect.NewAdminServiceClient(httpClient, apiUrl) +} + +// AuthenticatedApiKeyClient Returns an apikey client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedApiKeyClient(ctx context.Context, apiUrl string) sdpconnect.ApiKeyServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind apikeys API (pre-authenticated)") + return sdpconnect.NewApiKeyServiceClient(httpClient, apiUrl) +} + +// UnauthenticatedApiKeyClient Returns an apikey client with otel instrumentation +// but no authentication. Can only be used for ExchangeKeyForToken +func UnauthenticatedApiKeyClient(ctx context.Context, apiUrl string) sdpconnect.ApiKeyServiceClient { + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind apikeys API") + return sdpconnect.NewApiKeyServiceClient(tracing.HTTPClient(), apiUrl) +} + +// AuthenticatedBookmarkClient Returns a bookmark client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedBookmarkClient(ctx context.Context, apiUrl string) sdpconnect.BookmarksServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind bookmark API (pre-authenticated)") + return sdpconnect.NewBookmarksServiceClient(httpClient, apiUrl) +} + +// AuthenticatedChangesClient Returns a bookmark client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedChangesClient(ctx context.Context, apiUrl string) sdpconnect.ChangesServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind changes API (pre-authenticated)") + return sdpconnect.NewChangesServiceClient(httpClient, apiUrl) +} + +// AuthenticatedConfigurationClient Returns a bookmark client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedConfigurationClient(ctx context.Context, apiUrl string) sdpconnect.ConfigurationServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind configuration API (pre-authenticated)") + return sdpconnect.NewConfigurationServiceClient(httpClient, apiUrl) +} + +// AuthenticatedManagementClient Returns a bookmark client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedManagementClient(ctx context.Context, apiUrl string) sdpconnect.ManagementServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind management API (pre-authenticated)") + return sdpconnect.NewManagementServiceClient(httpClient, apiUrl) +} + +// AuthenticatedSnapshotsClient Returns a Snapshots client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedSnapshotsClient(ctx context.Context, apiUrl string) sdpconnect.SnapshotsServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind snapshot API (pre-authenticated)") + return sdpconnect.NewSnapshotsServiceClient(httpClient, apiUrl) +} + +// AuthenticatedInviteClient Returns a Invite client that uses the auth +// embedded in the context and otel instrumentation +func AuthenticatedInviteClient(ctx context.Context, apiUrl string) sdpconnect.InviteServiceClient { + httpClient := NewAuthenticatedClient(ctx, tracing.HTTPClient()) + log.WithContext(ctx).WithField("apiUrl", apiUrl).Debug("Connecting to overmind invite API (pre-authenticated)") + return sdpconnect.NewInviteServiceClient(httpClient, apiUrl) +} diff --git a/go/auth/auth_test.go b/go/auth/auth_test.go new file mode 100644 index 00000000..f8d85bae --- /dev/null +++ b/go/auth/auth_test.go @@ -0,0 +1,201 @@ +package auth + +import ( + "context" + "fmt" + "net" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/nats-io/nkeys" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" +) + +var tokenExchangeURLs = []string{ + "http://api-server:8080", + "http://localhost:8080", +} + +func TestBasicTokenClient(t *testing.T) { + var c TokenClient + + keys, err := nkeys.CreateUser() + + if err != nil { + t.Fatal(err) + } + + c = NewBasicTokenClient("tokeny_mc_tokenface", keys) + + var token string + + token, err = c.GetJWT() + + if err != nil { + t.Error(err) + } + + if token != "tokeny_mc_tokenface" { + t.Error("token mismatch") + } + + data := []byte{1, 156, 230, 4, 23, 175, 11} + + signed, err := c.Sign(data) + + if err != nil { + t.Fatal(err) + } + + err = keys.Verify(data, signed) + + if err != nil { + t.Error(err) + } +} + +func GetTestOAuthTokenClient(t *testing.T) *natsTokenClient { + var domain string + var clientID string + var clientSecret string + var exists bool + + errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/go/auth0-test-data" + + // Read secrets form the environment + if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_DOMAIN") + t.Skip("Skipping due to missing environment setup") + } + + if clientID, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_ID"); !exists || clientID == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_ID") + t.Skip("Skipping due to missing environment setup") + } + + if clientSecret, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_SECRET"); !exists || clientSecret == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_SECRET") + t.Skip("Skipping due to missing environment setup") + } + + exchangeURL, err := GetWorkingTokenExchange() + + if err != nil { + t.Fatal(err) + } + + flowConfig := ClientCredentialsConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + } + + return NewOAuthTokenClient( + exchangeURL, + "overmind-development", + flowConfig.TokenSource(t.Context(), fmt.Sprintf("https://%v/oauth/token", domain), os.Getenv("API_SERVER_AUDIENCE")), + ) +} + +func TestOAuthTokenClient(t *testing.T) { + if os.Getenv("CI") == "true" { + t.Skip("Skipping test in CI environment, missing nats token exchange server") + } + + c := GetTestOAuthTokenClient(t) + + var err error + + _, err = c.GetJWT() + + if err != nil { + t.Error(err) + } + + // Make sure it can sign + data := []byte{1, 156, 230, 4, 23, 175, 11} + + _, err = c.Sign(data) + + if err != nil { + t.Fatal(err) + } + +} + +type testAPIKeyHandler struct { + sdpconnect.UnimplementedApiKeyServiceHandler +} + +// Always return a valid token +func (h *testAPIKeyHandler) ExchangeKeyForToken(ctx context.Context, req *connect.Request[sdp.ExchangeKeyForTokenRequest]) (*connect.Response[sdp.ExchangeKeyForTokenResponse], error) { + return &connect.Response[sdp.ExchangeKeyForTokenResponse]{ + Msg: &sdp.ExchangeKeyForTokenResponse{ + AccessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjM5MDIyfQ.Tt0D8zOO3uzfbR1VLc3v7S1_jNrP9_crU1Gi_LpVinEXn4hndTWnI9rMd9r9D0iiv6U-CZAb9JKlun58MO3Pbf_S7apiLGHGE11coIMdk5OKuQFepwXPEk4ixs8_51wmWtJAKg7L5JJG6NuLGnGK8a53hzSHjoK80ROBqlsE9dJ4lpgigj8ZcL-xWpjS4TnUiGLHOvNDnHdqP5D_3DA1teWk9PNh9uU6Wn3U3ShH9rRCI9mKz9amdZ7QzH44J5Gsh2-uo0m2BtZILBE5_p-BeJ7op2RicEXbm69Vae8SPjkJLorBQxbO2lMG4y00q1n-wRDfg_eLFH8ZVC-5lpVXIw", + }, + }, nil +} + +func TestNewAPIKeyTokenSource(t *testing.T) { + _, handler := sdpconnect.NewApiKeyServiceHandler(&testAPIKeyHandler{}) + + testServer := httptest.NewServer(handler) + defer testServer.Close() + + ts := NewAPIKeyTokenSource("test", testServer.URL) + + token, err := ts.Token() + + if err != nil { + t.Fatal(err) + } + + // Make sure the expiry is correct + if token.Expiry.Unix() != 1516239022 { + t.Errorf("token expiry incorrect. Expected 1516239022, got %v", token.Expiry.Unix()) + } +} + +func GetWorkingTokenExchange() (string, error) { + errMap := make(map[string]error) + + for _, url := range tokenExchangeURLs { + var err error + if err = testURL(url); err == nil { + return url, nil + } + errMap[url] = err + } + + var errString string + + for url, err := range errMap { + errString = errString + fmt.Sprintf(" %v: %v\n", url, err.Error()) + } + + return "", fmt.Errorf("no working token exchanges found:\n%v", errString) +} + +func testURL(testURL string) error { + url, err := url.Parse(testURL) + + if err != nil { + return fmt.Errorf("could not parse NATS URL: %v. Error: %w", testURL, err) + } + + dialer := &net.Dialer{ + Timeout: time.Second, + } + conn, err := dialer.DialContext(context.Background(), "tcp", net.JoinHostPort(url.Hostname(), url.Port())) + + if err == nil { + conn.Close() + return nil + } + + return err +} diff --git a/go/auth/gcpauth.go b/go/auth/gcpauth.go new file mode 100644 index 00000000..6cf24ba1 --- /dev/null +++ b/go/auth/gcpauth.go @@ -0,0 +1,58 @@ +// This file is adapted from https://gist.github.com/ahmetb/548059cdbf12fb571e4e2f1e29c48997 + +package auth + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "k8s.io/client-go/rest" +) + +var ( + googleScopes = []string{ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email"} +) + +const ( + GoogleAuthPlugin = "custom_gcp" // so that this is different than "gcp" that's already in client-go tree. +) + +func init() { + if err := rest.RegisterAuthProviderPlugin(GoogleAuthPlugin, newGoogleAuthProvider); err != nil { + log.Fatalf("Failed to register %s auth plugin: %v", GoogleAuthPlugin, err) + } +} + +var _ rest.AuthProvider = &googleAuthProvider{} + +type googleAuthProvider struct { + tokenSource oauth2.TokenSource +} + +func (g *googleAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { + return &oauth2.Transport{ + Base: rt, + Source: g.tokenSource, + } +} +func (g *googleAuthProvider) Login() error { return nil } + +func newGoogleAuthProvider(addr string, config map[string]string, persister rest.AuthProviderConfigPersister) (rest.AuthProvider, error) { + scopes := googleScopes + scopesCfg, found := config["scopes"] + if found { + scopes = strings.Split(scopesCfg, " ") + } + ts, err := google.DefaultTokenSource(context.Background(), scopes...) + if err != nil { + return nil, fmt.Errorf("failed to create google token source: %w", err) + } + return &googleAuthProvider{tokenSource: ts}, nil +} diff --git a/go/auth/middleware.go b/go/auth/middleware.go new file mode 100644 index 00000000..5a806c55 --- /dev/null +++ b/go/auth/middleware.go @@ -0,0 +1,499 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/auth0/go-jwt-middleware/v2/jwks" + "github.com/auth0/go-jwt-middleware/v2/validator" + "github.com/getsentry/sentry-go" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// ScopeCheckBypassedContextKey is a key that is stored in the request context +// when scope checking is actively being bypassed, e.g. in development. When +// this is set the `HasScopes()` function will always return true, and can be +// set using the `WithBypassScopeCheck()` middleware. +type ScopeCheckBypassedContextKey struct{} + +// CustomClaimsContextKey is the key that is used to store the custom claims +// from the JWT +type CustomClaimsContextKey struct{} + +// AccountNameContextKey is the key that is used to store the currently acting +// account name +type AccountNameContextKey struct{} + +// UserTokenContextKey is the key that is used to store the full JWT token of the user +type UserTokenContextKey struct{} + +// CurrentSubjectContextKey is the key that is used to store the current subject attribute. +// This will be the auth0 `user_id` from the tokens `sub` claim. +type CurrentSubjectContextKey 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 + // checked in order with the first one that is found being used + AuthCookieNames []string + + // Use this to specify the full issuer URL for validating the JWTs. This + // should only be used if we aren't using Auth0 as a source for tokens (such + // as in testing). Auth0Domain will take precedence if both are set. + IssuerURL string + + // Bypasses all auth checks, meaning that HasScopes() will always return + // true. This should be used in conjunction with the `AccountOverride` field + // since there won't be a token to parse the account from + BypassAuth bool + + // Bypasses auth for the given paths. This is a regular expression that is + // matched against the path of the request. If the regex matches then the + // request will be allowed through without auth. This should be used with + // `AccountOverride` in order to avoid the required context values not being + // set and therefore causing issues (probably nil pointer panics) + BypassAuthForPaths *regexp.Regexp + + // Overrides the account name stored in the CustomClaimsContextKey + AccountOverride *string + + // Overrides the scope stored in the CustomClaimsContextKey + ScopeOverride *string +} + +// HasScopes compatibility alias for HasAllScopes +func HasScopes(ctx context.Context, requiredScopes ...string) bool { + return HasAllScopes(ctx, requiredScopes...) +} + +// HasAllScopes checks that the authenticated user in the request context has all the +// required scopes. If auth has been bypassed, this will always return true +func HasAllScopes(ctx context.Context, requiredScopes ...string) bool { + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.StringSlice("ovm.auth.requiredScopes.all", requiredScopes), + ) + + if ctx.Value(ScopeCheckBypassedContextKey{}) == true { + // this is always set when auth is bypassed + // set it here again to capture non-standard auth configs + span.SetAttributes(attribute.Bool("ovm.auth.bypass", true)) + + // Bypass all auth + return true + } + + claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) + if !ok { + span.SetAttributes(attribute.String("ovm.auth.missingClaims", "all")) + return false + } + + for _, scope := range requiredScopes { + if !claims.HasScope(scope) { + span.SetAttributes(attribute.String("ovm.auth.missingClaims", scope)) + return false + } + } + return true +} + +// HasAnyScopes checks that the authenticated user in the request context has any of the +// required scopes. If auth has been bypassed, this will always return true +func HasAnyScopes(ctx context.Context, requiredScopes ...string) bool { + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.StringSlice("ovm.auth.requiredScopes.any", requiredScopes), + ) + + if ctx.Value(ScopeCheckBypassedContextKey{}) == true { + // this is always set when auth is bypassed + // set it here again to capture non-standard auth configs + span.SetAttributes(attribute.Bool("ovm.auth.bypass", true)) + + // Bypass all auth + return true + } + + claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) + if !ok { + span.SetAttributes(attribute.String("ovm.auth.missingClaims", "all")) + return false + } + + span.SetAttributes( + attribute.String("ovm.auth.tokenScopes", claims.Scope), + ) + + for _, scope := range requiredScopes { + if claims.HasScope(scope) { + span.SetAttributes(attribute.String("ovm.auth.usedClaim", scope)) + return true + } + } + return false +} + +var ErrNoClaims = errors.New("error extracting claims from token") + +// ExtractAccount Extracts the account name from a context +func ExtractAccount(ctx context.Context) (string, error) { + claims := ctx.Value(CustomClaimsContextKey{}) + + if claims == nil { + return "", ErrNoClaims + } + + return claims.(*CustomClaims).AccountName, nil +} + +// NewAuthMiddleware Creates new auth middleware. The options allow you to +// bypass the authentication process or not, but either way this middleware will +// set the `CustomClaimsContextKey` in the request context which allows you to +// use the `HasScopes()` function to check the scopes without having to worry +// about whether the server is using auth or not. +// +// If auth is not bypassed, then tokens will be validated using Auth0 and +// 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 MiddlewareConfig, next http.Handler) http.Handler { + processOverrides := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + options := []OverrideAuthOptionFunc{} + + if config.ScopeOverride != nil { + options = append(options, WithScope(*config.ScopeOverride)) + } + + if config.AccountOverride != nil { + options = append(options, WithAccount(*config.AccountOverride)) + } + + ctx := r.Context() + if len(options) > 0 { + ctx = OverrideAuth(r.Context(), options...) + } + + r = r.Clone(ctx) + + next.ServeHTTP(w, r) + }) + + return ensureValidTokenHandler(config, processOverrides) +} + +type OverrideAuthOptionFunc func(ctx context.Context) context.Context + +// Sets the scope in the context to the given value. This should be the value +// that would be embedded directly in the token, with each scope being separated +// by a space. +func WithScope(scope string) OverrideAuthOptionFunc { + return withCustomClaims(func(claims *CustomClaims) { + claims.Scope = scope + }) +} + +// Sets the account in the context to the given value. +func WithAccount(account string) OverrideAuthOptionFunc { + return withCustomClaims(func(claims *CustomClaims) { + claims.AccountName = account + }) +} + +// Sets the auth info in the context directly from the validated claims produced +// by the `github.com/auth0/go-jwt-middleware/v2/validator` package. This is +// essentially what the middleware already does when receiving a request, and +// therefore should only be used in exceptional circumstances, like testing, when the +// middleware is not being used. +// +// If this is being used, there is no need to use the `WithScope` or `WithAccount` +// options as the claims will be extracted directly from the validated claims. +func WithValidatedClaims(claims *validator.ValidatedClaims) OverrideAuthOptionFunc { + return func(ctx context.Context) context.Context { + customClaims := claims.CustomClaims.(*CustomClaims) + ctx = context.WithValue(ctx, jwtmiddleware.ContextKey{}, claims) + ctx = context.WithValue(ctx, CustomClaimsContextKey{}, customClaims) + ctx = context.WithValue(ctx, CurrentSubjectContextKey{}, claims.RegisteredClaims.Subject) + ctx = context.WithValue(ctx, AccountNameContextKey{}, customClaims.AccountName) + return ctx + } +} + +// Bypasses the scope check, meaning that `HasScopes()` and `HasAllScopes` will +// always return true. This is useful for testing. +func WithBypassScopeCheck() OverrideAuthOptionFunc { + return func(ctx context.Context) context.Context { + return context.WithValue(ctx, ScopeCheckBypassedContextKey{}, true) + } +} + +// Overrides the authentication that is currently stored in the context. This +// can only be used within a single process, and doesn't mean that the overrides +// set here will be passed on if you are using `NewAuthenticatedClient` to pass +// through auth. It is however useful for testing, or for calling other handlers +// within the same process. +func OverrideAuth(ctx context.Context, opts ...OverrideAuthOptionFunc) context.Context { + for _, opt := range opts { + ctx = opt(ctx) + } + return ctx +} + +func withCustomClaims(modify func(*CustomClaims)) OverrideAuthOptionFunc { + return func(ctx context.Context) context.Context { + i := ctx.Value(CustomClaimsContextKey{}) + var claims *CustomClaims + var newClaims CustomClaims + var ok bool + + if claims, ok = i.(*CustomClaims); ok { + // clone out the values to avoid sharing + newClaims = *claims + } + + modify(&newClaims) + + // Store the new claims in the context + ctx = context.WithValue(ctx, CustomClaimsContextKey{}, &newClaims) + ctx = context.WithValue(ctx, AccountNameContextKey{}, newClaims.AccountName) + + return ctx + } +} + +// ensureValidTokenHandler is a middleware that will check the validity of our +// JWT. +// +// This will fail if all of Auth0Domain, Auth0Audience and AuthCookieName are +// empty. +// +// This middleware also extract custom claims form the token and stores them in +// CustomClaimsContextKey +func ensureValidTokenHandler(config MiddlewareConfig, next http.Handler) http.Handler { + if config.Auth0Domain == "" && config.IssuerURL == "" && config.Auth0Audience == "" { + log.Fatalf("Auth0 configuration is missing") + } + + var issuerURL *url.URL + var err error + + if config.Auth0Domain != "" { + issuerURL, err = url.Parse("https://" + config.Auth0Domain + "/") + } else { + issuerURL, err = url.Parse(config.IssuerURL) + } + if err != nil { + log.Fatalf("Failed to parse the issuer url: %v", err) + } + + provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute) + + jwtValidator, err := validator.New( + provider.KeyFunc, + validator.RS256, + issuerURL.String(), + []string{config.Auth0Audience}, + validator.WithCustomClaims( + func() validator.CustomClaims { + return &CustomClaims{} + }, + ), + validator.WithAllowedClockSkew(time.Minute), + ) + if err != nil { + log.Fatalf("Failed to set up the jwt validator") + } + + errorHandler := func(w http.ResponseWriter, r *http.Request, err error) { + // copied from auth0's DefaultErrorHandler, but with some extra logging and reporting + span := trace.SpanFromContext(r.Context()) + span.SetAttributes( + attribute.String("ovm.auth.error", err.Error()), + attribute.String("ovm.auth.audience", config.Auth0Audience), + attribute.String("ovm.auth.domain", config.Auth0Domain), + attribute.String("ovm.auth.expectedIssuer", issuerURL.String()), + ) + + // Check if this is a Connect/gRPC request by looking at the Content-Type header + // Connect requests use content types like: + // - application/connect+proto + // - application/connect+json + // - application/grpc (base type without suffix) + // - application/grpc+proto + // - application/grpc+json + // For these requests, we should not set Content-Type: application/json + // as it will cause content-type mismatch errors on the client side + contentType := r.Header.Get("Content-Type") + isConnectRequest := strings.HasPrefix(contentType, "application/connect+") || + strings.HasPrefix(contentType, "application/grpc") + + // Only set JSON content-type for non-Connect requests + if !isConnectRequest { + w.Header().Set("Content-Type", "application/json") + } + + switch { + case errors.Is(err, jwtmiddleware.ErrJWTMissing): + // since connectrpc would translate the original `BadRequest` to a + // `CodeInternal` instead of something sensible, we also need to + // return StatusUnauthorized here, to provide the correct status + // code to the client. + w.WriteHeader(http.StatusUnauthorized) + if !isConnectRequest { + _, _ = w.Write([]byte(`{"message":"JWT is missing."}`)) + } + case errors.Is(err, jwtmiddleware.ErrJWTInvalid): + w.WriteHeader(http.StatusUnauthorized) + if !isConnectRequest { + _, _ = w.Write([]byte(`{"message":"JWT is invalid."}`)) + } + default: + span.SetStatus(codes.Error, "Something went wrong while checking the JWT") + sentry.CaptureException(err) + + w.WriteHeader(http.StatusInternalServerError) + if !isConnectRequest { + _, _ = w.Write([]byte(`{"message":"Something went wrong while checking the JWT."}`)) + } + } + } + + // Set up token extractors based on what env vars are available + extractors := []jwtmiddleware.TokenExtractor{ + jwtmiddleware.AuthHeaderTokenExtractor, + } + + for _, cookieName := range config.AuthCookieNames { + extractors = append(extractors, jwtmiddleware.CookieTokenExtractor(cookieName)) + } + + tokenExtractor := jwtmiddleware.MultiTokenExtractor(extractors...) + + middleware := jwtmiddleware.New( + jwtValidator.ValidateToken, + jwtmiddleware.WithErrorHandler(errorHandler), + jwtmiddleware.WithTokenExtractor(tokenExtractor), + ) + + jwtValidationMiddleware := middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // extract account name and setup otel attributes after the JWT was validated, but before the actual handler runs + claims := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims) + + token, err := tokenExtractor(r) + // we should never hit this as the middleware wouldn't call the handler + if err != nil { + // This is not ErrJWTMissing because an error here means that the + // tokenExtractor had an error and _not_ that the token was missing. + errorHandler(w, r, fmt.Errorf("error extracting token: %w", err)) + return + } + + customClaims := claims.CustomClaims.(*CustomClaims) + if customClaims == nil { + errorHandler(w, r, fmt.Errorf("couldn't get claims from: %v", claims)) + return + } + + ctx := r.Context() + + // note that the values are looked up in last-in-first-out order, so + // there is an absolutely minor perf optimisation to have the context + // values set in ascending order of access frequency. + ctx = context.WithValue(ctx, UserTokenContextKey{}, token) + ctx = context.WithValue(ctx, CustomClaimsContextKey{}, customClaims) + ctx = context.WithValue(ctx, CurrentSubjectContextKey{}, claims.RegisteredClaims.Subject) + ctx = context.WithValue(ctx, AccountNameContextKey{}, customClaims.AccountName) + + trace.SpanFromContext(ctx).SetAttributes( + attribute.String("ovm.auth.accountName", customClaims.AccountName), + attribute.Int64("ovm.auth.expiry", claims.RegisteredClaims.Expiry), + attribute.String("ovm.auth.scopes", customClaims.Scope), + // subject is the auth0 client id or user id + attribute.String("ovm.auth.subject", claims.RegisteredClaims.Subject), + ) + + // if its a service impersonating an account, we should mark it as impersonation + if strings.HasSuffix(claims.RegisteredClaims.Subject, "@clients") { + trace.SpanFromContext(ctx).SetAttributes( + attribute.Bool("ovm.auth.impersonation", true), + ) + } + + r = r.Clone(ctx) + + next.ServeHTTP(w, r) + })) + + // Basically what I need to do here is I need to have a middleware that + // checks for bypassing, then passes on to middleware.checkJWT. + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + span := trace.SpanFromContext(ctx) + + var shouldBypass bool + + // If config.BypassAuth is true then bypass + if config.BypassAuth { + shouldBypass = true + } + + // If we aren't bypassing always and we have a regex then check if we + // should bypass + if !shouldBypass && config.BypassAuthForPaths != nil { + shouldBypass = config.BypassAuthForPaths.MatchString(r.URL.Path) + if shouldBypass { + span.SetAttributes(attribute.String("ovm.auth.bypassedPath", r.URL.Path)) + } + } + + span.SetAttributes(attribute.Bool("ovm.auth.bypass", shouldBypass)) + + if shouldBypass { + ctx = OverrideAuth(ctx, WithBypassScopeCheck()) + + r = r.Clone(ctx) + + // Call the next handler without adding any JWT validation + next.ServeHTTP(w, r) + } else { + // Otherwise we need to inject the JWT validation middleware + jwtValidationMiddleware.ServeHTTP(w, r) + } + }) +} + +// CustomClaims contains custom data we want from the token. +type CustomClaims struct { + Scope string `json:"scope"` + AccountName string `json:"https://api.overmind.tech/account-name"` +} + +// HasScope checks whether our claims have a specific scope. +func (c CustomClaims) HasScope(expectedScope string) bool { + result := strings.Split(c.Scope, " ") + for i := range result { + if result[i] == expectedScope { + return true + } + } + + return false +} + +// Validate does nothing for this example, but we need +// it to satisfy validator.CustomClaims interface. +func (c CustomClaims) Validate(ctx context.Context) error { + return nil +} diff --git a/go/auth/middleware_test.go b/go/auth/middleware_test.go new file mode 100644 index 00000000..0e18fd26 --- /dev/null +++ b/go/auth/middleware_test.go @@ -0,0 +1,792 @@ +package auth + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + "github.com/auth0/go-jwt-middleware/v2/validator" + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" + log "github.com/sirupsen/logrus" +) + +func TestHasScopes(t *testing.T) { + t.Run("with auth bypassed", func(t *testing.T) { + t.Parallel() + + ctx := OverrideAuth(context.Background(), WithBypassScopeCheck()) + + pass := HasAllScopes(ctx, "test") + + if !pass { + t.Error("expected to allow since auth is bypassed") + } + }) + + t.Run("with good scopes", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAllScopes(ctx, "test") + + if !pass { + t.Error("expected to allow since `test` scope is present") + } + }) + + t.Run("with multiple good scopes", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAllScopes(ctx, "test", "foo") + + if !pass { + t.Error("expected to allow since `test` scope is present") + } + }) + + t.Run("with bad scopes", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAllScopes(ctx, "baz") + + if pass { + t.Error("expected to deny since `baz` scope is not present") + } + }) + + t.Run("with one scope missing", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAllScopes(ctx, "test", "baz") + + if pass { + t.Error("expected to deny since `baz` scope is not present") + } + }) + + t.Run("with any scopes", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAnyScopes(ctx, "fail", "foo") + + if !pass { + t.Error("expected to allow since `foo` scope is present") + } + }) + + t.Run("without any scopes", func(t *testing.T) { + t.Parallel() + + account := "foo" + scope := "test foo bar" + ctx := OverrideAuth(context.Background(), WithScope(scope), WithAccount(account)) + + pass := HasAnyScopes(ctx, "fail", "fail harder") + + if pass { + t.Error("expected to deny since no matching scope is present") + } + }) +} + +func TestNewAuthMiddleware(t *testing.T) { + server, err := NewTestJWTServer() + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + jwksURL := server.Start(ctx) + + defaultConfig := MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + } + + bypassHealthConfig := MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + BypassAuthForPaths: regexp.MustCompile("/health"), + } + + correctAccount := "test" + correctScope := "test:pass" + + tests := []struct { + Name string + TokenOptions *TestTokenOptions + ExpectedCode int + AuthConfig MiddlewareConfig + Path string + }{ + { + Name: "with expired token", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(-time.Hour), + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusUnauthorized, + }, + { + Name: "with wrong audience", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://something.not.expected"}, + Expiry: time.Now().Add(time.Hour), + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusUnauthorized, + }, + { + Name: "with insufficient scopes", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:fail", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusUnauthorized, + }, + { + Name: "with correct scopes but wrong account", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "fail", + Scope: "test:pass", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusUnauthorized, + }, + { + Name: "with correct scopes and account", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusOK, + }, + { + Name: "with the correct scope and many others", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass test:fail foo:bar something", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusOK, + }, + { + Name: "with many audiences and many scopes", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech", "https://api.overmind.tech/other"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass test:other", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusOK, + }, + { + Name: "with many audiences and one scope", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech", "https://api.overmind.tech/other"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass", + }, + }, + AuthConfig: defaultConfig, + ExpectedCode: http.StatusOK, + }, + { + Name: "with good token and some bypassed paths", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass", + }, + }, + AuthConfig: MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + BypassAuthForPaths: regexp.MustCompile("/health"), + }, + ExpectedCode: http.StatusOK, + }, + { + Name: "with no token on a non-bypassed path", + Path: "/", + AuthConfig: bypassHealthConfig, + ExpectedCode: http.StatusUnauthorized, + }, + { + Name: "with no token on a bypassed path", + Path: "/health", + AuthConfig: bypassHealthConfig, + ExpectedCode: http.StatusOK, + }, + { + Name: "with bad token on a non-bypassed path", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:fail", + }, + }, + ExpectedCode: http.StatusUnauthorized, + AuthConfig: bypassHealthConfig, + }, + { + Name: "with bad token on a bypassed path", + Path: "/health", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:fail", + }, + }, + ExpectedCode: http.StatusOK, + AuthConfig: bypassHealthConfig, + }, + { + Name: "with a good token and bypassed auth", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass", + }, + }, + ExpectedCode: http.StatusOK, + AuthConfig: MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + BypassAuth: true, + }, + }, + { + Name: "with a bad token and bypassed auth", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(-time.Hour), // expired + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:pass", + }, + }, + ExpectedCode: http.StatusOK, + AuthConfig: MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + BypassAuth: true, + }, + }, + { + Name: "with account override", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "bad", + Scope: "test:pass", + }, + }, + ExpectedCode: http.StatusOK, + AuthConfig: MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + AccountOverride: &correctAccount, + }, + }, + { + Name: "with scope override", + Path: "/", + TokenOptions: &TestTokenOptions{ + Audience: []string{"https://api.overmind.tech"}, + Expiry: time.Now().Add(time.Hour), + CustomClaims: CustomClaims{ + AccountName: "test", + Scope: "test:fail", + }, + }, + ExpectedCode: http.StatusOK, + AuthConfig: MiddlewareConfig{ + IssuerURL: jwksURL, + Auth0Audience: "https://api.overmind.tech", + ScopeOverride: &correctScope, + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + handler := NewAuthMiddleware(test.AuthConfig, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // This is a test handler that always does the same thing, it checks + // that the account is set to the correct value and that the user has + // the test:pass scope + if !HasAnyScopes(ctx, "test:pass") { + w.WriteHeader(http.StatusUnauthorized) + _, err := w.Write([]byte("missing required scope")) + if err != nil { + t.Error(err) + } + return + } + + if ctx.Value(ScopeCheckBypassedContextKey{}) == true { + // If we are bypassing auth then we don't want to check the account + } else { + claims, ok := ctx.Value(CustomClaimsContextKey{}).(*CustomClaims) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + _, err := fmt.Fprintf(w, "expected *CustomClaims in context, got %T", ctx.Value(CustomClaimsContextKey{})) + if err != nil { + t.Error(err) + } + return + } + + if claims.AccountName != "test" { + w.WriteHeader(http.StatusUnauthorized) + _, err := fmt.Fprintf(w, "expected account to be 'test', but was '%s'", claims.AccountName) + if err != nil { + t.Error(err) + } + return + } + } + })) + + rr := httptest.NewRecorder() + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, test.Path, nil) + if err != nil { + t.Fatal(err) + } + + if test.TokenOptions != nil { + // Create a test Token + token, err := server.GenerateJWT(test.TokenOptions) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } + + handler.ServeHTTP(rr, req) + + if rr.Code != test.ExpectedCode { + t.Errorf("expected status code %d, but got %d", test.ExpectedCode, rr.Code) + t.Error(rr.Body.String()) + } + }) + } +} + +func TestOverrideAuth(t *testing.T) { + tests := []struct { + Name string + Options []OverrideAuthOptionFunc + HasAllScopes []string + HasAccountName string + }{ + { + Name: "with account override", + Options: []OverrideAuthOptionFunc{ + WithAccount("test"), + }, + HasAccountName: "test", + }, + { + Name: "with scope override", + Options: []OverrideAuthOptionFunc{ + WithScope("test:pass"), + }, + HasAllScopes: []string{"test:pass"}, + }, + { + Name: "with account and scope override", + Options: []OverrideAuthOptionFunc{ + WithAccount("test"), + WithScope("test:pass"), + }, + HasAccountName: "test", + HasAllScopes: []string{"test:pass"}, + }, + { + Name: "with account and scope override in reverse order", + Options: []OverrideAuthOptionFunc{ + WithScope("test:pass"), + WithAccount("test"), + }, + HasAccountName: "test", + HasAllScopes: []string{"test:pass"}, + }, + { + Name: "with validated custom claims", + Options: []OverrideAuthOptionFunc{ + WithValidatedClaims(&validator.ValidatedClaims{ + CustomClaims: &CustomClaims{ + Scope: "test:pass", + AccountName: "test", + }, + RegisteredClaims: validator.RegisteredClaims{ + Issuer: "https://api.overmind.tech", + Subject: "test", + Audience: []string{"https://api.overmind.tech"}, + ID: "test", + }, + }), + }, + HasAccountName: "test", + HasAllScopes: []string{"test:pass"}, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + ctx := context.Background() + + ctx = OverrideAuth(ctx, test.Options...) + + if test.HasAccountName != "" { + accountName, err := ExtractAccount(ctx) + if err != nil { + t.Error(err) + } + + if accountName != test.HasAccountName { + t.Errorf("expected account name to be %s, but got %s", test.HasAccountName, accountName) + } + } + + for _, scope := range test.HasAllScopes { + if !HasAllScopes(ctx, scope) { + t.Errorf("expected to have scope %s, but did not", scope) + } + } + }) + } +} + +func BenchmarkAuthMiddleware(b *testing.B) { + config := MiddlewareConfig{ + Auth0Domain: "auth.overmind-demo.com", + Auth0Audience: "https://api.overmind.tech", + } + + okHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + + handler := NewAuthMiddleware(config, http.HandlerFunc(okHandler)) + + // Reduce logging + log.SetLevel(log.FatalLevel) + + for range b.N { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) + + if err != nil { + b.Fatal(err) + } + + // Set to a known bad JWT (this JWT is garbage don't freak out) + req.Header.Set("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InBpQWx1Q1FkQTB4MTNweG1JQzM4dyJ9.eyJodHRwczovL2FwaS5vdmVybWluZC50ZWNoL2FjY291bnQtbmFtZSI6IlRFU1QiLCJpc3MiOiJodHRwczovL29tLWRvZ2Zvb2QuZXUuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfFRFU1QiLCJhdWQiOlsiaHR0cHM6Ly9hcGkuZGYub3Zlcm1pbmQtZGVtby5jb20iLCJodHRwczovL29tLWRvZ2Zvb2QuZXUuYXV0aDAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTcxNDA0MjA5MiwiZXhwIjoxNzE0MTI4NDkyLCJzY29wZSI6Im1hbnkgc2NvcGVzIiwiYXpwIjoiVEVTVCJ9.cEEh8jVnEItZel4SoyPybLUg7sArwduCrmSJHMz3YNRfzpRl9lxry39psuDUHFKdgOoNVxUv3Lgm-JWG-9uddCKYOW_zQxEvQvj6o8tcpQkmBZBlc8huG21dLPz7yrPhogVAcApLjdHf1fqii9EHxQegxch9FHlyfF7Xii5t9Hus62l4vdZ5dVWaIuiOLtcbG_hLxl9yqBf5tzN8eEC-Pa1SoAciRPesqH4AARfKyBFBhN774Fu3NzfNtW3wD_ASvnv7aFwzblS8ff5clqdTr2GuuJKdIPcmjQV2LaGSExHg2riCryf5guAhitAuwhugssW__STQmwp8dJmhifs7DA") + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + b.Errorf("expected status code %d, but got %d", http.StatusUnauthorized, rr.Code) + } + } +} + +// Creates a new server that mints real, signed JWTs for testing. It even +// provides its own JWKS endpoint so they can be externally validated. To start +// the JWKS server you should call .Start() +func NewTestJWTServer() (*TestJWTServer, error) { + // Generate an RSA private key + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + + // Wrap this in a JWK object + jwk := jose.JSONWebKey{ + Key: privateKey, + KeyID: "test-signing-key", + Algorithm: string(jose.RS256), + } + + // Create a signer that will sign all of our tokens + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: jwk, + } + signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) + + if err != nil { + return nil, err + } + + // Export the public key to be used for validation + pubJwk := jwk.Public() + + keySet := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{pubJwk}, + } + + return &TestJWTServer{ + signer: signer, + privateKey: jwk, + publicKey: pubJwk, + publicKeySet: keySet, + }, nil +} + +// This server is used to mint JWTs for testing purposes. It is basically the +// same as Auth0 when it comes to creating tokens in that it returns a JWKS +// endpoint that can be used to validate the tokens it creates, and the tokens +// use the same algorithm as Auth0 +type TestJWTServer struct { + signer jose.Signer + privateKey jose.JSONWebKey + publicKey jose.JSONWebKey + publicKeySet jose.JSONWebKeySet + server *httptest.Server +} + +type TestTokenOptions struct { + Audience []string + Expiry time.Time + + CustomClaims +} + +func (s *TestJWTServer) GenerateJWT(options *TestTokenOptions) (string, error) { + builder := jwt.Signed(s.signer) + + builder = builder.Claims(jwt.Claims{ + Issuer: s.server.URL, + Subject: "test", + Audience: jwt.Audience(options.Audience), + Expiry: jwt.NewNumericDate(options.Expiry), + IssuedAt: jwt.NewNumericDate(time.Now()), + }) + + builder = builder.Claims(options.CustomClaims) + + return builder.Serialize() +} + +// Starts the server in the background, the server will exit when the context is +// cancelled. Returns the URL of the server +func (s *TestJWTServer) Start(ctx context.Context) string { + s.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/openid-configuration": + // The endpoint tells the validating party where to find the JWKS, + // this contains our public keys that can be used to validate tokens + // issued by our server + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, err := fmt.Fprintf(w, `{"jwks_uri": "%s/.well-known/jwks.json"}`, s.server.URL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + case "/.well-known/jwks.json": + // Write the public key set as JSON + w.Header().Set("Content-Type", "application/json") + + b, err := json.MarshalIndent(s.publicKeySet, "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = w.Write(b) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + })) + + go func() { + <-ctx.Done() + s.server.Close() + }() + + return s.server.URL +} + +func TestConnectErrorHandling(t *testing.T) { + // Create a test JWT server + server, err := NewTestJWTServer() + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + jwksURL := server.Start(ctx) + + // Create the middleware + handler := NewAuthMiddleware(MiddlewareConfig{ + Auth0Domain: "", + Auth0Audience: "test", + IssuerURL: jwksURL, + }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + tests := []struct { + Name string + ContentType string + ExpectJSONResponse bool + ExpectContentType string + }{ + { + Name: "Regular JSON request without auth", + ContentType: "application/json", + ExpectJSONResponse: true, + ExpectContentType: "application/json", + }, + { + Name: "Connect proto request without auth", + ContentType: "application/connect+proto", + ExpectJSONResponse: false, + ExpectContentType: "", + }, + { + Name: "Connect json request without auth", + ContentType: "application/connect+json", + ExpectJSONResponse: false, + ExpectContentType: "", + }, + { + Name: "gRPC base request without auth", + ContentType: "application/grpc", + ExpectJSONResponse: false, + ExpectContentType: "", + }, + { + Name: "gRPC proto request without auth", + ContentType: "application/grpc+proto", + ExpectJSONResponse: false, + ExpectContentType: "", + }, + { + Name: "gRPC json request without auth", + ContentType: "application/grpc+json", + ExpectJSONResponse: false, + ExpectContentType: "", + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + rr := httptest.NewRecorder() + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/test", nil) + if err != nil { + t.Fatal(err) + } + + // Set the Content-Type header + req.Header.Set("Content-Type", test.ContentType) + + // Don't set any auth token, so it will fail auth + handler.ServeHTTP(rr, req) + + // Should return 401 Unauthorized + if rr.Code != http.StatusUnauthorized { + t.Errorf("expected status code %d, but got %d", http.StatusUnauthorized, rr.Code) + } + + // Check Content-Type header + contentType := rr.Header().Get("Content-Type") + if test.ExpectContentType != contentType { + t.Errorf("expected Content-Type header to be '%s', but got '%s'", test.ExpectContentType, contentType) + } + + // Check if response has JSON body + hasJSONBody := len(rr.Body.Bytes()) > 0 && contentType == "application/json" + if test.ExpectJSONResponse != hasJSONBody { + t.Errorf("expected JSON response: %v, but got: %v (body length: %d)", test.ExpectJSONResponse, hasJSONBody, len(rr.Body.Bytes())) + } + }) + } +} diff --git a/go/auth/nats.go b/go/auth/nats.go new file mode 100644 index 00000000..40c0aade --- /dev/null +++ b/go/auth/nats.go @@ -0,0 +1,258 @@ +package auth + +import ( + "errors" + "strings" + "time" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + + "github.com/nats-io/nats.go" +) + +// Defaults +const MaxReconnectsDefault = -1 +const ReconnectWaitDefault = 1 * time.Second +const ReconnectJitterDefault = 5 * time.Second +const ConnectionTimeoutDefault = 10 * time.Second + +type MaxRetriesError struct{} + +func (m MaxRetriesError) Error() string { + return "maximum retries reached" +} + +func fieldsFromConn(c *nats.Conn) log.Fields { + fields := log.Fields{} + + if c != nil { + fields["ovm.nats.address"] = c.ConnectedAddr() + fields["ovm.nats.reconnects"] = c.Reconnects + fields["ovm.nats.serverId"] = c.ConnectedServerId() + fields["ovm.nats.url"] = c.ConnectedUrl() + + if c.LastError() != nil { + fields["ovm.nats.lastError"] = c.LastError() + } + } + + return fields +} + +var DisconnectErrHandlerDefault = func(c *nats.Conn, err error) { + fields := fieldsFromConn(c) + + if err != nil { + log.WithError(err).WithFields(fields).Error("NATS disconnected") + } else { + log.WithFields(fields).Debug("NATS disconnected") + } +} + +var ConnectHandlerDefault = func(c *nats.Conn) { + fields := fieldsFromConn(c) + + log.WithFields(fields).Debug("NATS connected") +} +var ReconnectHandlerDefault = func(c *nats.Conn) { + fields := fieldsFromConn(c) + + log.WithFields(fields).Debug("NATS reconnected") +} +var ClosedHandlerDefault = func(c *nats.Conn) { + fields := fieldsFromConn(c) + + log.WithFields(fields).Debug("NATS connection closed") +} +var LameDuckModeHandlerDefault = func(c *nats.Conn) { + fields := fieldsFromConn(c) + + log.WithFields(fields).Debug("NATS server has entered lame duck mode") +} +var ErrorHandlerDefault = func(c *nats.Conn, s *nats.Subscription, err error) { + fields := fieldsFromConn(c) + + if s != nil { + fields["ovm.nats.subject"] = s.Subject + fields["ovm.nats.queue"] = s.Queue + } + + log.WithFields(fields).WithError(err).Error("NATS error") +} + +type NATSOptions struct { + Servers []string // List of server to connect to + ConnectionName string // The client name + MaxReconnects int // The maximum number of reconnect attempts + ConnectionTimeout time.Duration // The timeout for Dial on a connection + ReconnectWait time.Duration // Wait time between reconnect attempts + ReconnectJitter time.Duration // The upper bound of a random delay added ReconnectWait + TokenClient TokenClient // The client to use to get NATS tokens + ConnectHandler nats.ConnHandler // Runs when NATS is connected + DisconnectErrHandler nats.ConnErrHandler // Runs when NATS is disconnected + ReconnectHandler nats.ConnHandler // Runs when NATS has successfully reconnected + ClosedHandler nats.ConnHandler // Runs when NATS will no longer be connected + ErrorHandler nats.ErrHandler // Runs when there is a NATS error + LameDuckModeHandler nats.ConnHandler // Runs when the connection enters "lame duck mode" + AdditionalOptions []nats.Option // Addition options to pass to the connection + NumRetries int // How many times to retry connecting initially, use -1 to retry indefinitely + RetryDelay time.Duration // Delay between connection attempts +} + +// Creates a copy of the NATS options, **excluding** the token client as these +// should not be re-used +func (o NATSOptions) Copy() NATSOptions { + return NATSOptions{ + Servers: o.Servers, + ConnectionName: o.ConnectionName, + MaxReconnects: o.MaxReconnects, + ConnectionTimeout: o.ConnectionTimeout, + ReconnectWait: o.ReconnectWait, + ReconnectJitter: o.ReconnectJitter, + ConnectHandler: o.ConnectHandler, + DisconnectErrHandler: o.DisconnectErrHandler, + ReconnectHandler: o.ReconnectHandler, + ClosedHandler: o.ClosedHandler, + LameDuckModeHandler: o.LameDuckModeHandler, + ErrorHandler: o.ErrorHandler, + AdditionalOptions: o.AdditionalOptions, + NumRetries: o.NumRetries, + RetryDelay: o.RetryDelay, + } +} + +// ToNatsOptions Converts the struct to connection string and a set of NATS +// options +func (o NATSOptions) ToNatsOptions() (string, []nats.Option) { + serverString := strings.Join(o.Servers, ",") + options := []nats.Option{} + + if o.ConnectionName != "" { + options = append(options, nats.Name(o.ConnectionName)) + } + + if o.MaxReconnects != 0 { + options = append(options, nats.MaxReconnects(o.MaxReconnects)) + } else { + options = append(options, nats.MaxReconnects(MaxReconnectsDefault)) + } + + if o.ConnectionTimeout != 0 { + options = append(options, nats.Timeout(o.ConnectionTimeout)) + } else { + options = append(options, nats.Timeout(ConnectionTimeoutDefault)) + } + + if o.ReconnectWait != 0 { + options = append(options, nats.ReconnectWait(o.ReconnectWait)) + } else { + options = append(options, nats.ReconnectWait(ReconnectWaitDefault)) + } + + if o.ReconnectJitter != 0 { + options = append(options, nats.ReconnectJitter(o.ReconnectJitter, o.ReconnectJitter)) + } else { + options = append(options, nats.ReconnectJitter(ReconnectJitterDefault, ReconnectJitterDefault)) + } + + if o.TokenClient != nil { + options = append(options, nats.UserJWT(func() (string, error) { + return o.TokenClient.GetJWT() + }, o.TokenClient.Sign)) + } + + if o.ConnectHandler != nil { + options = append(options, nats.ConnectHandler(o.ConnectHandler)) + } else { + options = append(options, nats.ConnectHandler(ConnectHandlerDefault)) + } + + if o.DisconnectErrHandler != nil { + options = append(options, nats.DisconnectErrHandler(o.DisconnectErrHandler)) + } else { + options = append(options, nats.DisconnectErrHandler(DisconnectErrHandlerDefault)) + } + + if o.ReconnectHandler != nil { + options = append(options, nats.ReconnectHandler(o.ReconnectHandler)) + } else { + options = append(options, nats.ReconnectHandler(ReconnectHandlerDefault)) + } + + if o.ClosedHandler != nil { + options = append(options, nats.ClosedHandler(o.ClosedHandler)) + } else { + options = append(options, nats.ClosedHandler(ClosedHandlerDefault)) + } + + if o.LameDuckModeHandler != nil { + options = append(options, nats.LameDuckModeHandler(o.LameDuckModeHandler)) + } else { + options = append(options, nats.LameDuckModeHandler(LameDuckModeHandlerDefault)) + } + + if o.ErrorHandler != nil { + options = append(options, nats.ErrorHandler(o.ErrorHandler)) + } else { + options = append(options, nats.ErrorHandler(ErrorHandlerDefault)) + } + + options = append(options, o.AdditionalOptions...) + + return serverString, options +} + +// ConnectAs Connects to NATS using the supplied options, including retrying if +// unavailable +func (o NATSOptions) Connect() (sdp.EncodedConnection, error) { + servers, opts := o.ToNatsOptions() + + var triesLeft int + + if o.NumRetries >= 0 { + triesLeft = o.NumRetries + 1 + } else { + triesLeft = -1 + } + + var nc *nats.Conn + var err error + + for triesLeft != 0 { + if triesLeft > 0 { + triesLeft-- + } + // Log a non-negative value: 0 means unlimited retries (NumRetries < 0) + logTriesLeft := triesLeft + if logTriesLeft < 0 { + logTriesLeft = 0 + } + lf := log.Fields{ + "servers": servers, + "triesLeft": logTriesLeft, + } + log.WithFields(lf).Info("NATS connecting") + + nc, err = nats.Connect( + servers, + opts..., + ) + + if err != nil && triesLeft != 0 { + log.WithError(err).WithFields(lf).Error("Error connecting to NATS") + time.Sleep(o.RetryDelay) + continue + } + + log.WithFields(lf).Info("NATS connected") + break + } + + if err != nil { + err = errors.Join(err, MaxRetriesError{}) + return &sdp.EncodedConnectionImpl{}, err + } + + return &sdp.EncodedConnectionImpl{Conn: nc}, nil +} diff --git a/go/auth/nats_test.go b/go/auth/nats_test.go new file mode 100644 index 00000000..d286245a --- /dev/null +++ b/go/auth/nats_test.go @@ -0,0 +1,423 @@ +package auth + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/google/uuid" + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nats.go" + "github.com/nats-io/nkeys" + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestToNatsOptions(t *testing.T) { + t.Run("with defaults", func(t *testing.T) { + o := NATSOptions{} + + expectedOptions, err := optionsToStruct([]nats.Option{ + nats.Timeout(ConnectionTimeoutDefault), + nats.MaxReconnects(MaxReconnectsDefault), + nats.ReconnectWait(ReconnectWaitDefault), + nats.ReconnectJitter(ReconnectJitterDefault, ReconnectJitterDefault), + nats.ConnectHandler(ConnectHandlerDefault), + nats.DisconnectErrHandler(DisconnectErrHandlerDefault), + nats.ReconnectHandler(ReconnectHandlerDefault), + nats.ClosedHandler(ClosedHandlerDefault), + nats.LameDuckModeHandler(LameDuckModeHandlerDefault), + nats.ErrorHandler(ErrorHandlerDefault), + }) + if err != nil { + t.Fatal(err) + } + + server, options := o.ToNatsOptions() + + if server != "" { + t.Error("Expected server to be empty") + } + + actualOptions, err := optionsToStruct(options) + if err != nil { + t.Fatal(err) + } + + if expectedOptions.MaxReconnect != actualOptions.MaxReconnect { + t.Errorf("Expected MaxReconnect to be %v, got %v", expectedOptions.MaxReconnect, actualOptions.MaxReconnect) + } + + if expectedOptions.Timeout != actualOptions.Timeout { + t.Errorf("Expected ConnectionTimeout to be %v, got %v", expectedOptions.Timeout, actualOptions.Timeout) + } + + if expectedOptions.ReconnectWait != actualOptions.ReconnectWait { + t.Errorf("Expected ReconnectWait to be %v, got %v", expectedOptions.ReconnectWait, actualOptions.ReconnectWait) + } + + if expectedOptions.ReconnectJitter != actualOptions.ReconnectJitter { + t.Errorf("Expected ReconnectJitter to be %v, got %v", expectedOptions.ReconnectJitter, actualOptions.ReconnectJitter) + } + + // TokenClient + if expectedOptions.UserJWT != nil || expectedOptions.SignatureCB != nil { + t.Error("Expected TokenClient to be nil") + } + + if actualOptions.DisconnectedErrCB == nil { + t.Error("Expected DisconnectedErrCB to be non-nil") + } + + if actualOptions.ReconnectedCB == nil { + t.Error("Expected ReconnectedCB to be non-nil") + } + + if actualOptions.ClosedCB == nil { + t.Error("Expected ClosedCB to be non-nil") + } + + if actualOptions.LameDuckModeHandler == nil { + t.Error("Expected LameDuckModeHandler to be non-nil") + } + + if actualOptions.AsyncErrorCB == nil { + t.Error("Expected AsyncErrorCB to be non-nil") + } + }) + + t.Run("with non-defaults", func(t *testing.T) { + var connectHandlerUsed bool + var disconnectErrHandlerUsed bool + var reconnectHandlerUsed bool + var closedHandlerUsed bool + var lameDuckModeHandlerUsed bool + var errorHandlerUsed bool + + o := NATSOptions{ + Servers: []string{"one", "two"}, + ConnectionName: "foo", + MaxReconnects: 999, + ReconnectWait: 999, + ReconnectJitter: 999, + ConnectHandler: func(c *nats.Conn) { connectHandlerUsed = true }, + DisconnectErrHandler: func(c *nats.Conn, err error) { disconnectErrHandlerUsed = true }, + ReconnectHandler: func(c *nats.Conn) { reconnectHandlerUsed = true }, + ClosedHandler: func(c *nats.Conn) { closedHandlerUsed = true }, + LameDuckModeHandler: func(c *nats.Conn) { lameDuckModeHandlerUsed = true }, + ErrorHandler: func(c *nats.Conn, s *nats.Subscription, err error) { errorHandlerUsed = true }, + } + + expectedOptions, err := optionsToStruct([]nats.Option{ + nats.Name("foo"), + nats.MaxReconnects(999), + nats.ReconnectWait(999), + nats.ReconnectJitter(999, 999), + nats.DisconnectErrHandler(nil), + nats.ReconnectHandler(nil), + nats.ClosedHandler(nil), + nats.LameDuckModeHandler(nil), + nats.ErrorHandler(nil), + }) + if err != nil { + t.Fatal(err) + } + + server, options := o.ToNatsOptions() + + if server != "one,two" { + t.Errorf("Expected server to be one,two got %v", server) + } + + actualOptions, err := optionsToStruct(options) + if err != nil { + t.Fatal(err) + } + + if expectedOptions.MaxReconnect != actualOptions.MaxReconnect { + t.Errorf("Expected MaxReconnect to be %v, got %v", expectedOptions.MaxReconnect, actualOptions.MaxReconnect) + } + + if expectedOptions.ReconnectWait != actualOptions.ReconnectWait { + t.Errorf("Expected ReconnectWait to be %v, got %v", expectedOptions.ReconnectWait, actualOptions.ReconnectWait) + } + + if expectedOptions.ReconnectJitter != actualOptions.ReconnectJitter { + t.Errorf("Expected ReconnectJitter to be %v, got %v", expectedOptions.ReconnectJitter, actualOptions.ReconnectJitter) + } + + if actualOptions.DisconnectedErrCB != nil { + actualOptions.DisconnectedErrCB(nil, nil) + if !disconnectErrHandlerUsed { + t.Error("DisconnectErrHandler not used") + } + } else { + t.Error("Expected DisconnectedErrCB to non-nil") + } + + if actualOptions.ConnectedCB != nil { + actualOptions.ConnectedCB(nil) + if !connectHandlerUsed { + t.Error("ConnectHandler not used") + } + } else { + t.Error("Expected ConnectedCB to non-nil") + } + + if actualOptions.ReconnectedCB != nil { + actualOptions.ReconnectedCB(nil) + if !reconnectHandlerUsed { + t.Error("ReconnectHandler not used") + } + } else { + t.Error("Expected ReconnectedCB to non-nil") + } + + if actualOptions.ClosedCB != nil { + actualOptions.ClosedCB(nil) + if !closedHandlerUsed { + t.Error("ClosedHandler not used") + } + } else { + t.Error("Expected ClosedCB to non-nil") + } + + if actualOptions.LameDuckModeHandler != nil { + actualOptions.LameDuckModeHandler(nil) + if !lameDuckModeHandlerUsed { + t.Error("LameDuckModeHandler not used") + } + } else { + t.Error("Expected LameDuckModeHandler to non-nil") + } + + if actualOptions.AsyncErrorCB != nil { + actualOptions.AsyncErrorCB(nil, nil, nil) + if !errorHandlerUsed { + t.Error("ErrorHandler not used") + } + } else { + t.Error("Expected AsyncErrorCB to non-nil") + } + }) +} + +func TestNATSConnect(t *testing.T) { + if os.Getenv("CI") == "true" { + t.Skip("Skipping test in CI environment, missing nats token exchange server") + } + + t.Run("with a bad URL", func(t *testing.T) { + o := NATSOptions{ + Servers: []string{"nats://badname.dontresolve.com"}, + NumRetries: 5, + RetryDelay: 100 * time.Millisecond, + } + + start := time.Now() + + _, err := o.Connect() + + // Just sanity check the duration here, it should not be less than + // NumRetries * RetryDelay and it should be more than... Some larger + // number of seconds. This is very much dependant on how long it takes + // to not resolve the name + if time.Since(start) < 5*100*time.Millisecond { + t.Errorf("Reconnecting didn't take long enough, expected >0.5s got: %v", time.Since(start).String()) + } + + if time.Since(start) > 3*time.Second { + t.Errorf("Reconnecting took too long, expected <3s got: %v", time.Since(start).String()) + } + + var maxRetriesError MaxRetriesError + if !errors.As(err, &maxRetriesError) { + t.Errorf("Unknown error type %T: %v", err, err) + } + }) + + t.Run("with a bad URL, but a good token", func(t *testing.T) { + tk := GetTestOAuthTokenClient(t) + + startToken, err := tk.GetJWT() + if err != nil { + t.Fatal(err) + } + + o := NATSOptions{ + Servers: []string{"nats://badname.dontresolve.com"}, + TokenClient: tk, + NumRetries: 3, + RetryDelay: 100 * time.Millisecond, + } + + _, err = o.Connect() + + var maxRetriesError MaxRetriesError + if errors.As(err, &maxRetriesError) { + // Make sure we have only got one token, not three + currentToken, err := o.TokenClient.GetJWT() + if err != nil { + t.Fatal(err) + } + + if currentToken != startToken { + t.Error("Tokens have changed") + } + } else { + t.Errorf("Unknown error type %T", err) + } + }) + + t.Run("with a good URL", func(t *testing.T) { + o := NATSOptions{ + Servers: []string{ + "nats://nats:4222", + "nats://localhost:4222", + }, + NumRetries: 3, + RetryDelay: 100 * time.Millisecond, + } + + conn, err := o.Connect() + if err != nil { + t.Fatal(err) + } + + ValidateNATSConnection(t, conn) + }) + + t.Run("with a good URL but no retries", func(t *testing.T) { + o := NATSOptions{ + Servers: []string{ + "nats://nats:4222", + "nats://localhost:4222", + }, + } + + conn, err := o.Connect() + if err != nil { + t.Fatal(err) + } + + ValidateNATSConnection(t, conn) + }) + + t.Run("with a good URL and infinite retries", func(t *testing.T) { + o := NATSOptions{ + Servers: []string{ + "nats://nats:4222", + "nats://localhost:4222", + }, + NumRetries: -1, + RetryDelay: 100 * time.Millisecond, + } + + conn, err := o.Connect() + if err != nil { + t.Error(err) + } + + ValidateNATSConnection(t, conn) + }) +} + +func TestTokenRefresh(t *testing.T) { + if os.Getenv("CI") == "true" { + t.Skip("Skipping test in CI environment, missing nats token exchange server") + } + + tk := GetTestOAuthTokenClient(t) + + // Get a token + token, err := tk.GetJWT() + if err != nil { + t.Fatal(err) + } + + // Artificially set the expiry and replace the token + claims, err := jwt.DecodeUserClaims(token) + if err != nil { + t.Fatal(err) + } + + pair, err := nkeys.CreateAccount() + if err != nil { + t.Fatal(err) + } + + claims.Expires = time.Now().Add(-10 * time.Second).Unix() + tk.jwt, err = claims.Encode(pair) + expiredToken := tk.jwt + + if err != nil { + t.Error(err) + } + + // Get the token again + newToken, err := tk.GetJWT() + if err != nil { + t.Error(err) + } + + if expiredToken == newToken { + t.Error("token is unchanged") + } +} + +func ValidateNATSConnection(t *testing.T, ec sdp.EncodedConnection) { + t.Helper() + done := make(chan struct{}) + + sub, err := ec.Subscribe("test", sdp.NewQueryResponseHandler("test", func(ctx context.Context, qr *sdp.QueryResponse) { + rt, ok := qr.GetResponseType().(*sdp.QueryResponse_Response) + if !ok { + t.Errorf("Received unexpected message: %v", qr) + } + + if rt.Response.GetResponder() == "test" { + done <- struct{}{} + } + })) + if err != nil { + t.Error(err) + } + + ru := uuid.New() + err = ec.Publish(context.Background(), "test", sdp.NewQueryResponseFromResponse(&sdp.Response{ + Responder: "test", + ResponderUUID: ru[:], + State: sdp.ResponderState_COMPLETE, + })) + if err != nil { + t.Error(err) + } + + // Wait for the message to come back + select { + case <-done: + // Good + case <-time.After(500 * time.Millisecond): + t.Error("Didn't get message after 500ms") + } + + err = sub.Unsubscribe() + if err != nil { + t.Error(err) + } +} + +func optionsToStruct(options []nats.Option) (nats.Options, error) { + var o nats.Options + var err error + + for _, option := range options { + err = option(&o) + if err != nil { + return o, err + } + } + + return o, nil +} diff --git a/go/auth/tracing.go b/go/auth/tracing.go new file mode 100644 index 00000000..6a778742 --- /dev/null +++ b/go/auth/tracing.go @@ -0,0 +1,18 @@ +package auth + +import ( + "go.opentelemetry.io/otel" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" +) + +const ( + instrumentationName = "github.com/overmindtech/cli/go/auth" + instrumentationVersion = "0.0.1" +) + +var tracer = otel.GetTracerProvider().Tracer( + instrumentationName, + trace.WithInstrumentationVersion(instrumentationVersion), + trace.WithSchemaURL(semconv.SchemaURL), +) diff --git a/go/discovery/adapter.go b/go/discovery/adapter.go new file mode 100644 index 00000000..65821583 --- /dev/null +++ b/go/discovery/adapter.go @@ -0,0 +1,205 @@ +package discovery + +import ( + "context" + "slices" + "sync" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// Adapter is capable of finding information about items +// +// Adapters must implement all of the methods to satisfy this interface in order +// to be able to used as an SDP adapter. Note that the `context.Context` value +// that is passed to the Get(), List() and Search() (optional) methods needs to +// handled by each adapter individually. Adapter authors should make an effort +// ensure that expensive operations that the adapter undertakes can be cancelled +// if the context `ctx` is cancelled +type Adapter interface { + // Type The type of items that this adapter is capable of finding + Type() string + + // Descriptive name for the adapter, used in logging and metadata + Name() string + + // List of scopes that this adapter is capable of find items for. If the + // adapter supports all scopes the special value "*" + // should be used + Scopes() []string + + // Get Get a single item with a given scope and query. The item returned + // should have a UniqueAttributeValue that matches the `query` parameter. + Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) + + // A struct that contains information about the adapter, it is used by the api-server to determine the capabilities of the adapter + // It is mandatory for all adapters to implement this method + Metadata() *sdp.AdapterMetadata +} + +// An adapter that support the List method. This was previously part of the +// Adapter interface however it was split out to allow for the transition to +// streaming responses +type ListableAdapter interface { + Adapter + + // List Lists all items in a given scope + List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) +} + +// ListStreamableAdapter supports streaming for the List queries. +type ListStreamableAdapter interface { + Adapter + ListStream(ctx context.Context, scope string, ignoreCache bool, stream QueryResultStream) +} + +// SearchStreamableAdapter supports streaming for the Search queries. +type SearchStreamableAdapter interface { + Adapter + SearchStream(ctx context.Context, scope string, query string, ignoreCache bool, stream QueryResultStream) +} + +// CachingAdapter Is an adapter of items that supports caching +type CachingAdapter interface { + Adapter + Cache() sdpcache.Cache +} + +// SearchableAdapter Is an adapter of items that supports searching +type SearchableAdapter interface { + Adapter + // Search executes a specific search and returns zero or many items as a + // result (and optionally an error). The specific format of the query that + // needs to be provided to Search is dependant on the adapter itself as each + // adapter will respond to searches differently + Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) +} + +// HiddenAdapter adapters that define a `Hidden()` method are able to tell whether +// or not the items they produce should be marked as hidden within the metadata. +// Hidden items will not be shown in GUIs or stored in databases and are used +// for gathering data as part of other processes such as remotely executed +// secondary adapters +type HiddenAdapter interface { + Hidden() bool +} + +// WildcardScopeAdapter is an optional interface that adapters can implement +// to declare they can handle "*" wildcard scopes efficiently for LIST queries +// (e.g., using GCP's aggregatedList API). When an adapter implements this +// interface and returns true from SupportsWildcardScope(), the engine will +// pass wildcard scopes directly to the adapter instead of expanding them to +// all configured scopes—but only for LIST queries. +// +// For GET and SEARCH, the engine always expands wildcard scope so that +// multiple results can be returned when a resource exists in multiple scopes. +// Future work may extend this optimization to SEARCH once adapters support it. +type WildcardScopeAdapter interface { + Adapter + SupportsWildcardScope() bool +} + +// QueryResultStream is a stream of items and errors that are returned from a +// query. Adapters should send items to the stream as soon as they are +// discovered using the `SendItem` method and should send any errors that occur +// using the `SendError` method. These errors will be considered non-fatal. If +// the process encounters a fatal error it should return an error to the caller +// rather then sending one on the stream. +// +// Note that this interface does not have a `Close()` method. Clients of this +// interface are specific functions that get passed in an instance implementing +// this interface. The expectation is that those clients do not return until all +// calls into the stream have finished. +type QueryResultStream interface { + // SendItem sends an item to the stream. This method is thread-safe, but the + // ordering vs SendError is only guaranteed for non-overlapping calls. + SendItem(item *sdp.Item) + // SendError sends an Error to the stream. This method is thread-safe, but + // the ordering vs SendItem is only guaranteed for non-overlapping calls. + SendError(err error) +} + +// QueryResultStream is a stream of items and errors that are returned from a +// query. Adapters should send items to the stream as soon as they are +// discovered using the `SendItem` method and should send any errors that occur +// using the `SendError` method. These errors will be considered non-fatal. If +// the process encounters a fatal error it should return an error to the caller +// rather then sending one on the stream +type QueryResultStreamWithHandlers struct { + itemHandler ItemHandler + errHandler ErrHandler +} + +// assert interface implementation +var _ QueryResultStream = (*QueryResultStreamWithHandlers)(nil) + +// ItemHandler is a function that can be used to handle items as they are +// received from a QueryResultStream +type ItemHandler func(item *sdp.Item) + +// ErrHandler is a function that can be used to handle errors as they are +// received from a QueryResultStream +type ErrHandler func(err error) + +// NewQueryResultStream creates a new QueryResultStream that calls the provided +// handlers when items and errors are received. Note that the handlers are +// called asynchronously and need to provide for their own thread safety. +func NewQueryResultStream(itemHandler ItemHandler, errHandler ErrHandler) *QueryResultStreamWithHandlers { + stream := &QueryResultStreamWithHandlers{ + itemHandler: itemHandler, + errHandler: errHandler, + } + + return stream +} + +// SendItem sends an item to the stream +func (qrs *QueryResultStreamWithHandlers) SendItem(item *sdp.Item) { + qrs.itemHandler(item) +} + +// SendError sends an error to the stream +func (qrs *QueryResultStreamWithHandlers) SendError(err error) { + qrs.errHandler(err) +} + +type RecordingQueryResultStream struct { + streamMu sync.Mutex + items []*sdp.Item + errs []error +} + +// assert interface implementation +var _ QueryResultStream = (*RecordingQueryResultStream)(nil) + +func NewRecordingQueryResultStream() *RecordingQueryResultStream { + return &RecordingQueryResultStream{ + items: []*sdp.Item{}, + errs: []error{}, + } +} + +func (r *RecordingQueryResultStream) SendItem(item *sdp.Item) { + r.streamMu.Lock() + defer r.streamMu.Unlock() + r.items = append(r.items, item) +} + +func (r *RecordingQueryResultStream) GetItems() []*sdp.Item { + r.streamMu.Lock() + defer r.streamMu.Unlock() + return slices.Clone(r.items) +} + +func (r *RecordingQueryResultStream) SendError(err error) { + r.streamMu.Lock() + defer r.streamMu.Unlock() + r.errs = append(r.errs, err) +} + +func (r *RecordingQueryResultStream) GetErrors() []error { + r.streamMu.Lock() + defer r.streamMu.Unlock() + return slices.Clone(r.errs) +} diff --git a/go/discovery/adapter_test.go b/go/discovery/adapter_test.go new file mode 100644 index 00000000..fcf0ec91 --- /dev/null +++ b/go/discovery/adapter_test.go @@ -0,0 +1,736 @@ +package discovery + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestEngineAddAdapters(t *testing.T) { + ec := EngineConfig{} + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + adapter := TestAdapter{} + + if err := e.AddAdapters(&adapter); err != nil { + t.Fatalf("Error adding adapter: %v", err) + } + + if x := len(e.sh.Adapters()); x != 1 { + t.Fatalf("Expected 1 adapters, got %v", x) + } +} + +func TestGet(t *testing.T) { + adapter := TestAdapter{ + ReturnName: "orange", + ReturnType: "person", + ReturnScopes: []string{ + "test", + "empty", + }, + cache: sdpcache.NewMemoryCache(), + } + + e := newStartedEngine(t, "TestGet", nil, nil, &adapter) + + t.Run("Basic test", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Query: "three", + Method: sdp.QueryMethod_GET, + }) + if err != nil { + t.Fatal(err) + } + + if x := len(adapter.GetCalls); x != 1 { + t.Fatalf("Expected 1 get call, got %v", x) + } + + firstCall := adapter.GetCalls[0] + + if firstCall[0] != "test" || firstCall[1] != "three" { + t.Fatalf("First get call parameters unexpected: %v", firstCall) + } + }) + + t.Run("not found error", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + items, edges, errs, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "empty", + Query: "three", + Method: sdp.QueryMethod_GET, + }) + if err != nil { + t.Fatal(err) + } + + if len(errs) == 1 { + if errs[0].GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected ErrorType to be %v, got %v", sdp.QueryError_NOTFOUND, errs[0].GetErrorType()) + } + if errs[0].GetErrorString() != "no items found" { + t.Errorf("expected ErrorString to be '%v', got '%v'", "no items found", errs[0].GetErrorString()) + } + if errs[0].GetScope() != "empty" { + t.Errorf("expected Scope to be '%v', got '%v'", "empty", errs[0].GetScope()) + } + if errs[0].GetSourceName() != "testAdapter-orange" { + t.Errorf("expected Adapter name to be '%v', got '%v'", "testAdapter-orange", errs[0].GetSourceName()) + } + if errs[0].GetItemType() != "person" { + t.Errorf("expected ItemType to be '%v', got '%v'", "person", errs[0].GetItemType()) + } + if errs[0].GetResponderName() != "TestGet" { + t.Errorf("expected ResponderName to be '%v', got '%v'", "TestGet", errs[0].GetResponderName()) + } + } else { + t.Errorf("expected 1 error, got %v", len(errs)) + } + + if len(items) != 0 { + t.Errorf("expected 0 items, got %v: %v", len(items), items) + } + if len(edges) != 0 { + t.Errorf("expected 0 edges, got %v: %v", len(edges), edges) + } + }) + + t.Run("Test caching", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var list1 []*sdp.Item + var item2 []*sdp.Item + var item3 []*sdp.Item + var err error + + req := sdp.Query{ + Type: "person", + Scope: "test", + Query: "Dylan", + Method: sdp.QueryMethod_GET, + } + + list1, _, _, err = e.executeQuerySync(context.Background(), &req) + if err != nil { + t.Error(err) + } + + time.Sleep(10 * time.Millisecond) + item2, _, _, err = e.executeQuerySync(context.Background(), &req) + if err != nil { + t.Error(err) + } + + if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != item2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { + t.Errorf("Get queries 10ms apart had different timestamps, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue(), item2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue()) + } + + time.Sleep(10 * time.Millisecond) + + item3, _, _, err = e.executeQuerySync(context.Background(), &req) + if err != nil { + t.Error(err) + } + + if item2[0].GetMetadata().GetTimestamp().String() == item3[0].GetMetadata().GetTimestamp().String() { + t.Error("Get queries after purging had the same timestamps, cache not expiring") + } + }) + + t.Run("Test Get() caching errors", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + req := sdp.Query{ + Type: "person", + Scope: "empty", + Query: "query", + Method: sdp.QueryMethod_GET, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), &req) + if err != nil { + t.Fatal(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + _, _, errs, err = e.executeQuerySync(context.Background(), &req) + if err != nil { + t.Fatal(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.GetCalls); l != 1 { + t.Errorf("Expected 1 Get call due to caching og NOTFOUND errors, got %v", l) + } + }) + + t.Run("Hidden items", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + adapter.IsHidden = true + + t.Run("Get", func(t *testing.T) { + item, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Query: "three", + Method: sdp.QueryMethod_GET, + }) + if err != nil { + t.Fatal(err) + } + + if !item[0].GetMetadata().GetHidden() { + t.Fatal("Item was not marked as hidden in metadata") + } + }) + + t.Run("List", func(t *testing.T) { + items, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Method: sdp.QueryMethod_LIST, + }) + if err != nil { + t.Fatal(err) + } + + if !items[0].GetMetadata().GetHidden() { + t.Fatal("Item was not marked as hidden in metadata") + } + }) + + t.Run("Search", func(t *testing.T) { + items, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Query: "three", + Method: sdp.QueryMethod_SEARCH, + }) + if err != nil { + t.Fatal(err) + } + + if !items[0].GetMetadata().GetHidden() { + t.Fatal("Item was not marked as hidden in metadata") + } + }) + }) +} + +func TestList(t *testing.T) { + adapter := TestAdapter{} + adapter.cache = sdpcache.NewMemoryCache() + + e := newStartedEngine(t, "TestList", nil, nil, &adapter) + + _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Method: sdp.QueryMethod_LIST, + }) + if err != nil { + t.Fatal(err) + } + + if x := len(adapter.ListCalls); x != 1 { + t.Fatalf("Expected 1 find call, got %v", x) + } + + firstCall := adapter.ListCalls[0] + + if firstCall[0] != "test" { + t.Fatalf("First find call parameters unexpected: %v", firstCall) + } +} + +func TestSearch(t *testing.T) { + adapter := TestAdapter{} + adapter.cache = sdpcache.NewMemoryCache() + + e := newStartedEngine(t, "TestSearch", nil, nil, &adapter) + + _, _, _, err := e.executeQuerySync(context.Background(), &sdp.Query{ + Type: "person", + Scope: "test", + Query: "query", + Method: sdp.QueryMethod_SEARCH, + }) + if err != nil { + t.Fatal(err) + } + + if x := len(adapter.SearchCalls); x != 1 { + t.Fatalf("Expected 1 Search call, got %v", x) + } + + firstCall := adapter.SearchCalls[0] + + if firstCall[0] != "test" || firstCall[1] != "query" { + t.Fatalf("First Search call parameters unexpected: %v", firstCall) + } +} + +func TestListSearchCaching(t *testing.T) { + adapter := TestAdapter{ + ReturnScopes: []string{ + "test", + "empty", + "error", + }, + cache: sdpcache.NewMemoryCache(), + } + + e := newStartedEngine(t, "TestListSearchCaching", nil, nil, &adapter) + + t.Run("caching with successful list", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var list1 []*sdp.Item + var list2 []*sdp.Item + var list3 []*sdp.Item + var err error + q := sdp.Query{ + Type: "person", + Scope: "test", + Method: sdp.QueryMethod_LIST, + } + + list1, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + time.Sleep(10 * time.Millisecond) + + list2, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Fatal(err) + } + + if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { + t.Errorf("List queries had different generations, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) + } + + time.Sleep(10 * time.Millisecond) + + list3, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Fatal(err) + } + + if list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"] == list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"] { + t.Errorf("List queries after purging had the same generation, caching not working. %v == %v", list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) + } + }) + + t.Run("empty list", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var err error + q := sdp.Query{ + Type: "person", + Scope: "empty", + Method: sdp.QueryMethod_LIST, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Fatal(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + time.Sleep(10 * time.Millisecond) + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Fatal(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.ListCalls); l != 1 { + t.Errorf("Expected only 1 list call, got %v, cache not working: %v", l, adapter.ListCalls) + } + + time.Sleep(200 * time.Millisecond) + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Fatal(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.ListCalls); l != 2 { + t.Errorf("Expected 2 list calls, got %v, cache not clearing: %v", l, adapter.ListCalls) + } + }) + + t.Run("caching with successful search", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var list1 []*sdp.Item + var list2 []*sdp.Item + var list3 []*sdp.Item + var err error + q := sdp.Query{ + Type: "person", + Scope: "test", + Query: "query", + Method: sdp.QueryMethod_SEARCH, + } + + list1, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + time.Sleep(10 * time.Millisecond) + + list2, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { + t.Errorf("List queries had different generations, caching not working. %v != %v", list1[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) + } + + time.Sleep(200 * time.Millisecond) + + list3, _, _, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() == list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { + t.Errorf("List queries 200ms apart had the same generations, caching not working. %v == %v", list2[0].GetAttributes().GetAttrStruct().GetFields()["generation"], list3[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) + } + }) + + t.Run("empty search", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var err error + q := sdp.Query{ + Type: "person", + Scope: "empty", + Query: "query", + Method: sdp.QueryMethod_SEARCH, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + time.Sleep(10 * time.Millisecond) + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + time.Sleep(200 * time.Millisecond) + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.SearchCalls); l != 2 { + t.Errorf("Expected 2 find calls, got %v, cache not clearing", l) + } + }) + + t.Run("non-caching of OTHER errors", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + q := sdp.Query{ + Type: "person", + Scope: "error", + Query: "query", + Method: sdp.QueryMethod_GET, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.GetCalls); l != 2 { + t.Errorf("Expected 2 get calls, got %v, OTHER errors should not be cached", l) + } + }) + + t.Run("non-caching when ignoreCache is specified", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + q := sdp.Query{ + Type: "person", + Scope: "error", + Query: "query", + Method: sdp.QueryMethod_GET, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + q.Method = sdp.QueryMethod_LIST + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + q.Method = sdp.QueryMethod_SEARCH + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + _, _, errs, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + if len(errs) != 1 { + t.Fatalf("Expected 1 error, got %v", len(errs)) + } + + if l := len(adapter.GetCalls); l != 2 { + t.Errorf("Expected 2 get calls, got %v", l) + } + + if l := len(adapter.ListCalls); l != 2 { + t.Errorf("Expected 2 List calls, got %v", l) + } + + if l := len(adapter.SearchCalls); l != 2 { + t.Errorf("Expected 2 Search calls, got %v", l) + } + }) +} + +func TestSearchGetCaching(t *testing.T) { + // We want to be sure that if an item has been found via a search and + // cached, the cache will be hit if a Get is run for that particular item + + adapter := TestAdapter{ + ReturnScopes: []string{ + "test", + }, + cache: sdpcache.NewMemoryCache(), + } + + e := newStartedEngine(t, "TestSearchGetCaching", nil, nil, &adapter) + + t.Run("caching with successful search", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + var searchResult []*sdp.Item + var searchErrors []*sdp.QueryError + var getResult []*sdp.Item + var getErrors []*sdp.QueryError + var err error + q := sdp.Query{ + Type: "person", + Scope: "test", + Query: "Dylan", + Method: sdp.QueryMethod_SEARCH, + } + + t.Logf("Searching for %v", q.GetQuery()) + searchResult, _, searchErrors, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if len(searchErrors) != 0 { + for _, err := range searchErrors { + t.Error(err) + } + } + + if len(searchResult) == 0 { + t.Fatal("Got no results") + } + + if len(searchResult) > 1 { + t.Fatalf("Got too many results: %v", searchResult) + } + + time.Sleep(10 * time.Millisecond) + + // Do a get query for that same item + q.Method = sdp.QueryMethod_GET + q.Query = searchResult[0].UniqueAttributeValue() + + t.Logf("Getting %v from cache", q.GetQuery()) + getResult, _, getErrors, err = e.executeQuerySync(context.Background(), &q) + if err != nil { + t.Error(err) + } + + if len(getErrors) != 0 { + for _, err := range getErrors { + t.Error(err) + } + } + + if len(getResult) == 0 { + t.Error("No result from GET") + } + + if searchResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() != getResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"].GetNumberValue() { + t.Errorf("Search and Get queries had different generations, caching not working. %v != %v", searchResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"], getResult[0].GetAttributes().GetAttrStruct().GetFields()["generation"]) + } + }) +} + +func TestNewQueryResultStream(t *testing.T) { + items := make(chan *sdp.Item, 10) + errs := make(chan error, 10) + + itemHandler := func(item *sdp.Item) { + time.Sleep(10 * time.Millisecond) + items <- item + } + + errHandler := func(err error) { + time.Sleep(10 * time.Millisecond) + errs <- err + } + + stream := NewQueryResultStream(itemHandler, errHandler) + + // Test Initialization + if stream == nil { + t.Fatal("Expected stream to be initialized, got nil") + } + if stream.itemHandler == nil || stream.errHandler == nil { + t.Fatal("Expected handlers to be set") + } + + // Test SendItem + testItem := &sdp.Item{} + stream.SendItem(testItem) + + // Due to the fact that the handlers are executed in a goroutine it + // essentially gives us a buffered channel with a buffer depth of 1 since + // the item can be pulled off the internal items channel immediately then + // wait on the handler in parallel. That's what allows this test to work + // without extra synchronization + if x := <-items; x != testItem { + t.Fatalf("Expected item to be %v, got %v", testItem, x) + } + + // Test SendError + testErr := errors.New("test error") + stream.SendError(testErr) + + if x := <-errs; x.Error() != testErr.Error() { + t.Fatalf("Expected error to be %v, got %v", testErr, x) + } +} diff --git a/go/discovery/adapterhost.go b/go/discovery/adapterhost.go new file mode 100644 index 00000000..18014515 --- /dev/null +++ b/go/discovery/adapterhost.go @@ -0,0 +1,203 @@ +package discovery + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// AdapterHost This struct holds references to all Adapters in a process +// and provides utility functions to work with them. Methods of this +// struct are safe to call concurrently. +type AdapterHost struct { + // Map of types to all adapters for that type + adapters []Adapter + // Index for O(1) duplicate detection: map[type]map[scope]exists + adapterIndex map[string]map[string]bool + mutex sync.RWMutex +} + +func NewAdapterHost() *AdapterHost { + sh := &AdapterHost{ + adapters: make([]Adapter, 0), + adapterIndex: make(map[string]map[string]bool), + } + + return sh +} + +var ErrAdapterAlreadyExists = errors.New("adapter already exists") + +// AddAdapters Adds an adapter to this engine +func (sh *AdapterHost) AddAdapters(adapters ...Adapter) error { + sh.mutex.Lock() + defer sh.mutex.Unlock() + + for _, newAdapter := range adapters { + newType := newAdapter.Type() + newScopes := newAdapter.Scopes() + + // Check for overlapping scopes using O(1) index lookup instead of O(n) scan + if scopeMap, exists := sh.adapterIndex[newType]; exists { + for _, newScope := range newScopes { + if scopeMap[newScope] { + log.Errorf("Error: Adapter with type %s and overlapping scope %s already exists", + newType, newScope) + return fmt.Errorf("adapter with type %s and overlapping scopes already exists", newType) + } + } + } + + // Add to index + if sh.adapterIndex[newType] == nil { + sh.adapterIndex[newType] = make(map[string]bool) + } + for _, scope := range newScopes { + sh.adapterIndex[newType][scope] = true + } + + // Add to adapters list + sh.adapters = append(sh.adapters, newAdapter) + } + + return nil +} + +// Adapters Returns a slice of all known adapters +func (sh *AdapterHost) Adapters() []Adapter { + sh.mutex.RLock() + defer sh.mutex.RUnlock() + + adapters := make([]Adapter, 0) + + adapters = append(adapters, sh.adapters...) + + return adapters +} + +// VisibleAdapters Returns a slice of all known adapters excluding hidden ones +func (sh *AdapterHost) VisibleAdapters() []Adapter { + allAdapters := sh.Adapters() + result := make([]Adapter, 0) + + // Add all adapters unless they are hidden + for _, adapter := range allAdapters { + if hs, ok := adapter.(HiddenAdapter); ok { + if hs.Hidden() { + // If the adapter is hidden, continue without adding it + continue + } + } + + result = append(result, adapter) + } + + return result +} + +// AdapterByType Returns the adapters for a given type +func (sh *AdapterHost) AdaptersByType(typ string) []Adapter { + sh.mutex.RLock() + defer sh.mutex.RUnlock() + + adapters := make([]Adapter, 0) + + for _, adapter := range sh.adapters { + if adapter.Type() == typ { + adapters = append(adapters, adapter) + } + } + + return adapters +} + +// ExpandQuery Expands queries with wildcards to no longer contain wildcards. +// Meaning that if we support 5 types, and a query comes in with a wildcard +// type, this function will expand that query into 5 queries, one for each +// type. +// +// The same goes for scopes, if we have a query with a wildcard scope, and +// a single adapter that supports 5 scopes, we will end up with 5 queries. The +// exception to this is if we have an adapter that supports all scopes +// (implements WildcardScopeAdapter) and the query method is LIST. In that +// case we pass the wildcard scope directly to the adapter. For GET and +// SEARCH, we always expand so multiple results can be returned. +// +// This functions returns a map of queries with the adapters that they should be +// run against +func (sh *AdapterHost) ExpandQuery(q *sdp.Query) map[*sdp.Query]Adapter { + var checkAdapters []Adapter + + if IsWildcard(q.GetType()) { + // If the query has a wildcard type, all non-hidden adapters might try + // to respond + checkAdapters = sh.VisibleAdapters() + } else { + // If the type is specific, pull just adapters for that type + checkAdapters = append(checkAdapters, sh.AdaptersByType(q.GetType())...) + } + + expandedQueries := make(map[*sdp.Query]Adapter) + + for _, adapter := range checkAdapters { + // is the adapter is hidden + isHidden := false + if hs, ok := adapter.(HiddenAdapter); ok { + isHidden = hs.Hidden() + } + + // Check if adapter supports wildcard scopes + supportsWildcard := false + if ws, ok := adapter.(WildcardScopeAdapter); ok { + supportsWildcard = ws.SupportsWildcardScope() + } + + // If query has wildcard scope and adapter supports wildcards, + // create ONE query with wildcard scope (no expansion). + // Only for LIST: GET and SEARCH must expand so we can return + // multiple results when a resource exists in multiple scopes. + if supportsWildcard && IsWildcard(q.GetScope()) && !isHidden && q.GetMethod() == sdp.QueryMethod_LIST { + dest := proto.Clone(q).(*sdp.Query) + dest.Type = adapter.Type() // specialise the query to the adapter type + expandedQueries[dest] = adapter + continue // Skip normal scope expansion loop + } + + for _, adapterScope := range adapter.Scopes() { + // Create a new query if: + // + // * The adapter supports all scopes, or + // * The query scope is a wildcard (and the adapter is not hidden), or + // * The query scope substring matches adapter scope + if IsWildcard(adapterScope) || (IsWildcard(q.GetScope()) && !isHidden) || strings.Contains(adapterScope, q.GetScope()) { + dest := proto.Clone(q).(*sdp.Query) + + dest.Type = adapter.Type() + + // Choose the more specific scope + if IsWildcard(adapterScope) { + dest.Scope = q.GetScope() + } else { + dest.Scope = adapterScope + } + + expandedQueries[dest] = adapter + } + } + } + + return expandedQueries +} + +// ClearAllAdapters Removes all adapters from the engine +func (sh *AdapterHost) ClearAllAdapters() { + sh.mutex.Lock() + sh.adapters = make([]Adapter, 0) + sh.adapterIndex = make(map[string]map[string]bool) + sh.mutex.Unlock() +} diff --git a/go/discovery/adapterhost_bench_test.go b/go/discovery/adapterhost_bench_test.go new file mode 100644 index 00000000..91501d79 --- /dev/null +++ b/go/discovery/adapterhost_bench_test.go @@ -0,0 +1,822 @@ +package discovery + +import ( + "context" + "fmt" + "os" + "runtime" + "runtime/pprof" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/sourcegraph/conc/pool" +) + +// BenchmarkAddAdapters_GCPScenario simulates the real-world GCP organization scenario +// where we have many projects, regions, and zones creating thousands of adapters +func BenchmarkAddAdapters_GCPScenario(b *testing.B) { + scenarios := []struct { + name string + projects int + regions int + zones int + adapterTypes int // Simplified: different adapter types per scope level + }{ + {"Small_5proj", 5, 5, 10, 20}, + {"Medium_23proj", 23, 35, 135, 88}, // Current failing scenario + {"Large_100proj", 100, 35, 135, 88}, // Enterprise scenario + {"VeryLarge_500proj", 500, 35, 135, 88}, // Large enterprise + } + + for _, sc := range scenarios { + b.Run(sc.name, func(b *testing.B) { + b.ResetTimer() + for range b.N { + b.StopTimer() + adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.adapterTypes) + sh := NewAdapterHost() + b.StartTimer() + + start := time.Now() + err := sh.AddAdapters(adapters...) + elapsed := time.Since(start) + + b.StopTimer() + if err != nil { + b.Fatalf("Failed to add adapters: %v", err) + } + + totalAdapters := len(adapters) + b.ReportMetric(float64(totalAdapters), "adapters") + b.ReportMetric(elapsed.Seconds(), "seconds") + b.ReportMetric(float64(totalAdapters)/elapsed.Seconds(), "adapters/sec") + } + }) + } +} + +// BenchmarkAddAdapters_Scaling tests at different scales to demonstrate O(n²) behavior +func BenchmarkAddAdapters_Scaling(b *testing.B) { + sizes := []int{100, 500, 1000, 5000, 10000, 25000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) { + b.ResetTimer() + for range b.N { + b.StopTimer() + adapters := generateSimpleAdapters(size) + sh := NewAdapterHost() + b.StartTimer() + + start := time.Now() + err := sh.AddAdapters(adapters...) + elapsed := time.Since(start) + + b.StopTimer() + if err != nil { + b.Fatalf("Failed to add adapters: %v", err) + } + + b.ReportMetric(elapsed.Seconds(), "seconds") + b.ReportMetric(float64(size)/elapsed.Seconds(), "adapters/sec") + } + }) + } +} + +// BenchmarkAddAdapters_IncrementalAdd simulates adding adapters one project at a time +// This is closer to how it might be used in practice +func BenchmarkAddAdapters_IncrementalAdd(b *testing.B) { + projects := 100 + regionsPerProject := 35 + zonesPerProject := 135 + typesPerScope := 30 + + b.ResetTimer() + for range b.N { + b.StopTimer() + sh := NewAdapterHost() + b.StartTimer() + + start := time.Now() + + // Add adapters project by project (like we do in the real code) + for p := range projects { + projectAdapters := generateProjectAdapters(p, regionsPerProject, zonesPerProject, typesPerScope) + err := sh.AddAdapters(projectAdapters...) + if err != nil { + b.Fatalf("Failed to add adapters for project %d: %v", p, err) + } + } + + elapsed := time.Since(start) + b.StopTimer() + + totalAdapters := len(sh.Adapters()) + b.ReportMetric(float64(totalAdapters), "total_adapters") + b.ReportMetric(elapsed.Seconds(), "seconds") + b.ReportMetric(float64(totalAdapters)/elapsed.Seconds(), "adapters/sec") + } +} + +// generateGCPLikeAdapters creates adapters that mimic the GCP source structure: +// - Project-level adapters (one per project per type) +// - Regional adapters (one per project per region per type) +// - Zonal adapters (one per project per zone per type) +func generateGCPLikeAdapters(projects, regions, zones, typesPerScope int) []Adapter { + projectTypes := typesPerScope / 3 + regionalTypes := typesPerScope / 3 + zonalTypes := typesPerScope / 3 + + totalAdapters := (projects * projectTypes) + + (projects * regions * regionalTypes) + + (projects * zones * zonalTypes) + + adapters := make([]Adapter, 0, totalAdapters) + + for p := range projects { + projectID := fmt.Sprintf("project-%d", p) + + // Project-level adapters + for t := range projectTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{projectID}, + ReturnType: fmt.Sprintf("gcp-project-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), + }) + } + + // Regional adapters + for r := range regions { + scope := fmt.Sprintf("%s.region-%d", projectID, r) + for t := range regionalTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }) + } + } + + // Zonal adapters + for z := range zones { + scope := fmt.Sprintf("%s.zone-%d", projectID, z) + for t := range zonalTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }) + } + } + } + + return adapters +} + +// generateProjectAdapters creates all adapters for a single project +func generateProjectAdapters(projectNum, regions, zones, typesPerScope int) []Adapter { + projectTypes := typesPerScope / 3 + regionalTypes := typesPerScope / 3 + zonalTypes := typesPerScope / 3 + + totalAdapters := projectTypes + (regions * regionalTypes) + (zones * zonalTypes) + adapters := make([]Adapter, 0, totalAdapters) + + projectID := fmt.Sprintf("project-%d", projectNum) + + // Project-level adapters + for t := range projectTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{projectID}, + ReturnType: fmt.Sprintf("gcp-project-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), + }) + } + + // Regional adapters + for r := range regions { + scope := fmt.Sprintf("%s.region-%d", projectID, r) + for t := range regionalTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }) + } + } + + // Zonal adapters + for z := range zones { + scope := fmt.Sprintf("%s.zone-%d", projectID, z) + for t := range zonalTypes { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }) + } + } + + return adapters +} + +// generateSimpleAdapters creates n unique adapters for simple scaling tests +func generateSimpleAdapters(n int) []Adapter { + adapters := make([]Adapter, 0, n) + for i := range n { + adapters = append(adapters, &TestAdapter{ + ReturnScopes: []string{fmt.Sprintf("scope-%d", i)}, + ReturnType: fmt.Sprintf("type-%d", i%100), // Reuse 100 types + ReturnName: fmt.Sprintf("adapter-%d", i), + }) + } + return adapters +} + +// BenchmarkListAdapter is a test adapter that returns 10 items per LIST query +// instead of the default 1 item. This is used for memory benchmarks to simulate +// realistic query execution patterns. +type BenchmarkListAdapter struct { + TestAdapter + itemsPerList int // Number of items to return per LIST query +} + +// List returns exactly 10 items (or itemsPerList if set) for each LIST query +func (b *BenchmarkListAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + // Use the embedded TestAdapter's List method logic but return multiple items + // We'll call the parent's cache lookup, but then generate multiple items + itemsPerList := b.itemsPerList + if itemsPerList == 0 { + itemsPerList = 10 // Default to 10 items + } + + cacheHit, ck, cachedItems, qErr, done := b.cache.Lookup(ctx, b.Name(), sdp.QueryMethod_LIST, scope, b.Type(), "", ignoreCache) + defer done() + if qErr != nil { + return nil, qErr + } + if cacheHit { + // If we have cached items, return them (they should already be 10 items from previous call) + return cachedItems, nil + } + + // Track the call + b.ListCalls = append(b.ListCalls, []string{scope}) + + switch scope { + case "empty": + err := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no items found", + Scope: scope, + } + b.cache.StoreError(ctx, err, b.DefaultCacheDuration(), ck) + return nil, err + case "error": + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Error for testing", + Scope: scope, + } + default: + // Generate exactly itemsPerList items + items := make([]*sdp.Item, 0, itemsPerList) + for i := range itemsPerList { + item := b.NewTestItem(scope, fmt.Sprintf("item-%d", i)) + items = append(items, item) + b.cache.StoreItem(ctx, item, b.DefaultCacheDuration(), ck) + } + return items, nil + } +} + +// generateBenchmarkGCPLikeAdapters creates adapters that mimic the GCP source structure +// but use BenchmarkListAdapter which returns 10 items per LIST query +func generateBenchmarkGCPLikeAdapters(projects, regions, zones, typesPerScope int) []Adapter { + projectTypes := typesPerScope / 3 + regionalTypes := typesPerScope / 3 + zonalTypes := typesPerScope / 3 + + totalAdapters := (projects * projectTypes) + + (projects * regions * regionalTypes) + + (projects * zones * zonalTypes) + + adapters := make([]Adapter, 0, totalAdapters) + + for p := range projects { + projectID := fmt.Sprintf("project-%d", p) + + // Project-level adapters + for t := range projectTypes { + adapters = append(adapters, &BenchmarkListAdapter{ + TestAdapter: TestAdapter{ + ReturnScopes: []string{projectID}, + ReturnType: fmt.Sprintf("gcp-project-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", projectID, t), + }, + itemsPerList: 10, + }) + } + + // Regional adapters + for r := range regions { + scope := fmt.Sprintf("%s.region-%d", projectID, r) + for t := range regionalTypes { + adapters = append(adapters, &BenchmarkListAdapter{ + TestAdapter: TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-regional-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }, + itemsPerList: 10, + }) + } + } + + // Zonal adapters + for z := range zones { + scope := fmt.Sprintf("%s.zone-%d", projectID, z) + for t := range zonalTypes { + adapters = append(adapters, &BenchmarkListAdapter{ + TestAdapter: TestAdapter{ + ReturnScopes: []string{scope}, + ReturnType: fmt.Sprintf("gcp-zonal-type-%d", t), + ReturnName: fmt.Sprintf("adapter-%s-type-%d", scope, t), + }, + itemsPerList: 10, + }) + } + } + } + + return adapters +} + +// newBenchmarkEngine creates an Engine for benchmarks without requiring NATS connection +// The execution pools are manually initialized so queries can be executed without Start() +func newBenchmarkEngine(adapters ...Adapter) (*Engine, error) { + ec := &EngineConfig{ + MaxParallelExecutions: 2000, + SourceName: "benchmark-engine", + NATSQueueName: "", + Unauthenticated: true, + // No NATSOptions - we don't need NATS for benchmarks + } + + e, err := NewEngine(ec) + if err != nil { + return nil, fmt.Errorf("error creating engine: %w", err) + } + + // Manually initialize execution pools (normally done in Start()) + // This allows us to use ExecuteQuery without connecting to NATS + e.listExecutionPool = pool.New().WithMaxGoroutines(ec.MaxParallelExecutions) + e.getExecutionPool = pool.New().WithMaxGoroutines(ec.MaxParallelExecutions) + + if err := e.AddAdapters(adapters...); err != nil { + return nil, fmt.Errorf("error adding adapters: %w", err) + } + + return e, nil +} + +// TestAddAdapters_LargeScale is a regular test (not benchmark) that validates +// the system can handle a realistic large-scale scenario +func TestAddAdapters_LargeScale(t *testing.T) { + if testing.Short() { + t.Skip("Skipping large-scale test in short mode") + } + + scenarios := []struct { + name string + projects int + regions int + zones int + types int + timeout time.Duration + }{ + {"23_projects", 23, 35, 135, 88, 30 * time.Second}, + {"100_projects", 100, 35, 135, 88, 5 * time.Minute}, + } + + for _, sc := range scenarios { + t.Run(sc.name, func(t *testing.T) { + adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) + sh := NewAdapterHost() + + t.Logf("Testing with %d adapters", len(adapters)) + + done := make(chan error, 1) + go func() { + done <- sh.AddAdapters(adapters...) + }() + + select { + case err := <-done: + if err != nil { + t.Fatalf("Failed to add adapters: %v", err) + } + t.Logf("Successfully added %d adapters", len(sh.Adapters())) + case <-time.After(sc.timeout): + t.Fatalf("AddAdapters timed out after %v (likely O(n²) issue)", sc.timeout) + } + }) + } +} + +// TestMemoryFootprint_EnterpriseScale measures actual memory usage at enterprise scale +// This provides accurate memory consumption data for capacity planning +func TestMemoryFootprint_EnterpriseScale(t *testing.T) { + if testing.Short() { + t.Skip("Skipping memory footprint test in short mode") + } + + scenarios := []struct { + name string + projects int + regions int + zones int + types int + }{ + {"23_projects", 23, 35, 135, 88}, + {"100_projects", 100, 35, 135, 88}, + {"500_projects", 500, 35, 135, 88}, + } + + for _, sc := range scenarios { + t.Run(sc.name, func(t *testing.T) { + // Force GC and get baseline + runtime.GC() + var m1 runtime.MemStats + runtime.ReadMemStats(&m1) + + // Create adapters + adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) + sh := NewAdapterHost() + err := sh.AddAdapters(adapters...) + if err != nil { + t.Fatal(err) + } + + // Get memory stats immediately (don't GC, we want to see actual usage) + var m2 runtime.MemStats + runtime.ReadMemStats(&m2) + + // Calculate memory used - use TotalAlloc which is monotonically increasing + totalAllocated := m2.TotalAlloc - m1.TotalAlloc + currentHeap := m2.HeapAlloc + memUsedMB := float64(totalAllocated) / (1024 * 1024) + heapUsedMB := float64(currentHeap) / (1024 * 1024) + bytesPerAdapter := float64(totalAllocated) / float64(len(adapters)) + sysMemMB := float64(m2.Sys) / (1024 * 1024) + + // Log detailed stats + t.Logf("=== Memory Footprint Analysis ===") + t.Logf("Adapters created: %d", len(adapters)) + t.Logf("Total allocated: %d bytes (%.2f MB)", totalAllocated, memUsedMB) + t.Logf("Current heap usage: %d bytes (%.2f MB)", currentHeap, heapUsedMB) + t.Logf("Bytes per adapter: %.2f", bytesPerAdapter) + t.Logf("Heap objects: %d", m2.HeapObjects) + t.Logf("System memory (from OS): %.2f MB", sysMemMB) + t.Logf("Number of GC cycles: %d", m2.NumGC-m1.NumGC) + + // Project memory usage for larger scales based on heap usage + if sc.projects == 500 { + mem1000 := (heapUsedMB / 500) * 1000 + mem5000 := (heapUsedMB / 500) * 5000 + t.Logf("\n=== Projected Heap Memory Usage ===") + t.Logf("1,000 projects: ~%.0f MB (~%.1f GB)", mem1000, mem1000/1024) + t.Logf("5,000 projects: ~%.0f MB (~%.1f GB)", mem5000, mem5000/1024) + } + }) + } +} + +// TestMemoryFootprint_WithListQueries measures memory usage when actually executing +// LIST queries against adapters, not just adding them. This simulates real-world +// usage where queries are executed and items are returned and cached. +// +// Memory Profiling: +// +// To generate memory profiles for analysis: +// +// 1. Generate memory profile: +// go test -run TestMemoryFootprint_WithListQueries/35_projects \ +// -memprofile=mem_35_projects.pprof ./discovery/... +// +// 2. Analyze the profile: +// go tool pprof mem_35_projects.pprof +// # Then use: top, list , web, etc. +// +// 3. Or use web UI: +// go tool pprof -http=:8080 mem_35_projects.pprof +// # Then open http://localhost:8080 in browser +// +// For heap profiles at specific points (after adapters, after queries): +// HEAP_PROFILE=heap go test -run TestMemoryFootprint_WithListQueries/35_projects -v ./discovery/... +func TestMemoryFootprint_WithListQueries(t *testing.T) { + if testing.Short() { + t.Skip("Skipping memory footprint test with list queries in short mode") + } + + scenarios := []struct { + name string + projects int + regions int + zones int + types int + timeout time.Duration + }{ + {"35_projects", 35, 35, 135, 88, 5 * time.Minute}, + } + + for _, sc := range scenarios { + t.Run(sc.name, func(t *testing.T) { + // Force GC and get baseline + runtime.GC() + var m1 runtime.MemStats + runtime.ReadMemStats(&m1) + + // Create adapters using BenchmarkListAdapter (returns 10 items per query) + adapters := generateBenchmarkGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) + engine, err := newBenchmarkEngine(adapters...) + if err != nil { + t.Fatalf("Failed to create engine: %v", err) + } + + // Get memory stats after adding adapters (before queries) + var m2 runtime.MemStats + runtime.ReadMemStats(&m2) + + // Write heap profile after adapters if requested + if heapProfile := os.Getenv("HEAP_PROFILE"); heapProfile != "" { + f, err := os.Create(fmt.Sprintf("%s_%s_%d_projects_after_adapters.pprof", heapProfile, sc.name, sc.projects)) + if err == nil { + defer f.Close() + runtime.GC() + if err := pprof.WriteHeapProfile(f); err != nil { + t.Logf("Failed to write heap profile: %v", err) + } else { + t.Logf("Heap profile (after adapters) written to: %s", f.Name()) + } + } + } + + // Execute LIST queries for each unique adapter type + // This will expand to all matching scopes via ExpandQuery + ctx, cancel := context.WithTimeout(context.Background(), sc.timeout) + defer cancel() + + // Collect unique adapter types + typeSet := make(map[string]bool) + for _, adapter := range adapters { + typeSet[adapter.Type()] = true + } + + // Execute LIST queries for each unique adapter type across all scopes + // This will expand to all matching scopes via ExpandQuery + totalItems := 0 + totalErrors := 0 + + // Execute one LIST query per adapter type (will expand to all scopes) + for adapterType := range typeSet { + query := &sdp.Query{ + Type: adapterType, + Scope: "*", // Wildcard to match all scopes + Method: sdp.QueryMethod_LIST, + } + + items, _, errs, err := engine.executeQuerySync(ctx, query) + if err != nil { + t.Logf("Query execution error for type %s: %v", adapterType, err) + } + + totalItems += len(items) + totalErrors += len(errs) + } + + // Get final memory stats after queries + var m3 runtime.MemStats + runtime.ReadMemStats(&m3) + + // Write heap profile if requested via environment variable + if heapProfile := os.Getenv("HEAP_PROFILE"); heapProfile != "" { + f, err := os.Create(fmt.Sprintf("%s_%s_%d_projects.pprof", heapProfile, sc.name, sc.projects)) + if err != nil { + t.Logf("Failed to create heap profile: %v", err) + } else { + defer f.Close() + runtime.GC() // Get accurate picture + if err := pprof.WriteHeapProfile(f); err != nil { + t.Logf("Failed to write heap profile: %v", err) + } else { + t.Logf("Heap profile written to: %s", f.Name()) + } + } + } + + // Calculate memory deltas + allocAfterAdapters := m2.TotalAlloc - m1.TotalAlloc + allocAfterQueries := m3.TotalAlloc - m2.TotalAlloc + totalAllocated := m3.TotalAlloc - m1.TotalAlloc + + heapAfterAdapters := m2.HeapAlloc + heapAfterQueries := m3.HeapAlloc + + // Convert to MB + allocAfterAdaptersMB := float64(allocAfterAdapters) / (1024 * 1024) + allocAfterQueriesMB := float64(allocAfterQueries) / (1024 * 1024) + totalAllocatedMB := float64(totalAllocated) / (1024 * 1024) + heapAfterAdaptersMB := float64(heapAfterAdapters) / (1024 * 1024) + heapAfterQueriesMB := float64(heapAfterQueries) / (1024 * 1024) + + // Calculate per-item and per-adapter metrics + bytesPerAdapter := float64(totalAllocated) / float64(len(adapters)) + bytesPerItem := float64(allocAfterQueries) / float64(totalItems) + bytesPerProject := float64(totalAllocated) / float64(sc.projects) + + // Log detailed stats + t.Logf("=== Memory Footprint Analysis with List Queries ===") + t.Logf("Adapters created: %d", len(adapters)) + t.Logf("Adapter types queried: %d", len(typeSet)) + t.Logf("Total items returned: %d", totalItems) + t.Logf("Total errors: %d", totalErrors) + t.Logf("\n=== Memory After Adding Adapters ===") + t.Logf("Total allocated: %d bytes (%.2f MB)", allocAfterAdapters, allocAfterAdaptersMB) + t.Logf("Heap usage: %d bytes (%.2f MB)", heapAfterAdapters, heapAfterAdaptersMB) + t.Logf("\n=== Memory After Executing Queries ===") + t.Logf("Additional allocated: %d bytes (%.2f MB)", allocAfterQueries, allocAfterQueriesMB) + t.Logf("Heap usage: %d bytes (%.2f MB)", heapAfterQueries, heapAfterQueriesMB) + t.Logf("\n=== Total Memory Usage ===") + t.Logf("Total allocated: %d bytes (%.2f MB)", totalAllocated, totalAllocatedMB) + t.Logf("Bytes per adapter: %.2f", bytesPerAdapter) + t.Logf("Bytes per item returned: %.2f", bytesPerItem) + t.Logf("Bytes per project: %.2f", bytesPerProject) + t.Logf("Heap objects: %d", m3.HeapObjects) + t.Logf("System memory (from OS): %.2f MB", float64(m3.Sys)/(1024*1024)) + t.Logf("Number of GC cycles: %d", m3.NumGC-m1.NumGC) + + // Project memory usage for larger scales + if sc.projects >= 100 { + mem1000 := (heapAfterQueriesMB / float64(sc.projects)) * 1000 + mem5000 := (heapAfterQueriesMB / float64(sc.projects)) * 5000 + t.Logf("\n=== Projected Heap Memory Usage (with queries) ===") + t.Logf("1,000 projects: ~%.0f MB (~%.1f GB)", mem1000, mem1000/1024) + t.Logf("5,000 projects: ~%.0f MB (~%.1f GB)", mem5000, mem5000/1024) + } + }) + } +} + +// BenchmarkMemoryFootprint_WithStats measures memory with runtime.MemStats +func BenchmarkMemoryFootprint_WithStats(b *testing.B) { + scenarios := []struct { + name string + projects int + regions int + zones int + types int + }{ + {"Small_23proj", 23, 35, 135, 88}, + {"Medium_100proj", 100, 35, 135, 88}, + {"Large_500proj", 500, 35, 135, 88}, + } + + for _, sc := range scenarios { + b.Run(sc.name, func(b *testing.B) { + for range b.N { + b.StopTimer() + + // Get baseline memory + runtime.GC() + var m1 runtime.MemStats + runtime.ReadMemStats(&m1) + + b.StartTimer() + + // Create and add adapters + adapters := generateGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) + sh := NewAdapterHost() + err := sh.AddAdapters(adapters...) + + b.StopTimer() + + if err != nil { + b.Fatal(err) + } + + // Measure final memory (no GC to see actual usage) + var m2 runtime.MemStats + runtime.ReadMemStats(&m2) + + totalAllocated := m2.TotalAlloc - m1.TotalAlloc + heapUsed := m2.HeapAlloc + memUsedMB := float64(totalAllocated) / (1024 * 1024) + heapUsedMB := float64(heapUsed) / (1024 * 1024) + + b.ReportMetric(float64(len(adapters)), "adapters") + b.ReportMetric(memUsedMB, "total_alloc_MB") + b.ReportMetric(heapUsedMB, "heap_MB") + b.ReportMetric(float64(totalAllocated)/float64(len(adapters)), "bytes/adapter") + b.ReportMetric(float64(m2.HeapObjects), "heap_objects") + b.ReportMetric(float64(m2.Sys)/(1024*1024), "sys_memory_MB") + } + }) + } +} + +// BenchmarkMemoryFootprint_WithListQueries measures memory usage when executing +// LIST queries against adapters that return 10 items each. This provides realistic +// memory consumption data for capacity planning when queries are actually executed. +func BenchmarkMemoryFootprint_WithListQueries(b *testing.B) { + scenarios := []struct { + name string + projects int + regions int + zones int + types int + }{ + {"Medium_35proj", 35, 35, 135, 88}, + } + + for _, sc := range scenarios { + b.Run(sc.name, func(b *testing.B) { + for range b.N { + b.StopTimer() + + // Get baseline memory + runtime.GC() + var m1 runtime.MemStats + runtime.ReadMemStats(&m1) + + // Create adapters using BenchmarkListAdapter (returns 10 items per query) + adapters := generateBenchmarkGCPLikeAdapters(sc.projects, sc.regions, sc.zones, sc.types) + engine, err := newBenchmarkEngine(adapters...) + if err != nil { + b.Fatalf("Failed to create engine: %v", err) + } + + // Get memory after adding adapters + var m2 runtime.MemStats + runtime.ReadMemStats(&m2) + + b.StartTimer() + + // Execute LIST queries for each unique adapter type + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Collect unique adapter types + typeSet := make(map[string]bool) + for _, adapter := range adapters { + typeSet[adapter.Type()] = true + } + + totalItems := 0 + for adapterType := range typeSet { + query := &sdp.Query{ + Type: adapterType, + Scope: "*", // Wildcard to match all scopes + Method: sdp.QueryMethod_LIST, + } + + items, _, _, err := engine.executeQuerySync(ctx, query) + if err != nil { + // Log but don't fail - some queries might timeout in benchmarks + b.Logf("Query execution error for type %s: %v", adapterType, err) + } + totalItems += len(items) + } + + b.StopTimer() + + // Measure final memory after queries + var m3 runtime.MemStats + runtime.ReadMemStats(&m3) + + allocAfterAdapters := m2.TotalAlloc - m1.TotalAlloc + allocAfterQueries := m3.TotalAlloc - m2.TotalAlloc + totalAllocated := m3.TotalAlloc - m1.TotalAlloc + heapAfterQueries := m3.HeapAlloc + + allocAfterAdaptersMB := float64(allocAfterAdapters) / (1024 * 1024) + allocAfterQueriesMB := float64(allocAfterQueries) / (1024 * 1024) + totalAllocatedMB := float64(totalAllocated) / (1024 * 1024) + heapAfterQueriesMB := float64(heapAfterQueries) / (1024 * 1024) + + b.ReportMetric(float64(len(adapters)), "adapters") + b.ReportMetric(float64(totalItems), "items_returned") + b.ReportMetric(allocAfterAdaptersMB, "alloc_after_adapters_MB") + b.ReportMetric(allocAfterQueriesMB, "alloc_after_queries_MB") + b.ReportMetric(totalAllocatedMB, "total_alloc_MB") + b.ReportMetric(heapAfterQueriesMB, "heap_MB") + b.ReportMetric(float64(totalAllocated)/float64(len(adapters)), "bytes/adapter") + b.ReportMetric(float64(allocAfterQueries)/float64(totalItems), "bytes/item") + b.ReportMetric(float64(m3.HeapObjects), "heap_objects") + b.ReportMetric(float64(m3.Sys)/(1024*1024), "sys_memory_MB") + } + }) + } +} diff --git a/go/discovery/adapterhost_test.go b/go/discovery/adapterhost_test.go new file mode 100644 index 00000000..74199b99 --- /dev/null +++ b/go/discovery/adapterhost_test.go @@ -0,0 +1,362 @@ +package discovery + +import ( + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestAdapterHostExpandQuery(t *testing.T) { + sh := NewAdapterHost() + + err := sh.AddAdapters( + &TestAdapter{ + ReturnScopes: []string{"test"}, + ReturnType: "person", + ReturnName: "person", + }, + &TestAdapter{ + ReturnScopes: []string{"test"}, + ReturnType: "fish", + ReturnName: "fish", + }, + &TestAdapter{ + ReturnScopes: []string{ + "multiA", + "multiB", + }, + ReturnType: "chair", + ReturnName: "chair", + }, + &TestAdapter{ + ReturnScopes: []string{"test"}, + ReturnType: "hidden_person", + IsHidden: true, + ReturnName: "hidden_person", + }, + ) + if err != nil { + t.Fatal(err) + } + + t.Run("Right type wrong scope", func(t *testing.T) { + req := sdp.Query{ + Type: "person", + Scope: "wrong", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 0 { + t.Fatalf("Expected 0 queries, got %v", len(m)) + } + }) + + t.Run("Right scope wrong type", func(t *testing.T) { + req := sdp.Query{ + Type: "wrong", + Scope: "test", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 0 { + t.Fatalf("Expected 0 queries, got %v", len(m)) + } + }) + + t.Run("Right both", func(t *testing.T) { + req := sdp.Query{ + Type: "person", + Scope: "test", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 1 { + t.Fatalf("Expected 1 query, got %v", len(m)) + } + }) + + t.Run("Multi-scope", func(t *testing.T) { + req := sdp.Query{ + Type: "chair", + Scope: "multiB", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 1 { + t.Fatalf("Expected 1 query, got %v", len(m)) + } + }) + + t.Run("Wildcard scope", func(t *testing.T) { + req := sdp.Query{ + Type: "person", + Scope: sdp.WILDCARD, + } + + m := sh.ExpandQuery(&req) + + if len(m) != 1 { + t.Fatalf("Expected 1 query, got %v", len(m)) + } + + req = sdp.Query{ + Type: "chair", + Scope: sdp.WILDCARD, + } + + m = sh.ExpandQuery(&req) + + if len(m) != 2 { + t.Fatalf("Expected 2 queries, got %v", len(m)) + } + }) + + t.Run("Wildcard type", func(t *testing.T) { + req := sdp.Query{ + Type: sdp.WILDCARD, + Scope: "test", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 2 { + t.Fatalf("Expected 2 adapters, got %v", len(m)) + } + }) + + t.Run("Wildcard both", func(t *testing.T) { + req := sdp.Query{ + Type: sdp.WILDCARD, + Scope: sdp.WILDCARD, + } + + m := sh.ExpandQuery(&req) + + if len(m) != 4 { + t.Fatalf("Expected 4 adapters, got %v", len(m)) + } + }) + + t.Run("substring match", func(t *testing.T) { + req := sdp.Query{ + Type: sdp.WILDCARD, + Scope: "multi", + } + + m := sh.ExpandQuery(&req) + + if len(m) != 2 { + t.Fatalf("Expected 2 queries, got %v", len(m)) + } + }) + + t.Run("Listing hidden adapter with wildcard scope", func(t *testing.T) { + req := sdp.Query{ + Type: "hidden_person", + Scope: sdp.WILDCARD, + } + if x := len(sh.ExpandQuery(&req)); x != 0 { + t.Errorf("expected to find 0 adapters, found %v", x) + } + + req = sdp.Query{ + Type: "hidden_person", + Scope: "test", + } + if x := len(sh.ExpandQuery(&req)); x != 1 { + t.Errorf("expected to find 1 adapter, found %v", x) + } + }) +} + +func TestAdapterHostAddAdapters(t *testing.T) { + sh := NewAdapterHost() + + adapter := TestAdapter{} + + err := sh.AddAdapters(&adapter) + if err != nil { + t.Fatal(err) + } + + if x := len(sh.Adapters()); x != 1 { + t.Fatalf("Expected 1 adapters, got %v", x) + } +} + +func TestAdapterHostExpandQuery_WildcardScope(t *testing.T) { + sh := NewAdapterHost() + + // Add regular adapter without wildcard support + regularAdapter := &TestAdapter{ + ReturnScopes: []string{"project.zone-a", "project.zone-b"}, + ReturnType: "regular-type", + ReturnName: "regular", + } + + // Add wildcard-supporting adapter + wildcardAdapter := &TestWildcardAdapter{ + TestAdapter: TestAdapter{ + ReturnScopes: []string{"project.zone-a", "project.zone-b"}, + ReturnType: "wildcard-type", + ReturnName: "wildcard", + }, + supportsWildcard: true, + } + + err := sh.AddAdapters(regularAdapter, wildcardAdapter) + if err != nil { + t.Fatal(err) + } + + t.Run("Regular adapter with wildcard scope expands to all scopes", func(t *testing.T) { + req := sdp.Query{ + Type: "regular-type", + Scope: sdp.WILDCARD, + } + + expanded := sh.ExpandQuery(&req) + + // Should expand to 2 queries (one per zone) + if len(expanded) != 2 { + t.Fatalf("Expected 2 expanded queries for regular adapter, got %v", len(expanded)) + } + + // Check that scopes are specific, not wildcard + for q := range expanded { + if q.GetScope() == sdp.WILDCARD { + t.Errorf("Expected specific scope, got wildcard") + } + } + }) + + t.Run("Wildcard-supporting adapter with wildcard scope does not expand for LIST", func(t *testing.T) { + req := sdp.Query{ + Type: "wildcard-type", + Method: sdp.QueryMethod_LIST, + Scope: sdp.WILDCARD, + } + + expanded := sh.ExpandQuery(&req) + + // Should NOT expand - just 1 query with wildcard scope + if len(expanded) != 1 { + t.Fatalf("Expected 1 query for wildcard adapter, got %v", len(expanded)) + } + + // Check that scope is still wildcard + for q := range expanded { + if q.GetScope() != sdp.WILDCARD { + t.Errorf("Expected wildcard scope to be preserved, got %v", q.GetScope()) + } + } + }) + + t.Run("Wildcard-supporting adapter with wildcard scope expands for GET", func(t *testing.T) { + req := sdp.Query{ + Type: "wildcard-type", + Method: sdp.QueryMethod_GET, + Scope: sdp.WILDCARD, + } + + expanded := sh.ExpandQuery(&req) + + // Should expand to 2 queries (one per scope) for GET + if len(expanded) != 2 { + t.Fatalf("Expected 2 expanded queries for wildcard adapter with GET, got %v", len(expanded)) + } + + // Check that scopes are specific, not wildcard + for q := range expanded { + if q.GetScope() == sdp.WILDCARD { + t.Errorf("Expected specific scope for GET, got wildcard") + } + } + }) + + t.Run("Wildcard-supporting adapter with wildcard scope expands for SEARCH", func(t *testing.T) { + req := sdp.Query{ + Type: "wildcard-type", + Method: sdp.QueryMethod_SEARCH, + Scope: sdp.WILDCARD, + } + + expanded := sh.ExpandQuery(&req) + + // Should expand to 2 queries (one per scope) for SEARCH + if len(expanded) != 2 { + t.Fatalf("Expected 2 expanded queries for wildcard adapter with SEARCH, got %v", len(expanded)) + } + + // Check that scopes are specific, not wildcard + for q := range expanded { + if q.GetScope() == sdp.WILDCARD { + t.Errorf("Expected specific scope for SEARCH, got wildcard") + } + } + }) + + t.Run("Wildcard-supporting adapter with specific scope works normally", func(t *testing.T) { + req := sdp.Query{ + Type: "wildcard-type", + Scope: "project.zone-a", + } + + expanded := sh.ExpandQuery(&req) + + // Should return 1 query with specific scope + if len(expanded) != 1 { + t.Fatalf("Expected 1 query, got %v", len(expanded)) + } + + for q := range expanded { + if q.GetScope() != "project.zone-a" { + t.Errorf("Expected scope 'project.zone-a', got %v", q.GetScope()) + } + } + }) + + t.Run("Hidden wildcard adapter with wildcard scope is not included", func(t *testing.T) { + hiddenWildcardAdapter := &TestWildcardAdapter{ + TestAdapter: TestAdapter{ + ReturnScopes: []string{"project.zone-a"}, + ReturnType: "hidden-wildcard-type", + ReturnName: "hidden-wildcard", + IsHidden: true, + }, + supportsWildcard: true, + } + + err := sh.AddAdapters(hiddenWildcardAdapter) + if err != nil { + t.Fatal(err) + } + + req := sdp.Query{ + Type: "hidden-wildcard-type", + Scope: sdp.WILDCARD, + } + + expanded := sh.ExpandQuery(&req) + + // Hidden adapters should not be expanded for wildcard scopes + if len(expanded) != 0 { + t.Fatalf("Expected 0 queries for hidden wildcard adapter, got %v", len(expanded)) + } + }) +} + +// TestWildcardAdapter extends TestAdapter to implement WildcardScopeAdapter +type TestWildcardAdapter struct { + TestAdapter + supportsWildcard bool +} + +// SupportsWildcardScope implements the WildcardScopeAdapter interface +func (t *TestWildcardAdapter) SupportsWildcardScope() bool { + return t.supportsWildcard +} diff --git a/go/discovery/cmd.go b/go/discovery/cmd.go new file mode 100644 index 00000000..c2b4fe7f --- /dev/null +++ b/go/discovery/cmd.go @@ -0,0 +1,326 @@ +package discovery + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "runtime" + "time" + + "github.com/getsentry/sentry-go" + "github.com/google/uuid" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "golang.org/x/oauth2" +) + +const defaultApp = "https://app.overmind.tech" + +func AddEngineFlags(command *cobra.Command) { + command.PersistentFlags().String("source-name", "", "The name of the source") + cobra.CheckErr(viper.BindEnv("source-name", "SOURCE_NAME")) + command.PersistentFlags().String("source-uuid", "", "The UUID of the source, is this is blank it will be auto-generated. This is used in heartbeats and shouldn't be supplied usually") + cobra.CheckErr(viper.BindEnv("source-uuid", "SOURCE_UUID")) + command.PersistentFlags().String("source-access-token", "", "The access token to use to authenticate the source for managed sources") + cobra.CheckErr(viper.BindEnv("source-access-token", "SOURCE_ACCESS_TOKEN")) + command.PersistentFlags().String("source-access-token-type", "", "The type of token to use to authenticate the source for managed sources") + cobra.CheckErr(viper.BindEnv("source-access-token-type", "SOURCE_ACCESS_TOKEN_TYPE")) + + command.PersistentFlags().String("api-server-service-host", "", "The host of the API server service, only if the source is managed by Overmind") + cobra.CheckErr(viper.BindEnv("api-server-service-host", "API_SERVER_SERVICE_HOST")) + command.PersistentFlags().String("api-server-service-port", "", "The port of the API server service, only if the source is managed by Overmind") + cobra.CheckErr(viper.BindEnv("api-server-service-port", "API_SERVER_SERVICE_PORT")) + command.PersistentFlags().String("nats-service-host", "", "The host of the NATS service, only if the source is managed by Overmind") + cobra.CheckErr(viper.BindEnv("nats-service-host", "NATS_SERVICE_HOST")) + command.PersistentFlags().String("nats-service-port", "", "The port of the NATS service, only if the source is managed by Overmind") + cobra.CheckErr(viper.BindEnv("nats-service-port", "NATS_SERVICE_PORT")) + + command.PersistentFlags().Bool("overmind-managed-source", false, "If you are running the source yourself or if it is managed by Overmind") + cobra.CheckErr(command.PersistentFlags().MarkHidden("overmind-managed-source")) + cobra.CheckErr(viper.BindEnv("overmind-managed-source", "OVERMIND_MANAGED_SOURCE")) + + command.PersistentFlags().String("app", defaultApp, "The URL of the Overmind app to use") + cobra.CheckErr(viper.BindEnv("app", "APP")) + command.PersistentFlags().String("api-key", "", "The API key to use to authenticate to the Overmind API") + cobra.CheckErr(viper.BindEnv("api-key", "OVM_API_KEY", "API_KEY")) + + command.PersistentFlags().String("nats-connection-name", "", "The name that the source should use to connect to NATS") + cobra.CheckErr(viper.BindEnv("nats-connection-name", "NATS_CONNECTION_NAME")) + command.PersistentFlags().Int("nats-connection-timeout", 10, "The timeout for connecting to NATS") + cobra.CheckErr(viper.BindEnv("nats-connection-timeout", "NATS_CONNECTION_TIMEOUT")) + + command.PersistentFlags().Int("max-parallel", 0, "The maximum number of parallel executions") + cobra.CheckErr(viper.BindEnv("max-parallel", "MAX_PARALLEL")) +} + +func EngineConfigFromViper(engineType, version string) (*EngineConfig, error) { + var sourceName string + hostname, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("error getting hostname: %w", err) + } + + if viper.GetString("source-name") == "" { + sourceName = fmt.Sprintf("%s-%s", engineType, hostname) + } else { + sourceName = viper.GetString("source-name") + } + + sourceUUIDString := viper.GetString("source-uuid") + var sourceUUID uuid.UUID + if sourceUUIDString == "" { + sourceUUID = uuid.New() + } else { + var err error + sourceUUID, err = uuid.Parse(sourceUUIDString) + if err != nil { + return nil, fmt.Errorf("error parsing source-uuid: %w", err) + } + } + + var managedSource sdp.SourceManaged + if viper.GetBool("overmind-managed-source") { + managedSource = sdp.SourceManaged_MANAGED + } else { + managedSource = sdp.SourceManaged_LOCAL + } + + var apiServerURL string + var natsServerURL string + appURL := viper.GetString("app") + if managedSource == sdp.SourceManaged_MANAGED { + apiServerHost := viper.GetString("api-server-service-host") + apiServerPort := viper.GetString("api-server-service-port") + if apiServerHost == "" || apiServerPort == "" { + return nil, errors.New("API_SERVER_SERVICE_HOST and API_SERVER_SERVICE_PORT (provided by k8s) must be set for managed sources") + } + apiServerURL = net.JoinHostPort(apiServerHost, apiServerPort) + if apiServerPort == "443" { + apiServerURL = "https://" + apiServerURL + } else { + apiServerURL = "http://" + apiServerURL + } + + natsServerHost := viper.GetString("nats-service-host") + natsServerPort := viper.GetString("nats-service-port") + if natsServerHost == "" || natsServerPort == "" { + return nil, errors.New("NATS_SERVICE_HOST and NATS_SERVICE_PORT (provided by k8s) must be set for managed sources") + } + natsServerURL = net.JoinHostPort(natsServerHost, natsServerPort) + // default to websocket if the port is 443; this is to allow GCP sources + // to connect to NATS from outside the EKS cluster + if natsServerPort == "443" { + natsServerURL = "wss://" + natsServerURL + } else { + natsServerURL = "nats://" + natsServerURL + } + } else { + // look up the api server url from the app url + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + oi, err := sdp.NewOvermindInstance(ctx, appURL) + if err != nil { + err = fmt.Errorf("Could not determine Overmind instance URLs from app URL %s: %w", appURL, err) + return nil, err + } + apiServerURL = oi.ApiUrl.String() + natsServerURL = oi.NatsUrl.String() + } + + // setup natsOptions + var natsConnectionName string + if viper.GetString("nats-connection-name") == "" { + natsConnectionName = hostname + } + natsOptions := auth.NATSOptions{ + NumRetries: -1, + RetryDelay: 5 * time.Second, + Servers: []string{natsServerURL}, + ConnectionName: natsConnectionName, + ConnectionTimeout: time.Duration(viper.GetInt("nats-connection-timeout")) * time.Second, + MaxReconnects: -1, + ReconnectWait: 1 * time.Second, + ReconnectJitter: 1 * time.Second, + } + + allow := os.Getenv("ALLOW_UNAUTHENTICATED") + allowUnauthenticated := allow == "true" + + // order of precedence is: + // unauthenticated overrides everything # used for local development + // if managed source, we expect a token + // if local source, we expect an api key + + if allowUnauthenticated { + log.Warn("Using unauthenticated mode as ALLOW_UNAUTHENTICATED is set") + } else { + if viper.GetBool("overmind-managed-source") { + log.Info("Running source in managed mode") + // If managed source, we expect a token + if viper.GetString("source-access-token") == "" { + return nil, errors.New("source-access-token must be set for managed sources") + } + } else if viper.GetString("api-key") == "" { + return nil, errors.New("api-key must be set for local sources") + } + } + + maxParallelExecutions := viper.GetInt("max-parallel") + if maxParallelExecutions == 0 { + maxParallelExecutions = runtime.NumCPU() * 100 // we expect most source interactions to be waiting on external services, so adding more parallelism can help + } + + return &EngineConfig{ + EngineType: engineType, + Version: version, + SourceName: sourceName, + SourceUUID: sourceUUID, + OvermindManagedSource: managedSource, + SourceAccessToken: viper.GetString("source-access-token"), + SourceAccessTokenType: viper.GetString("source-access-token-type"), + App: appURL, + APIServerURL: apiServerURL, + ApiKey: viper.GetString("api-key"), + NATSOptions: &natsOptions, + Unauthenticated: allowUnauthenticated, + MaxParallelExecutions: maxParallelExecutions, + }, nil +} + +// MapFromEngineConfig Returns the config as a map +func MapFromEngineConfig(ec *EngineConfig) map[string]any { + var apiKeyClientSecret string + if ec.ApiKey != "" { + apiKeyClientSecret = "[REDACTED]" + } + var sourceAccessToken string + if ec.SourceAccessToken != "" { + sourceAccessToken = "[REDACTED]" + } + + return map[string]interface{}{ + "engine-type": ec.EngineType, + "version": ec.Version, + "source-name": ec.SourceName, + "source-uuid": ec.SourceUUID, + "source-access-token": sourceAccessToken, + "source-access-token-type": ec.SourceAccessTokenType, + "managed-source": ec.OvermindManagedSource, + "app": ec.App, + "api-key": apiKeyClientSecret, + "api-server-url": ec.APIServerURL, + "max-parallel-executions": ec.MaxParallelExecutions, + "nats-servers": ec.NATSOptions.Servers, + "nats-connection-name": ec.NATSOptions.ConnectionName, + "nats-connection-timeout": ec.NATSConnectionTimeout, + "nats-queue-name": ec.NATSQueueName, + "unauthenticated": ec.Unauthenticated, + } +} + +// CreateClients sets up NATS TokenClient and HeartbeatOptions.ManagementClient from config. +// Each client is only created if not already set (idempotent), so callers like the CLI +// can pre-configure clients without them being overwritten. +func (ec *EngineConfig) CreateClients() error { + // If we are running in unauthenticated mode then do nothing here + if ec.Unauthenticated { + log.Warn("Using unauthenticated NATS as ALLOW_UNAUTHENTICATED is set") + if ec.NATSOptions != nil { + log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") + } + return nil + } + + // If both clients are already configured (e.g. CLI), skip entirely + if ec.NATSOptions != nil && ec.NATSOptions.TokenClient != nil && + ec.HeartbeatOptions != nil && ec.HeartbeatOptions.ManagementClient != nil { + return nil + } + + switch ec.OvermindManagedSource { + case sdp.SourceManaged_LOCAL: + log.Info("Using API Key for authentication, heartbeats will be sent") + + if ec.NATSOptions != nil && ec.NATSOptions.TokenClient == nil { + tokenClient, err := auth.NewAPIKeyClient(ec.APIServerURL, ec.ApiKey) + if err != nil { + return fmt.Errorf("error creating API key client: %w", err) + } + ec.NATSOptions.TokenClient = tokenClient + } + + if ec.HeartbeatOptions == nil { + ec.HeartbeatOptions = &HeartbeatOptions{} + } + if ec.HeartbeatOptions.ManagementClient == nil { + tokenSource := auth.NewAPIKeyTokenSource(ec.ApiKey, ec.APIServerURL) + transport := oauth2.Transport{ + Source: tokenSource, + Base: http.DefaultTransport, + } + authenticatedClient := http.Client{ + Transport: otelhttp.NewTransport(&transport), + } + ec.HeartbeatOptions.ManagementClient = sdpconnect.NewManagementServiceClient( + &authenticatedClient, + ec.APIServerURL, + ) + ec.HeartbeatOptions.Frequency = time.Second * 30 + } + + if ec.NATSOptions != nil { + log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") + } + return nil + case sdp.SourceManaged_MANAGED: + log.Info("Using static token for authentication, heartbeats will be sent") + + if ec.NATSOptions != nil && ec.NATSOptions.TokenClient == nil { + tokenClient, err := auth.NewStaticTokenClient(ec.APIServerURL, ec.SourceAccessToken, ec.SourceAccessTokenType) + if err != nil { + err = fmt.Errorf("error creating static token client: %w", err) + sentry.CaptureException(err) + return err + } + ec.NATSOptions.TokenClient = tokenClient + } + + if ec.HeartbeatOptions == nil { + ec.HeartbeatOptions = &HeartbeatOptions{} + } + if ec.HeartbeatOptions.ManagementClient == nil { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: ec.SourceAccessToken, + TokenType: ec.SourceAccessTokenType, + }) + transport := oauth2.Transport{ + Source: tokenSource, + Base: http.DefaultTransport, + } + authenticatedClient := http.Client{ + Transport: otelhttp.NewTransport(&transport), + } + ec.HeartbeatOptions.ManagementClient = sdpconnect.NewManagementServiceClient( + &authenticatedClient, + ec.APIServerURL, + ) + ec.HeartbeatOptions.Frequency = time.Second * 30 + } + + if ec.NATSOptions != nil { + log.WithFields(MapFromEngineConfig(ec)).Info("Engine config") + } + return nil + } + + err := fmt.Errorf("unable to setup authentication. Please check your configuration %v", ec) + return err +} diff --git a/go/discovery/cmd_test.go b/go/discovery/cmd_test.go new file mode 100644 index 00000000..1bbc1b0a --- /dev/null +++ b/go/discovery/cmd_test.go @@ -0,0 +1,235 @@ +package discovery + +import ( + "os" + "runtime" + "testing" + + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// NB we do not call AddEngineFlags so we use command line flags, not environment variables +func TestEngineConfigFromViper(t *testing.T) { + tests := []struct { + name string + setupViper func() + engineType string + version string + expectedSourceName string + expectedSourceUUID uuid.UUID + expectedSourceAccessToken string + expectedSourceAccessTokenType string + expectedManagedSource sdp.SourceManaged + expectedApp string + expectedApiServerURL string + expectedApiKey string + expectedNATSUrl string + expectedMaxParallel int + expectUnauthenticated bool + expectError bool + }{ + { + name: "default values", + setupViper: func() { + viper.Set("app", "https://app.overmind.tech") + viper.Set("api-key", "api-key") + }, + engineType: "test-engine", + version: "1.0", + expectedSourceName: "test-engine-" + getHostname(t), + expectedSourceUUID: uuid.Nil, + expectedSourceAccessToken: "", + expectedSourceAccessTokenType: "", + expectedManagedSource: sdp.SourceManaged_LOCAL, + expectedApp: "https://app.overmind.tech", + expectedApiServerURL: "https://api.app.overmind.tech", + expectedNATSUrl: "wss://messages.app.overmind.tech", + expectedApiKey: "api-key", + expectedMaxParallel: runtime.NumCPU() * 100, + expectError: false, + }, + { + name: "custom values", + setupViper: func() { + viper.Set("source-name", "custom-source") + viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") + viper.Set("app", "https://df.overmind-demo.com/") + viper.Set("api-key", "custom-api-key") + viper.Set("max-parallel", 10) + }, + engineType: "test-engine", + version: "1.0", + expectedSourceName: "custom-source", + expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), + expectedSourceAccessToken: "", + expectedSourceAccessTokenType: "", + expectedManagedSource: sdp.SourceManaged_LOCAL, + expectedApp: "https://df.overmind-demo.com/", + expectedApiServerURL: "https://api.df.overmind-demo.com", + expectedNATSUrl: "wss://messages.df.overmind-demo.com", + expectedApiKey: "custom-api-key", + expectedMaxParallel: 10, + expectError: false, + }, + { + name: "invalid UUID", + setupViper: func() { + viper.Set("source-uuid", "invalid-uuid") + }, + engineType: "test-engine", + version: "1.0", + expectError: true, + }, + { + name: "managed source - nats", + setupViper: func() { + viper.Set("source-name", "custom-source") + viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") + viper.Set("source-access-token", "custom-access-token") + viper.Set("source-access-token-type", "custom-token-type") + viper.Set("overmind-managed-source", true) + viper.Set("max-parallel", 10) + + viper.Set("api-server-service-host", "api.app.overmind.tech") + viper.Set("api-server-service-port", "443") + viper.Set("nats-service-host", "messages.app.overmind.tech") + viper.Set("nats-service-port", "4222") + }, + engineType: "test-engine", + version: "1.0", + expectedSourceName: "custom-source", + expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), + expectedSourceAccessToken: "custom-access-token", + expectedSourceAccessTokenType: "custom-token-type", + expectedManagedSource: sdp.SourceManaged_MANAGED, + + expectedApiServerURL: "https://api.app.overmind.tech:443", + expectedNATSUrl: "nats://messages.app.overmind.tech:4222", + expectedMaxParallel: 10, + expectError: false, + }, + { + name: "managed source - wss", + setupViper: func() { + viper.Set("source-name", "custom-source") + viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") + viper.Set("source-access-token", "custom-access-token") + viper.Set("source-access-token-type", "custom-token-type") + viper.Set("overmind-managed-source", true) + viper.Set("max-parallel", 10) + + viper.Set("api-server-service-host", "api.app.overmind.tech") + viper.Set("api-server-service-port", "443") + viper.Set("nats-service-host", "messages.app.overmind.tech") + viper.Set("nats-service-port", "443") + }, + engineType: "test-engine", + version: "1.0", + expectedSourceName: "custom-source", + expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), + expectedSourceAccessToken: "custom-access-token", + expectedSourceAccessTokenType: "custom-token-type", + expectedManagedSource: sdp.SourceManaged_MANAGED, + + expectedApiServerURL: "https://api.app.overmind.tech:443", + expectedNATSUrl: "wss://messages.app.overmind.tech:443", + expectedMaxParallel: 10, + expectError: false, + }, + { + name: "managed source local insecure", + setupViper: func() { + viper.Set("source-name", "custom-source") + viper.Set("source-uuid", "123e4567-e89b-12d3-a456-426614174000") + viper.Set("source-access-token", "custom-access-token") + viper.Set("source-access-token-type", "custom-token-type") + viper.Set("overmind-managed-source", true) + viper.Set("max-parallel", 10) + + viper.Set("api-server-service-host", "localhost") + viper.Set("api-server-service-port", "8080") + viper.Set("nats-service-host", "localhost") + viper.Set("nats-service-port", "4222") + }, + engineType: "test-engine", + version: "1.0", + expectedSourceName: "custom-source", + expectedSourceUUID: uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"), + expectedSourceAccessToken: "custom-access-token", + expectedSourceAccessTokenType: "custom-token-type", + expectedManagedSource: sdp.SourceManaged_MANAGED, + + expectedApiServerURL: "http://localhost:8080", + expectedNATSUrl: "nats://localhost:4222", + expectedMaxParallel: 10, + expectError: false, + }, + { + name: "source access token and api key not set", + setupViper: func() {}, + engineType: "test-engine", + version: "1.0", + expectError: true, + }, + { + name: "fully unauthenticated", + setupViper: func() { + viper.Set("app", "https://app.overmind.tech") + viper.Set("source-name", "custom-source") + t.Setenv("ALLOW_UNAUTHENTICATED", "true") + }, + engineType: "test-engine", + version: "1.0", + expectError: false, + expectedMaxParallel: runtime.NumCPU() * 100, + expectedSourceName: "custom-source", + expectedApp: "https://app.overmind.tech", + expectedApiServerURL: "https://api.app.overmind.tech", + expectedNATSUrl: "wss://messages.app.overmind.tech", + expectUnauthenticated: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("ALLOW_UNAUTHENTICATED", "") + viper.Reset() + tt.setupViper() + engineConfig, err := EngineConfigFromViper(tt.engineType, tt.version) + if tt.expectError { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.engineType, engineConfig.EngineType) + assert.Equal(t, tt.version, engineConfig.Version) + assert.Equal(t, tt.expectedSourceName, engineConfig.SourceName) + if tt.expectedSourceUUID == uuid.Nil { + assert.NotEqual(t, uuid.Nil, engineConfig.SourceUUID) + } else { + assert.Equal(t, tt.expectedSourceUUID, engineConfig.SourceUUID) + } + assert.Equal(t, tt.expectedSourceAccessToken, engineConfig.SourceAccessToken) + assert.Equal(t, tt.expectedSourceAccessTokenType, engineConfig.SourceAccessTokenType) + assert.Equal(t, tt.expectedManagedSource, engineConfig.OvermindManagedSource) + assert.Equal(t, tt.expectedApp, engineConfig.App) + assert.Equal(t, tt.expectedApiServerURL, engineConfig.APIServerURL) + assert.Equal(t, tt.expectedNATSUrl, engineConfig.NATSOptions.Servers[0]) + assert.Equal(t, tt.expectedApiKey, engineConfig.ApiKey) + assert.Equal(t, tt.expectedMaxParallel, engineConfig.MaxParallelExecutions) + assert.Equal(t, tt.expectUnauthenticated, engineConfig.Unauthenticated) + } + }) + } +} + +func getHostname(t *testing.T) string { + hostname, err := os.Hostname() + if err != nil { + t.Fatalf("error getting hostname: %v", err) + } + return hostname +} diff --git a/go/discovery/doc.go b/go/discovery/doc.go new file mode 100644 index 00000000..ab24ca1e --- /dev/null +++ b/go/discovery/doc.go @@ -0,0 +1,33 @@ +// Package discovery provides the engine and protocol types for Overmind sources. +// Sources discover infrastructure (AWS, K8s, GCP, etc.) and respond to queries via NATS. +// +// # Startup sequence for source authors +// +// Sources should follow this canonical flow so that health probes and heartbeats +// work even when adapter initialization fails (avoiding CrashLoopBackOff): +// +// 1. EngineConfigFromViper(engineType, version) — fail: return/exit +// 2. NewEngine(engineConfig) — fail: return/exit (includes CreateClients internally) +// 3. ServeHealthProbes(port) +// 4. Start(ctx) — fail: return/exit (NATS connection required) +// 5. Validate source config — permanent config errors: SetInitError(err), then idle +// 6. Adapter init — use InitialiseAdapters (blocks until success or ctx cancelled) for retryable init, or SetInitError for single-attempt +// 7. Wait for SIGTERM, then Stop() +// +// # Error handling +// +// Fatal errors (caller must return or exit): EngineConfigFromViper, NewEngine, Start. +// The engine cannot function without a valid config, auth clients, or NATS connection. +// +// Recoverable errors (call SetInitError and keep running): source config validation +// failures (e.g. missing credentials, invalid regions) and adapter initialization +// failures that may be transient. The pod stays Running, readiness fails, and the +// error is reported via heartbeats and the API/UI. +// +// Permanent config errors (e.g. invalid API key, missing required flags) should +// be detected before calling InitialiseAdapters and reported via SetInitError — +// do not retry. Transient adapter init errors (e.g. upstream API temporarily +// unavailable) should use InitialiseAdapters, which retries with backoff. +// +// See SetInitError and InitialiseAdapters for details and examples. +package discovery diff --git a/go/discovery/engine.go b/go/discovery/engine.go new file mode 100644 index 00000000..1b64388e --- /dev/null +++ b/go/discovery/engine.go @@ -0,0 +1,897 @@ +package discovery + +import ( + "context" + "errors" + "fmt" + "net/http" + "slices" + "strings" + "sync" + "time" + + "connectrpc.com/connect" + "github.com/cenkalti/backoff/v5" + "github.com/getsentry/sentry-go" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "github.com/sourcegraph/conc/pool" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + DefaultMaxRequestTimeout = 5 * time.Minute + DefaultConnectionWatchInterval = 3 * time.Second +) + +// The client that will be used to send heartbeats. This will usually be an +// `sdpconnect.ManagementServiceClient` +type HeartbeatClient interface { + SubmitSourceHeartbeat(context.Context, *connect.Request[sdp.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp.SubmitSourceHeartbeatResponse], error) +} + +type HeartbeatOptions struct { + // The client that will be used to send heartbeats + ManagementClient HeartbeatClient + + // ReadinessCheck is called during readiness probes to verify adapters are healthy and ready. + // This should be a lightweight, adapter-only check (do NOT include engine/liveness checks). + // Timeouts are controlled by the caller (e.g., Kubernetes probe timeout / SendHeartbeat). + // See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes + ReadinessCheck func(context.Context) error + + // How frequently to send a heartbeat + Frequency time.Duration +} + +// EngineConfig is the configuration for the engine +// it is used to configure the engine before starting it +type EngineConfig struct { + EngineType string // The type of the engine, e.g. "aws" or "kubernetes" + Version string // The version of the adapter that should be reported in the heartbeat + SourceName string // normally follows the format of "type-hostname", e.g. "stdlib-source" + SourceUUID uuid.UUID // The UUID of the source, is this is blank it will be auto-generated. This is used in heartbeats and shouldn't be supplied usually" + App string // "https://app.overmind.tech", "The URL of the Overmind app to use" + APIServerURL string // The URL of the Overmind API server to uses for the heartbeat, this is calculated + + // The 'ovm_*' API key to use to authenticate to the Overmind API. + // This and 'SourceAccessToken' are mutually exclusive + ApiKey string // The API key to use to authenticate to the Overmind API" + // Static token passed to the source to authenticate. + SourceAccessToken string // The access token to use to authenticate to the source + SourceAccessTokenType string // The type of token to use to authenticate the source for managed sources + + // NATS options + NATSOptions *auth.NATSOptions // Options for connecting to NATS + NATSConnectionTimeout int // The timeout for connecting to NATS + NATSQueueName string // The name of the queue to use when subscribing + Unauthenticated bool // Whether the source is unauthenticated + + // The options for the heartbeat. If this is nil the engine won't send + // it is not used if we are nats only or unauthenticated. this will only happen if we are running in a test environment + HeartbeatOptions *HeartbeatOptions + + // Whether this adapter is managed by Overmind. This is initially used for + // reporting so that you can tell the difference between managed adapters and + // ones you're running locally + OvermindManagedSource sdp.SourceManaged + MaxParallelExecutions int // 2_000, Max number of requests to run in parallel +} + +// Engine is the main discovery engine. This is where all of the Adapters and +// adapters are stored and is responsible for calling out to the right adapters to +// discover everything +// +// Note that an engine that does not have a connected NATS connection will +// simply not communicate over NATS +type Engine struct { + EngineConfig *EngineConfig + // The maximum request timeout. Defaults to `DefaultMaxRequestTimeout` if + // set to zero. If a client does not send a timeout, it will default to this + // value. Requests with timeouts larger than this value will have their + // timeouts overridden + MaxRequestTimeout time.Duration + + // How often to check for closed connections and try to recover + ConnectionWatchInterval time.Duration + connectionWatcher NATSWatcher + + // The configuration for the heartbeat for this engine. If this is nil the + // engine won't send heartbeats when started + + // Internal throttle used to limit MaxParallelExecutions. This reads + // MaxParallelExecutions and is populated when the engine is started. This + // pool is only used for LIST requests. Since GET requests can be blocked by + // LIST requests, they need to be handled in a different pool to avoid + // deadlocking. + listExecutionPool *pool.Pool + + // Internal throttle used to limit MaxParallelExecutions. This reads + // MaxParallelExecutions and is populated when the engine is started. This + // pool is only used for GET and SEARCH requests. Since GET requests can be + // blocked by LIST requests, they need to be handled in a different pool to + // avoid deadlocking. + getExecutionPool *pool.Pool + + // The NATS connection + natsConnection sdp.EncodedConnection + natsConnectionMutex sync.Mutex + + // All Adapters managed by this Engine + sh *AdapterHost + + // handle log requests with this adapter + logAdapter LogAdapter + logAdapterMu sync.RWMutex + + // GetListMutex used for locking out Get queries when there's a List happening + gfm GetListMutex + + // trackedQueries is used for storing queries that have a UUID so they can + // be cancelled if required + trackedQueries map[uuid.UUID]*QueryTracker + trackedQueriesMutex sync.RWMutex + + // Prevents the engine being restarted many times in parallel + restartMutex sync.Mutex + + // Context to background jobs like cache purging and heartbeats. These will + // stop when the context is cancelled + backgroundJobContext context.Context + backgroundJobCancel context.CancelFunc + heartbeatCancel context.CancelFunc + + // Heartbeat status tracking for healthz checks + lastSuccessfulHeartbeat time.Time + lastHeartbeatError error + heartbeatStatusMutex sync.RWMutex + + // initError stores configuration/credential/initialization failures that prevent + // adapters from being added to the engine. This includes: + // - AWS: AssumeRole failures, GetCallerIdentity errors, invalid credentials + // - K8s: Namespace listing failures, kubeconfig errors + // - Harness: API authentication failures, hierarchy discovery errors + // The error is surfaced via readiness checks (pod becomes 0/1 Ready) and + // heartbeats (visible in UI/API), allowing the pod to stay Running instead of + // CrashLoopBackOff so customers can diagnose and fix configuration issues. + initError error + initErrorMutex sync.RWMutex +} + +func NewEngine(engineConfig *EngineConfig) (*Engine, error) { + if err := engineConfig.CreateClients(); err != nil { + return nil, fmt.Errorf("could not create auth clients: %w", err) + } + sh := NewAdapterHost() + return &Engine{ + EngineConfig: engineConfig, + MaxRequestTimeout: DefaultMaxRequestTimeout, + ConnectionWatchInterval: DefaultConnectionWatchInterval, + sh: sh, + trackedQueries: make(map[uuid.UUID]*QueryTracker), + }, nil +} + +// TrackQuery Stores a QueryTracker in the engine so that it can be looked +// up later and cancelled if required. The UUID should be supplied as part of +// the query itself +func (e *Engine) TrackQuery(uuid uuid.UUID, qt *QueryTracker) { + e.trackedQueriesMutex.Lock() + defer e.trackedQueriesMutex.Unlock() + e.trackedQueries[uuid] = qt +} + +// GetTrackedQuery Returns the QueryTracker object for a given UUID. This +// tracker can then be used to cancel the query +func (e *Engine) GetTrackedQuery(uuid uuid.UUID) (*QueryTracker, error) { + e.trackedQueriesMutex.RLock() + defer e.trackedQueriesMutex.RUnlock() + + if qt, ok := e.trackedQueries[uuid]; ok { + return qt, nil + } else { + return nil, fmt.Errorf("tracker with UUID %x not found", uuid) + } +} + +// DeleteTrackedQuery Deletes a query from tracking +func (e *Engine) DeleteTrackedQuery(uuid [16]byte) { + e.trackedQueriesMutex.Lock() + defer e.trackedQueriesMutex.Unlock() + delete(e.trackedQueries, uuid) +} + +// AddAdapters Adds an adapter to this engine +func (e *Engine) AddAdapters(adapters ...Adapter) error { + return e.sh.AddAdapters(adapters...) +} + +// Connect Connects to NATS +func (e *Engine) connect() error { + if e.EngineConfig.NATSOptions != nil { + encodedConnection, err := e.EngineConfig.NATSOptions.Connect() + if err != nil { + return fmt.Errorf("error connecting to NATS '%+v' : %w", e.EngineConfig.NATSOptions.Servers, err) + } + + e.natsConnectionMutex.Lock() + e.natsConnection = encodedConnection + e.natsConnectionMutex.Unlock() + + // TODO: this could be replaced by setting the various callbacks on the + // natsConnection and waiting for notification from the underlying + // connection. + e.connectionWatcher = NATSWatcher{ + Connection: e.natsConnection, + // If the connection stays disconnected for more than 5 minutes, + // force a reconnection attempt. This prevents the source from being + // stuck in RECONNECTING state indefinitely. + ReconnectionTimeout: 5 * time.Minute, + FailureHandler: func() { + go func() { + log.Warn("NATSWatcher triggered failure handler, attempting to reconnect") + e.disconnect() + + if err := e.connect(); err != nil { + log.WithError(err).Error("Error reconnecting during failure handler") + } + }() + }, + } + e.connectionWatcher.Start(e.ConnectionWatchInterval) + + // Wait for the connection to be completed + err = e.natsConnection.Underlying().FlushTimeout(10 * time.Minute) + if err != nil { + return fmt.Errorf("error flushing NATS connection: %w", err) + } + + log.WithFields(log.Fields{ + "ServerID": e.natsConnection.Underlying().ConnectedServerId(), + "URL:": e.natsConnection.Underlying().ConnectedUrl(), + }).Info("NATS connected") + } + + if e.natsConnection == nil { + return errors.New("no NATSOptions struct and no natsConnection provided") + } + + // Since the underlying query processing logic creates its own spans + // when it has some real work to do, we are not passing a name to these + // query handlers so that we don't get spans that are completely empty + err := e.subscribe("request.all", sdp.NewAsyncRawQueryHandler("", func(ctx context.Context, _ *nats.Msg, i *sdp.Query) { + e.HandleQuery(ctx, i) + })) + if err != nil { + return fmt.Errorf("error subscribing to request.all: %w", err) + } + + err = e.subscribe("request.scope.>", sdp.NewAsyncRawQueryHandler("", func(ctx context.Context, m *nats.Msg, i *sdp.Query) { + e.HandleQuery(ctx, i) + })) + if err != nil { + return fmt.Errorf("error subscribing to request.scope.>: %w", err) + } + + err = e.subscribe("cancel.all", sdp.NewAsyncRawCancelQueryHandler("CancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.CancelQuery) { + e.HandleCancelQuery(ctx, i) + })) + if err != nil { + return fmt.Errorf("error subscribing to cancel.all: %w", err) + } + + err = e.subscribe("cancel.scope.>", sdp.NewAsyncRawCancelQueryHandler("WildcardCancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.CancelQuery) { + e.HandleCancelQuery(ctx, i) + })) + if err != nil { + return fmt.Errorf("error subscribing to cancel.scope.>: %w", err) + } + + if e.logAdapter != nil { + for _, scope := range e.logAdapter.Scopes() { + subj := fmt.Sprintf("logs.scope.%v", scope) + err = e.subscribe(subj, sdp.NewAsyncRawNATSGetLogRecordsRequestHandler("WildcardCancelQueryHandler", func(ctx context.Context, m *nats.Msg, i *sdp.NATSGetLogRecordsRequest) { + replyTo := m.Header.Get("reply-to") + e.HandleLogRecordsRequest(ctx, replyTo, i) + })) + if err != nil { + return fmt.Errorf("error subscribing to %v: %w", subj, err) + } + } + } + + return nil +} + +// disconnect Disconnects the engine from the NATS network +func (e *Engine) disconnect() { + e.connectionWatcher.Stop() + + e.natsConnectionMutex.Lock() + defer e.natsConnectionMutex.Unlock() + + if e.natsConnection == nil { + return + } + + e.natsConnection.Close() + e.natsConnection.Drop() +} + +// Start performs all of the initialisation steps required for the engine to +// work. Note that this creates NATS subscriptions for all available adapters so +// modifying the Adapters value after an engine has been started will not have +// any effect until the engine is restarted +func (e *Engine) Start(ctx context.Context) error { + e.listExecutionPool = pool.New().WithMaxGoroutines(e.EngineConfig.MaxParallelExecutions) + e.getExecutionPool = pool.New().WithMaxGoroutines(e.EngineConfig.MaxParallelExecutions) + + e.backgroundJobContext, e.backgroundJobCancel = context.WithCancel(ctx) + + // Decide your own UUID if not provided + if e.EngineConfig.SourceUUID == uuid.Nil { + e.EngineConfig.SourceUUID = uuid.New() + } + + err := e.connect() //nolint:contextcheck // context is passed in through backgroundJobContext + if err != nil { + _ = e.SendHeartbeat(e.backgroundJobContext, err) //nolint:contextcheck + return fmt.Errorf("could not connect to NATS: %w", err) + } + + // Start background jobs + e.StartSendingHeartbeats(e.backgroundJobContext) //nolint:contextcheck + return nil +} + +// subscribe Subscribes to a subject using the current NATS connection. +// Remember to use sdp's genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling +func (e *Engine) subscribe(subject string, handler nats.MsgHandler) error { + var err error + + e.natsConnectionMutex.Lock() + defer e.natsConnectionMutex.Unlock() + + if e.natsConnection.Underlying() == nil { + return errors.New("cannot subscribe. NATS connection is nil") + } + + log.WithFields(log.Fields{ + "queueName": e.EngineConfig.NATSQueueName, + "subject": subject, + "engineName": e.EngineConfig.SourceName, + }).Debug("creating NATS subscription") + + if e.EngineConfig.NATSQueueName == "" { + _, err = e.natsConnection.Subscribe(subject, handler) + } else { + _, err = e.natsConnection.QueueSubscribe(subject, e.EngineConfig.NATSQueueName, handler) + } + if err != nil { + return fmt.Errorf("error subscribing to NATS: %w", err) + } + + return nil +} + +// Stop Stops the engine running and disconnects from NATS +func (e *Engine) Stop() error { + e.disconnect() + + // Stop purging and clear the cache + if e.backgroundJobCancel != nil { + e.backgroundJobCancel() + } + if e.heartbeatCancel != nil { + e.heartbeatCancel() + } + return nil +} + +// Restart Restarts the engine. If called in parallel, subsequent calls are +// ignored until the restart is completed +func (e *Engine) Restart(ctx context.Context) error { + e.restartMutex.Lock() + defer e.restartMutex.Unlock() + + err := e.Stop() + if err != nil { + return fmt.Errorf("Restart.Stop: %w", err) + } + + err = e.Start(ctx) + return fmt.Errorf("Restart.Start: %w", err) +} + +// IsNATSConnected returns whether the engine is connected to NATS +func (e *Engine) IsNATSConnected() bool { + e.natsConnectionMutex.Lock() + defer e.natsConnectionMutex.Unlock() + + if e.natsConnection == nil { + return false + } + + if conn := e.natsConnection.Underlying(); conn != nil { + return conn.IsConnected() + } + + return false +} + +// LivenessHealthCheck reports only engine initialization/health (NATS + heartbeat status). +// Kubernetes runs liveness/startup independently from readiness; adapter checks do NOT belong here. +// See: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes +func (e *Engine) LivenessHealthCheck(ctx context.Context) error { + span := trace.SpanFromContext(ctx) + + e.natsConnectionMutex.Lock() + var ( + encodedConn = e.natsConnection + underlying *nats.Conn + ) + if encodedConn != nil { + underlying = encodedConn.Underlying() + } + e.natsConnectionMutex.Unlock() + + natsConnected := underlying != nil && underlying.IsConnected() + + // Read memory stats and add them to the span + memStats := tracing.ReadMemoryStats() + tracing.SetMemoryAttributes(span, "ovm.healthcheck", memStats) + + span.SetAttributes( + attribute.String("ovm.engine.name", e.EngineConfig.SourceName), + attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), + attribute.Bool("ovm.nats.connected", natsConnected), + attribute.Int("ovm.discovery.listExecutionPoolCount", int(listExecutionPoolCount.Load())), + attribute.Int("ovm.discovery.getExecutionPoolCount", int(getExecutionPoolCount.Load())), + ) + + if underlying != nil { + span.SetAttributes( + attribute.String("ovm.nats.serverId", underlying.ConnectedServerId()), + attribute.String("ovm.nats.url", underlying.ConnectedUrl()), + attribute.Int64("ovm.nats.reconnects", int64(underlying.Reconnects)), //nolint:gosec // Reconnects is always a small positive number + ) + } + + if !natsConnected { + return errors.New("NATS connection is not connected") + } + + // Check if heartbeats are failing to submit to api-server + // This fails healthz faster than api-server marks sources as DISCONNECTED, + // allowing seamless pod recycling without customer-visible downtime + if e.EngineConfig.HeartbeatOptions != nil && e.EngineConfig.HeartbeatOptions.Frequency > 0 { + e.heartbeatStatusMutex.RLock() + lastSuccessfulHeartbeat := e.lastSuccessfulHeartbeat + lastHeartbeatError := e.lastHeartbeatError + e.heartbeatStatusMutex.RUnlock() + + // Only check if we've had at least one successful heartbeat + // This allows initial startup grace period + if !lastSuccessfulHeartbeat.IsZero() { + // Healthz fails at 2.0x frequency, api-server marks DISCONNECTED at 2.5x + // This 0.5x buffer allows time for pod recycling + healthzFailureThreshold := lastSuccessfulHeartbeat.Add(time.Duration(float64(e.EngineConfig.HeartbeatOptions.Frequency) * 2.0)) + now := time.Now() + + if now.After(healthzFailureThreshold) && lastHeartbeatError != nil { + return fmt.Errorf("heartbeat submission to api-server has been failing: %w (last successful heartbeat: %v, threshold: %v)", lastHeartbeatError, lastSuccessfulHeartbeat, healthzFailureThreshold) + } + } + } + + return nil +} + +// ReadinessHealthCheck reports whether adapters are ready to serve requests. +// It must not call LivenessHealthCheck; readiness should reflect adapter health only. +// See: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes +func (e *Engine) ReadinessHealthCheck(ctx context.Context) error { + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.String("ovm.healthcheck.type", "readiness"), + ) + + // Check for persistent initialization errors first + if initErr := e.GetInitError(); initErr != nil { + return fmt.Errorf("source initialization failed: %w", initErr) + } + + // Check adapter-specific health using the ReadinessCheck function + if e.EngineConfig.HeartbeatOptions != nil && e.EngineConfig.HeartbeatOptions.ReadinessCheck != nil { + if err := e.EngineConfig.HeartbeatOptions.ReadinessCheck(ctx); err != nil { + return err + } + } + + return nil +} + +// HandleCancelQuery Takes a CancelQuery and cancels that query if it exists +func (e *Engine) HandleCancelQuery(ctx context.Context, cancelQuery *sdp.CancelQuery) { + span := trace.SpanFromContext(ctx) + span.SetName("HandleCancelQuery") + span.SetAttributes( + attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), + ) + + u, err := uuid.FromBytes(cancelQuery.GetUUID()) + if err != nil { + log.Errorf("Error parsing UUID for cancel query: %v", err) + return + } + + rt, err := e.GetTrackedQuery(u) + if err != nil { + log.WithFields(log.Fields{ + "UUID": u.String(), + }).Debug("Received cancel query for unknown UUID") + return + } + + if rt != nil && rt.Cancel != nil { + log.WithFields(log.Fields{ + "UUID": u.String(), + }).Debug("Cancelling query") + rt.Cancel() + } +} + +func (e *Engine) HandleLogRecordsRequest(ctx context.Context, replyTo string, request *sdp.NATSGetLogRecordsRequest) { + span := trace.SpanFromContext(ctx) + span.SetName("HandleLogRecordsRequest") + span.SetAttributes( + attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), + ) + + if !strings.HasPrefix(replyTo, "logs.records.") { + sentry.CaptureException(fmt.Errorf("received log records request with invalid reply-to header: %s", replyTo)) + return + } + + err := e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ + Content: &sdp.NATSGetLogRecordsResponse_Status{ + Status: &sdp.NATSGetLogRecordsResponseStatus{ + Status: sdp.NATSGetLogRecordsResponseStatus_STARTED, + }, + }, + }) + if err != nil { + sentry.CaptureException(fmt.Errorf("error publishing log records STARTED response: %w", err)) + return + } + + // ensure that we send an error response if the HandleLogRecordsRequestWithErrors call panics + defer func() { + if r := recover(); r != nil { + sentry.CaptureException(fmt.Errorf("panic in log records request handler: %v", r)) + err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ + Content: &sdp.NATSGetLogRecordsResponse_Status{ + Status: &sdp.NATSGetLogRecordsResponseStatus{ + Status: sdp.NATSGetLogRecordsResponseStatus_ERRORED, + Error: sdp.NewLocalSourceError(connect.CodeInternal, "panic in log records request handler"), + }, + }, + }) + if err != nil { + sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) + return + } + } + }() + + srcErr := e.HandleLogRecordsRequestWithErrors(ctx, replyTo, request) + if srcErr != nil { + err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ + Content: &sdp.NATSGetLogRecordsResponse_Status{ + Status: &sdp.NATSGetLogRecordsResponseStatus{ + Status: sdp.NATSGetLogRecordsResponseStatus_ERRORED, + Error: srcErr, + }, + }, + }) + if err != nil { + sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) + return + } + return + } + + err = e.natsConnection.Publish(ctx, replyTo, &sdp.NATSGetLogRecordsResponse{ + Content: &sdp.NATSGetLogRecordsResponse_Status{ + Status: &sdp.NATSGetLogRecordsResponseStatus{ + Status: sdp.NATSGetLogRecordsResponseStatus_FINISHED, + }, + }, + }) + if err != nil { + sentry.CaptureException(fmt.Errorf("error publishing log records FINISHED response: %w", err)) + return + } +} + +func (e *Engine) HandleLogRecordsRequestWithErrors(ctx context.Context, replyTo string, natsRequest *sdp.NATSGetLogRecordsRequest) *sdp.SourceError { + if e.logAdapter == nil { + return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "no logs adapter registered") + } + + if natsRequest == nil { + return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "received nil log records request") + } + + req := natsRequest.GetRequest() + if req == nil { + return sdp.NewLocalSourceError(connect.CodeInvalidArgument, "received nil log records request body") + } + + err := req.Validate() + if err != nil { + return sdp.NewLocalSourceError(connect.CodeInvalidArgument, fmt.Sprintf("invalid log records request: %v", err)) + } + + if !slices.Contains(e.logAdapter.Scopes(), req.GetScope()) { + return sdp.NewLocalSourceError(connect.CodeInvalidArgument, fmt.Sprintf("scope %s is not available", req.GetScope())) + } + + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.String("ovm.logs.replyTo", replyTo), + attribute.String("ovm.logs.scope", req.GetScope()), + attribute.String("ovm.logs.query", req.GetQuery()), + attribute.String("ovm.logs.from", req.GetFrom().String()), + attribute.String("ovm.logs.to", req.GetTo().String()), + attribute.Int("ovm.logs.maxRecords", int(req.GetMaxRecords())), + attribute.Bool("ovm.logs.startFromOldest", req.GetStartFromOldest()), + attribute.String("ovm.sdp.source_name", e.EngineConfig.SourceName), + ) + + stream := &LogRecordsStreamImpl{ + subject: replyTo, + stream: e.natsConnection, + } + err = e.logAdapter.Get(ctx, req, stream) + + span.SetAttributes( + attribute.Int("ovm.logs.numResponses", stream.responses), + attribute.Int("ovm.logs.numRecords", stream.records), + ) + srcErr := &sdp.SourceError{} + if errors.As(err, &srcErr) { + return srcErr + } + if errors.Is(err, context.DeadlineExceeded) || ctx.Err() == context.DeadlineExceeded { + return sdp.NewLocalSourceError(connect.CodeDeadlineExceeded, "log records request deadline exceeded") + } + if err != nil { + return sdp.NewLocalSourceError(connect.CodeInternal, fmt.Sprintf("error handling log records request: %v", err)) + } + + return nil +} + +// ClearAdapters Deletes all adapters from the engine, allowing new adapters to be +// added using `AddAdapter()`. Note that this requires a restart using +// `Restart()` in order to take effect +func (e *Engine) ClearAdapters() { + e.sh.ClearAllAdapters() +} + +// IsWildcard checks if a string is the wildcard. Use this instead of +// implementing the wildcard check everywhere so that if we need to change the +// wildcard at a later date we can do so here +func IsWildcard(s string) bool { + return s == sdp.WILDCARD +} + +// SetLogAdapter registers a single LogAdapter with the engine. +// Returns an error when there is already a log adapter registered. +func (e *Engine) SetLogAdapter(adapter LogAdapter) error { + if adapter == nil { + return errors.New("log adapter cannot be nil") + } + + e.logAdapterMu.Lock() + defer e.logAdapterMu.Unlock() + + if e.logAdapter != nil { + return errors.New("log adapter already registered") + } + + e.logAdapter = adapter + return nil +} + +// GetAvailableScopesAndMetadata returns the available scopes and adapter metadata +// from all visible adapters. This is useful for heartbeats and other reporting. +func (e *Engine) GetAvailableScopesAndMetadata() ([]string, []*sdp.AdapterMetadata) { + // Get available types and scopes + availableScopesMap := map[string]bool{} + adapterMetadata := []*sdp.AdapterMetadata{} + + for _, adapter := range e.sh.VisibleAdapters() { + for _, scope := range adapter.Scopes() { + availableScopesMap[scope] = true + } + adapterMetadata = append(adapterMetadata, adapter.Metadata()) + } + + // Extract slices from maps + availableScopes := []string{} + for s := range availableScopesMap { + availableScopes = append(availableScopes, s) + } + + return availableScopes, adapterMetadata +} + +// AdaptersByType returns adapters of the specified type. This is useful for health checks. +func (e *Engine) AdaptersByType(typ string) []Adapter { + return e.sh.AdaptersByType(typ) +} + +// SetInitError stores a persistent initialization error that will be reported via heartbeat and readiness checks. +// This should be called when source initialization fails in a way that prevents adapters from being added, +// but the process should continue running to serve probes and heartbeats (avoiding CrashLoopBackOff). +// +// Pass nil to clear a previously set error (e.g. after successful retry/restart). +// +// Example usage: +// +// if err := initializeAdapters(); err != nil { +// e.SetInitError(fmt.Errorf("adapter initialization failed: %w", err)) +// // Continue running - pod stays Running with readiness failing +// } +func (e *Engine) SetInitError(err error) { + e.initErrorMutex.Lock() + defer e.initErrorMutex.Unlock() + e.initError = err +} + +// GetInitError returns the persistent initialization error if any. +// Returns nil if no init error is set or if it was cleared via SetInitError(nil). +func (e *Engine) GetInitError() error { + e.initErrorMutex.RLock() + defer e.initErrorMutex.RUnlock() + return e.initError +} + +// InitialiseAdapters retries initFn with exponential backoff (capped at +// 5 minutes) until it succeeds or ctx is cancelled. It blocks the caller. +// +// This is intended for adapter initialization that makes API calls to upstream +// services and may fail transiently. Because it blocks, the caller can +// safely set up namespace watches or other reload mechanisms after it returns +// without racing against a background retry goroutine. +// +// On each attempt: +// - ClearAdapters() is called to remove any leftovers from previous attempts. +// - initFn is called. The init error is updated via SetInitError immediately +// (cleared on success, set on failure) and then a heartbeat is sent so the +// API/UI always reflects the current status. +// - On success, StartSendingHeartbeats is called and the function returns. +// +// The caller should have already called Start() before calling this. +func (e *Engine) InitialiseAdapters(ctx context.Context, initFn func(ctx context.Context) error) { + b := backoff.NewExponentialBackOff() + b.MaxInterval = 5 * time.Minute + tick := backoff.NewTicker(b) + defer tick.Stop() + + for { + select { + case <-ctx.Done(): + return + case _, ok := <-tick.C: + if !ok { + // Backoff exhausted (shouldn't happen with default MaxElapsedTime=0) + return + } + + e.ClearAdapters() + + err := initFn(ctx) + + if err != nil { + e.SetInitError(fmt.Errorf("adapter initialisation failed: %w", err)) + log.WithError(err).Warn("Adapter initialisation failed, will retry") + } else { + // Clear any previous init error before the heartbeat so the + // API/UI immediately sees the healthy status. + e.SetInitError(nil) + } + + // Send heartbeat regardless of outcome so the API/UI reflects current status + if hbErr := e.SendHeartbeat(ctx, nil); hbErr != nil { + log.WithError(hbErr).Error("Error sending heartbeat during adapter initialisation") + } + + if err != nil { + continue + } + + e.StartSendingHeartbeats(ctx) + return + } + } +} + +// LivenessProbeHandlerFunc returns an HTTP handler function for liveness probes. +// This checks only engine initialization (NATS connection, heartbeats) and does NOT check adapter-specific health. +func (e *Engine) LivenessProbeHandlerFunc() func(http.ResponseWriter, *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + ctx, span := tracing.HealthCheckTracer().Start(r.Context(), "healthcheck.liveness") + defer span.End() + + err := e.LivenessHealthCheck(ctx) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Liveness check failed") + http.Error(rw, err.Error(), http.StatusServiceUnavailable) + return + } + + fmt.Fprint(rw, "ok") + } +} + +// SetReadinessCheck sets the readiness check and ensures HeartbeatOptions is initialized. +func (e *Engine) SetReadinessCheck(check func(context.Context) error) { + if e.EngineConfig.HeartbeatOptions == nil { + e.EngineConfig.HeartbeatOptions = &HeartbeatOptions{} + } + e.EngineConfig.HeartbeatOptions.ReadinessCheck = check +} + +// ReadinessProbeHandlerFunc returns an HTTP handler function for readiness probes. +// This checks adapter-specific health only (not engine/liveness). +func (e *Engine) ReadinessProbeHandlerFunc() func(http.ResponseWriter, *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + ctx, span := tracing.HealthCheckTracer().Start(r.Context(), "healthcheck.readiness") + defer span.End() + + err := e.ReadinessHealthCheck(ctx) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Readiness check failed") + http.Error(rw, err.Error(), http.StatusServiceUnavailable) + return + } + + fmt.Fprint(rw, "ok") + } +} + +// ServeHealthProbes starts an HTTP server for Kubernetes health probes on the given port. +// Registers /healthz/alive (liveness) and /healthz/ready (readiness). +// Runs in a goroutine. Use for sources that only need health checks on the given port. +func (e *Engine) ServeHealthProbes(port int) { + mux := http.NewServeMux() + mux.HandleFunc("/healthz/alive", e.LivenessProbeHandlerFunc()) + mux.HandleFunc("/healthz/ready", e.ReadinessProbeHandlerFunc()) + + 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") + + 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/go/discovery/engine_initerror_test.go b/go/discovery/engine_initerror_test.go new file mode 100644 index 00000000..26acfa34 --- /dev/null +++ b/go/discovery/engine_initerror_test.go @@ -0,0 +1,365 @@ +package discovery + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestSetInitError(t *testing.T) { + e := &Engine{ + initError: nil, + initErrorMutex: sync.RWMutex{}, + } + + testErr := errors.New("initialization failed") + e.SetInitError(testErr) + + // Direct pointer comparison is intentional here - we want to verify the exact error object is stored + if e.initError == nil || e.initError.Error() != testErr.Error() { + t.Errorf("expected initError to be %v, got %v", testErr, e.initError) + } +} + +func TestGetInitError(t *testing.T) { + e := &Engine{ + initError: nil, + initErrorMutex: sync.RWMutex{}, + } + + // Test nil case + if err := e.GetInitError(); err != nil { + t.Errorf("expected nil error, got %v", err) + } + + // Test with error set + testErr := errors.New("test error") + e.initError = testErr + + if err := e.GetInitError(); err == nil || err.Error() != testErr.Error() { + t.Errorf("expected error to be %v, got %v", testErr, err) + } +} + +func TestSetInitErrorNil(t *testing.T) { + e := &Engine{ + initError: errors.New("previous error"), + initErrorMutex: sync.RWMutex{}, + } + + // Clear the error + e.SetInitError(nil) + + if e.initError != nil { + t.Errorf("expected initError to be nil after clearing, got %v", e.initError) + } + + if err := e.GetInitError(); err != nil { + t.Errorf("expected GetInitError to return nil after clearing, got %v", err) + } +} + +func TestInitErrorConcurrentAccess(t *testing.T) { + e := &Engine{ + initError: nil, + initErrorMutex: sync.RWMutex{}, + } + + // Test concurrent access from multiple goroutines + var wg sync.WaitGroup + iterations := 100 + + // Writers + for i := range 10 { + wg.Add(1) + go func(id int) { + defer wg.Done() + for j := range iterations { + e.SetInitError(fmt.Errorf("error from goroutine %d iteration %d", id, j)) + } + }(i) + } + + // Readers + for range 10 { + wg.Add(1) + go func() { + defer wg.Done() + for range iterations { + _ = e.GetInitError() + } + }() + } + + wg.Wait() + + // Should not panic - error should be one of the written values or nil + finalErr := e.GetInitError() + if finalErr == nil { + t.Log("Final error is nil (acceptable in concurrent test)") + } else { + t.Logf("Final error: %v", finalErr) + } +} + +func TestReadinessHealthCheckWithInitError(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + ReadinessCheck: func(ctx context.Context) error { + // Adapter health is fine + return nil + }, + }, + } + + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx := context.Background() + + // Readiness should pass when no init error + if err := e.ReadinessHealthCheck(ctx); err != nil { + t.Errorf("expected readiness to pass with no init error, got: %v", err) + } + + // Set an init error + testErr := errors.New("AWS AssumeRole denied") + e.SetInitError(testErr) + + // Readiness should now fail with the init error + err = e.ReadinessHealthCheck(ctx) + if err == nil { + t.Error("expected readiness to fail with init error, got nil") + } else if !errors.Is(err, testErr) { + t.Errorf("expected readiness error to wrap init error, got: %v", err) + } + + // Clear the init error + e.SetInitError(nil) + + // Readiness should pass again + if err := e.ReadinessHealthCheck(ctx); err != nil { + t.Errorf("expected readiness to pass after clearing init error, got: %v", err) + } +} + +func TestSendHeartbeatWithInitError(t *testing.T) { + requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 10) + responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 10) + + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + ManagementClient: testHeartbeatClient{ + Requests: requests, + Responses: responses, + }, + Frequency: 0, // Disable automatic heartbeats + ReadinessCheck: func(ctx context.Context) error { + return nil // Adapters are fine + }, + }, + } + + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx := context.Background() + + // Send heartbeat with init error + testErr := errors.New("configuration error: invalid credentials") + e.SetInitError(testErr) + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + err = e.SendHeartbeat(ctx, nil) + if err != nil { + t.Errorf("expected SendHeartbeat to succeed, got: %v", err) + } + + // Verify the heartbeat included the init error + req := <-requests + if req.Msg.GetError() == "" { + t.Error("expected heartbeat to include error, got empty string") + } else if !strings.Contains(req.Msg.GetError(), testErr.Error()) { + t.Errorf("expected heartbeat error to contain %q, got: %q", testErr.Error(), req.Msg.GetError()) + } +} + +func TestSendHeartbeatWithInitErrorAndCustomError(t *testing.T) { + requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 10) + responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 10) + + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + ManagementClient: testHeartbeatClient{ + Requests: requests, + Responses: responses, + }, + Frequency: 0, + }, + } + + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx := context.Background() + + // Set init error and send heartbeat with custom error + initErr := errors.New("init failed: invalid config") + customErr := errors.New("custom error: readiness failed") + e.SetInitError(initErr) + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + err = e.SendHeartbeat(ctx, customErr) + if err != nil { + t.Errorf("expected SendHeartbeat to succeed, got: %v", err) + } + + // Verify both errors are included in the heartbeat + req := <-requests + if req.Msg.GetError() == "" { + t.Error("expected heartbeat to include errors, got empty string") + } else { + errMsg := req.Msg.GetError() + // Both errors should be in the joined error string + if !strings.Contains(errMsg, initErr.Error()) { + t.Errorf("expected heartbeat error to include init error %q, got: %q", initErr.Error(), errMsg) + } + if !strings.Contains(errMsg, customErr.Error()) { + t.Errorf("expected heartbeat error to include custom error %q, got: %q", customErr.Error(), errMsg) + } + } +} + +func TestInitialiseAdapters_Success(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + Frequency: 0, // Disable automatic heartbeats from StartSendingHeartbeats + }, + } + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + // Set an init error to verify it gets cleared on success + e.SetInitError(errors.New("previous error")) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var called bool + e.InitialiseAdapters(ctx, func(ctx context.Context) error { + called = true + return nil + }) + + if !called { + t.Error("initFn was not called") + } + if err := e.GetInitError(); err != nil { + t.Errorf("expected init error to be cleared after success, got: %v", err) + } +} + +func TestInitialiseAdapters_RetryThenSuccess(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + Frequency: 0, + }, + } + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + attempts := 0 + e.InitialiseAdapters(ctx, func(ctx context.Context) error { + attempts++ + if attempts < 3 { + return fmt.Errorf("transient error attempt %d", attempts) + } + return nil + }) + + if attempts < 3 { + t.Errorf("expected at least 3 attempts, got %d", attempts) + } + if err := e.GetInitError(); err != nil { + t.Errorf("expected init error to be cleared after eventual success, got: %v", err) + } +} + +func TestInitialiseAdapters_ContextCancelled(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + Frequency: 0, + }, + } + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + var callCount int + + // InitialiseAdapters blocks; cancel ctx after a short delay so it returns + time.AfterFunc(500*time.Millisecond, cancel) + + done := make(chan struct{}) + go func() { + e.InitialiseAdapters(ctx, func(ctx context.Context) error { + callCount++ + return errors.New("always fails") + }) + close(done) + }() + + select { + case <-done: + // InitialiseAdapters returned (ctx was cancelled) + case <-time.After(5 * time.Second): + t.Fatal("InitialiseAdapters did not return after context cancellation") + } + + if callCount == 0 { + t.Error("expected initFn to be called at least once before context cancellation") + } + if err := e.GetInitError(); err == nil { + t.Error("expected init error to be set after context cancellation with failures") + } +} diff --git a/go/discovery/engine_test.go b/go/discovery/engine_test.go new file mode 100644 index 00000000..f90ae870 --- /dev/null +++ b/go/discovery/engine_test.go @@ -0,0 +1,808 @@ +package discovery + +import ( + "context" + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats-server/v2/test" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "golang.org/x/oauth2" +) + +func newEngine(t *testing.T, name string, no *auth.NATSOptions, eConn sdp.EncodedConnection, adapters ...Adapter) *Engine { + t.Helper() + + if no != nil && eConn != nil { + t.Fatal("Cannot provide both NATSOptions and EncodedConnection") + } + + ec := EngineConfig{ + MaxParallelExecutions: 10, + SourceName: name, + NATSQueueName: "test", + } + if no != nil { + ec.NATSOptions = no + if no.TokenClient == nil { + ec.Unauthenticated = true + } + } else if eConn == nil { + ec.NATSOptions = &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + TokenClient: GetTestOAuthTokenClient(t, "org_hdeUXbB55sMMvJLa"), + } + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + if eConn != nil { + e.natsConnection = eConn + } + + if err := e.AddAdapters(adapters...); err != nil { + t.Fatalf("Error adding adapters: %v", err) + } + + return e +} + +func newStartedEngine(t *testing.T, name string, no *auth.NATSOptions, eConn sdp.EncodedConnection, adapters ...Adapter) *Engine { + t.Helper() + + e := newEngine(t, name, no, eConn, adapters...) + + err := e.Start(t.Context()) + if err != nil { + t.Fatalf("Error starting Engine: %v", err) + } + + t.Cleanup(func() { + err = e.Stop() + if err != nil { + t.Errorf("Error stopping Engine: %v", err) + } + }) + + return e +} + +func TestTrackQuery(t *testing.T) { + t.Run("With normal query", func(t *testing.T) { + t.Parallel() + + e := newStartedEngine(t, "TestTrackQuery_normal", nil, nil) + + u := uuid.New() + + qt := QueryTracker{ + Engine: e, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_LIST, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 10, + }, + UUID: u[:], + }, + } + + e.TrackQuery(u, &qt) + + if got, err := e.GetTrackedQuery(u); err == nil { + if got != &qt { + t.Errorf("Got mismatched QueryTracker objects %v and %v", got, &qt) + } + } else { + t.Error(err) + } + }) + + t.Run("With many queries", func(t *testing.T) { + t.Parallel() + + e := newStartedEngine(t, "TestTrackQuery_many", nil, nil) + + var wg sync.WaitGroup + + for i := range 1000 { + wg.Add(1) + go func(i int) { + defer wg.Done() + u := uuid.New() + + qt := QueryTracker{ + Engine: e, + Query: &sdp.Query{ + Type: "person", + Query: fmt.Sprintf("person-%v", i), + Method: sdp.QueryMethod_GET, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 10, + }, + UUID: u[:], + }, + } + + e.TrackQuery(u, &qt) + }(i) + } + + wg.Wait() + + if len(e.trackedQueries) != 1000 { + t.Errorf("Expected 1000 tracked queries, got %v", len(e.trackedQueries)) + } + }) +} + +func TestDeleteTrackedQuery(t *testing.T) { + t.Parallel() + e := newStartedEngine(t, "TestDeleteTrackedQuery", nil, nil) + + var wg sync.WaitGroup + + // Add and delete many query in parallel + for i := 1; i < 1000; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + u := uuid.New() + + qt := QueryTracker{ + Engine: e, + Query: &sdp.Query{ + Type: "person", + Query: fmt.Sprintf("person-%v", i), + Method: sdp.QueryMethod_GET, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 10, + }, + UUID: u[:], + }, + } + + e.TrackQuery(u, &qt) + wg.Add(1) + go func(u uuid.UUID) { + defer wg.Done() + e.DeleteTrackedQuery(u) + }(u) + }(i) + } + + wg.Wait() + + if len(e.trackedQueries) != 0 { + t.Errorf("Expected 0 tracked queries, got %v", len(e.trackedQueries)) + } +} + +func TestNats(t *testing.T) { + SkipWithoutNats(t) + + ec := EngineConfig{ + MaxParallelExecutions: 10, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + }, + NATSQueueName: "test", + } + + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + adapter := TestAdapter{} + adapter.cache = sdpcache.NewNoOpCache() + err = e.AddAdapters( + &adapter, + &TestAdapter{ + ReturnScopes: []string{ + sdp.WILDCARD, + }, + ReturnName: "test-adapter", + ReturnType: "test", + cache: sdpcache.NewNoOpCache(), + }, + ) + if err != nil { + t.Fatal(err) + } + + t.Run("Starting", func(t *testing.T) { + err := e.Start(t.Context()) + if err != nil { + t.Error(err) + } + + if e.natsConnection.Underlying().NumSubscriptions() != 4 { + t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) + } + }) + + t.Run("Restarting", func(t *testing.T) { + err := e.Stop() + if err != nil { + t.Error(err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Error(err) + } + + if e.natsConnection.Underlying().NumSubscriptions() != 4 { + t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) + } + }) + + t.Run("Handling a basic query", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + query := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "basic", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + } + + _, _, _, err := sdp.RunSourceQuerySync(context.Background(), query, sdp.DefaultStartTimeout, e.natsConnection) + if err != nil { + t.Error(err) + } + + if len(adapter.GetCalls) != 1 { + t.Errorf("expected 1 get call, got %v: %v", len(adapter.GetCalls), adapter.GetCalls) + } + }) + + t.Run("stopping", func(t *testing.T) { + err := e.Stop() + if err != nil { + t.Error(err) + } + }) +} + +func TestNatsCancel(t *testing.T) { + SkipWithoutNats(t) + + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + adapter := SpeedTestAdapter{ + QueryDelay: 2 * time.Second, + ReturnType: "person", + ReturnScopes: []string{"test"}, + } + + if err := e.AddAdapters(&adapter); err != nil { + t.Fatalf("Error adding adapters: %v", err) + } + + t.Run("Starting", func(t *testing.T) { + err := e.Start(t.Context()) + if err != nil { + t.Error(err) + } + }) + + t.Run("Cancelling queries", func(t *testing.T) { + conn := e.natsConnection + u := uuid.New() + + query := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "foo", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 100, + }, + Scope: "*", + UUID: u[:], + } + + responses := make(chan *sdp.QueryResponse, 1000) + progress, err := sdp.RunSourceQuery(t.Context(), query, sdp.DefaultStartTimeout, conn, responses) + if err != nil { + t.Error(err) + } + + time.Sleep(250 * time.Millisecond) + + err = conn.Publish(context.Background(), "cancel.all", &sdp.CancelQuery{ + UUID: u[:], + }) + if err != nil { + t.Error(err) + } + + // Read and discard all items and errors until they are closed + for range responses { + } + + time.Sleep(250 * time.Millisecond) + + if progress.Progress().Cancelled != 1 { + t.Errorf("Expected query to be cancelled, got\n%v", progress.String()) + } + }) + + t.Run("stopping", func(t *testing.T) { + err := e.Stop() + if err != nil { + t.Error(err) + } + }) +} + +func TestNatsConnections(t *testing.T) { + t.Run("with a bad hostname", func(t *testing.T) { + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + Servers: []string{"nats://bad.server"}, + ConnectionName: "test-disconnection", + ConnectionTimeout: time.Second, + MaxReconnects: 1, + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + err = e.Start(t.Context()) + + if err == nil { + t.Error("expected error but got nil") + } + }) + + t.Run("with a server that disconnects", func(t *testing.T) { + // We are running a custom server here so that we can control its lifecycle + opts := test.DefaultTestOptions + // Need to change this to avoid port clashes in github actions + opts.Port = 4111 + s := test.RunServer(&opts) + + if !s.ReadyForConnections(10 * time.Second) { + t.Fatal("Could not start goroutine NATS server") + } + + t.Cleanup(func() { + if s != nil { + s.Shutdown() + } + }) + + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: []string{"127.0.0.1:4111"}, + ConnectionName: "test-disconnection", + ConnectionTimeout: time.Second, + MaxReconnects: 10, + ReconnectWait: time.Second, + ReconnectJitter: time.Second, + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + + t.Log("Stopping NATS server") + s.Shutdown() + + for i := range 21 { + if i == 20 { + t.Errorf("Engine did not report a NATS disconnect after %v tries", i) + } + + if !e.IsNATSConnected() { + break + } + + time.Sleep(time.Second) + } + + // Reset the server + s = test.RunServer(&opts) + + // Wait for the server to start + s.ReadyForConnections(10 * time.Second) + + // Wait 2 more seconds for a reconnect + time.Sleep(2 * time.Second) + + for range 21 { + if e.IsNATSConnected() { + return + } + + time.Sleep(time.Second) + } + + t.Error("Engine should have reconnected but hasn't") + }) + + t.Run("with a server that takes a while to start", func(t *testing.T) { + // We are running a custom server here so that we can control its lifecycle + opts := test.DefaultTestOptions + // Need to change this to avoid port clashes in github actions + opts.Port = 4112 + + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 10, + RetryDelay: time.Second, + Servers: []string{"127.0.0.1:4112"}, + ConnectionName: "test-disconnection", + ConnectionTimeout: time.Second, + MaxReconnects: 10, + ReconnectWait: time.Second, + ReconnectJitter: time.Second, + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + var s *server.Server + + go func() { + // Start the server after a delay + time.Sleep(2 * time.Second) + + // We are running a custom server here so that we can control its lifecycle + s = test.RunServer(&opts) + + t.Cleanup(func() { + if s != nil { + s.Shutdown() + } + }) + }() + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestNATSFailureRestart(t *testing.T) { + restartTestOption := test.DefaultTestOptions + restartTestOption.Port = 4113 + + // We are running a custom server here so that we can control its lifecycle + s := test.RunServer(&restartTestOption) + + if !s.ReadyForConnections(10 * time.Second) { + t.Fatal("Could not start goroutine NATS server") + } + + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 10, + RetryDelay: time.Second, + Servers: []string{"127.0.0.1:4113"}, + ConnectionName: "test-disconnection", + ConnectionTimeout: time.Second, + MaxReconnects: 10, + ReconnectWait: 100 * time.Millisecond, + ReconnectJitter: 10 * time.Millisecond, + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + e.ConnectionWatchInterval = 1 * time.Second + + // Connect successfully + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err = e.Stop() + if err != nil { + t.Fatal(err) + } + }) + + // Lose the connection + t.Log("Stopping NATS server") + s.Shutdown() + s.WaitForShutdown() + + // The watcher should keep watching while the nats connection is + // RECONNECTING, once it's CLOSED however it won't keep trying to connect so + // we want to make sure that the watcher detects this and kills the whole + // thing + time.Sleep(2 * time.Second) + + s = test.RunServer(&restartTestOption) + if !s.ReadyForConnections(10 * time.Second) { + t.Fatal("Could not start goroutine NATS server a second time") + } + + t.Cleanup(func() { + s.Shutdown() + }) + + time.Sleep(3 * time.Second) + + if !e.IsNATSConnected() { + t.Error("NATS didn't manage to reconnect") + } +} + +func TestNatsAuth(t *testing.T) { + SkipWithoutNatsAuth(t) + + ec := EngineConfig{ + MaxParallelExecutions: 1, + SourceName: "nats-test", + NATSOptions: &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + TokenClient: GetTestOAuthTokenClient(t, "org_hdeUXbB55sMMvJLa"), + }, + NATSQueueName: "test", + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + adapter := TestAdapter{} + adapter.cache = sdpcache.NewNoOpCache() + if err := e.AddAdapters( + &adapter, + &TestAdapter{ + ReturnScopes: []string{ + sdp.WILDCARD, + }, + ReturnType: "test", + ReturnName: "test-adapter", + cache: sdpcache.NewNoOpCache(), + }, + ); err != nil { + t.Fatalf("Error adding adapters: %v", err) + } + + t.Run("Starting", func(t *testing.T) { + err := e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + + if e.natsConnection.Underlying().NumSubscriptions() != 4 { + t.Errorf("Expected engine to have 4 subscriptions, got %v", e.natsConnection.Underlying().NumSubscriptions()) + } + }) + + t.Run("Handling a basic query", func(t *testing.T) { + t.Cleanup(func() { + adapter.ClearCalls() + }) + + query := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "basic", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + } + + _, _, _, err := sdp.RunSourceQuerySync(t.Context(), query, sdp.DefaultStartTimeout, e.natsConnection) + if err != nil { + t.Error(err) + } + + if len(adapter.GetCalls) != 1 { + t.Errorf("expected 1 get call, got %v: %v", len(adapter.GetCalls), adapter.GetCalls) + } + }) + + t.Run("stopping", func(t *testing.T) { + err := e.Stop() + if err != nil { + t.Error(err) + } + }) +} + +func TestSetupMaxQueryTimeout(t *testing.T) { + t.Run("with no value", func(t *testing.T) { + ec := EngineConfig{} + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + + if e.MaxRequestTimeout != DefaultMaxRequestTimeout { + t.Errorf("max request timeout did not default. Got %v expected %v", e.MaxRequestTimeout.String(), DefaultMaxRequestTimeout.String()) + } + }) + + t.Run("with a value", func(t *testing.T) { + ec := EngineConfig{} + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + e.MaxRequestTimeout = 1 * time.Second + + if e.MaxRequestTimeout != 1*time.Second { + t.Errorf("max request timeout did not take provided value. Got %v expected %v", e.MaxRequestTimeout.String(), (1 * time.Second).String()) + } + }) +} + +func TestEngineHealthCheckHandlesNilConnection(t *testing.T) { + t.Parallel() + + t.Run("without connection", func(t *testing.T) { + t.Parallel() + e := newEngine(t, "TestEngineHealthCheckHandlesNilConnection_NoConn", &auth.NATSOptions{}, nil) + + assertHealthCheckDoesNotPanic(t, e) + }) + + t.Run("with dropped underlying connection", func(t *testing.T) { + t.Parallel() + e := newEngine(t, "TestEngineHealthCheckHandlesNilConnection_Dropped", &auth.NATSOptions{}, nil) + e.natsConnection = &sdp.EncodedConnectionImpl{} + + assertHealthCheckDoesNotPanic(t, e) + }) +} + +func assertHealthCheckDoesNotPanic(t *testing.T, e *Engine) { + t.Helper() + + defer func() { + if r := recover(); r != nil { + t.Fatalf("LivenessHealthCheck panic: %v", r) + } + }() + + ctx := context.Background() + if err := e.LivenessHealthCheck(ctx); err == nil { + t.Fatalf("expected LivenessHealthCheck to report disconnected NATS") + } +} + +var ( + testTokenSource oauth2.TokenSource + testTokenSourceMu sync.Mutex +) + +func GetTestOAuthTokenClient(t *testing.T, account string) auth.TokenClient { + var domain string + var clientID string + var clientSecret string + var exists bool + + errorFormat := "environment variable %v not found. Set up your test environment first. See: https://github.com/overmindtech/cli/go/auth0-test-data" + + // Read secrets form the environment + if domain, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_DOMAIN"); !exists || domain == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_DOMAIN") + t.Skip("Skipping due to missing environment setup") + } + + if clientID, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_ID"); !exists || clientID == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_ID") + t.Skip("Skipping due to missing environment setup") + } + + if clientSecret, exists = os.LookupEnv("OVERMIND_NTE_ALLPERMS_CLIENT_SECRET"); !exists || clientSecret == "" { + t.Errorf(errorFormat, "OVERMIND_NTE_ALLPERMS_CLIENT_SECRET") + t.Skip("Skipping due to missing environment setup") + } + + exchangeURL, err := GetWorkingTokenExchange() + if err != nil { + t.Skipf("Token exchange API server not available: %v", err) + return nil + } + + testTokenSourceMu.Lock() + defer testTokenSourceMu.Unlock() + if testTokenSource == nil { + ccc := auth.ClientCredentialsConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + } + testTokenSource = ccc.TokenSource( + t.Context(), + fmt.Sprintf("https://%v/oauth/token", domain), + os.Getenv("API_SERVER_AUDIENCE"), + ) + } + + return auth.NewOAuthTokenClient( + exchangeURL, + account, + testTokenSource, + ) +} diff --git a/go/discovery/enginerequests.go b/go/discovery/enginerequests.go new file mode 100644 index 00000000..affb09ca --- /dev/null +++ b/go/discovery/enginerequests.go @@ -0,0 +1,532 @@ +package discovery + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "github.com/sourcegraph/conc/pool" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// NewItemSubject Generates a random subject name for returning items e.g. +// return.item._INBOX.712ab421 +func NewItemSubject() string { + return fmt.Sprintf("return.item.%v", nats.NewInbox()) +} + +// NewResponseSubject Generates a random subject name for returning responses +// e.g. return.response._INBOX.978af6de +func NewResponseSubject() string { + return fmt.Sprintf("return.response.%v", nats.NewInbox()) +} + +// HandleQuery Handles a single query. This includes responses, linking +// etc. +func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { + var deadlineOverride bool + + // Respond saying we've got it + responder := sdp.ResponseSender{ + ResponseSubject: query.Subject(), + } + + var pub sdp.EncodedConnection + + if e.IsNATSConnected() { + pub = e.natsConnection + } else { + pub = NilConnection{} + } + + ru := uuid.New() + responder.Start( + ctx, + pub, + e.EngineConfig.SourceName, + ru, + ) + + // Ensure responder ends exactly once (prevents double-ending on panic) + var responderEndOnce sync.Once + defer func() { + // Safety net: if we panic before explicitly ending, mark as error + responderEndOnce.Do(func() { + responder.ErrorWithContext(ctx) + }) + }() + + // If there is no deadline OR further in the future than MaxRequestTimeout, clamp the deadline to MaxRequestTimeout + maxRequestDeadline := time.Now().Add(e.MaxRequestTimeout) + if query.GetDeadline() == nil || query.GetDeadline().AsTime().After(maxRequestDeadline) { + query.Deadline = timestamppb.New(maxRequestDeadline) + deadlineOverride = true + log.WithContext(ctx).WithField("ovm.deadline", query.GetDeadline().AsTime()).Debug("capping deadline to MaxRequestTimeout") + } + + // Add the query timeout to the context stack + ctx, cancel := query.TimeoutContext(ctx) + defer cancel() + + numExpandedQueries := len(e.sh.ExpandQuery(query)) + + if numExpandedQueries == 0 { + // If we don't have any relevant adapters, mark as done (OK) and exit + responderEndOnce.Do(func() { + responder.DoneWithContext(ctx) + }) + return + } + + // Extract and parse the UUID + u, uuidErr := uuid.FromBytes(query.GetUUID()) + + // 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.Bool("ovm.sdp.deadlineOverridden", deadlineOverride), + 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())), + attribute.Bool("ovm.sdp.followOnlyBlastPropagation", query.GetRecursionBehaviour().GetFollowOnlyBlastPropagation()), + ) + } + + qt := QueryTracker{ + Query: query, + Engine: e, + Context: ctx, + Cancel: cancel, + } + + if uuidErr == nil { + e.TrackQuery(u, &qt) + defer e.DeleteTrackedQuery(u) + } + + // the query tracker will send responses directly through the embedded + // engine's nats connection + _, _, _, err := qt.Execute(ctx) + + // End responder based on execution result + if err != nil { + if errors.Is(err, context.Canceled) { + responderEndOnce.Do(func() { + responder.CancelWithContext(ctx) + }) + } else { + responderEndOnce.Do(func() { + responder.ErrorWithContext(ctx) + }) + } + span.SetAttributes( + attribute.String("ovm.sdp.errorType", "OTHER"), + attribute.String("ovm.sdp.errorString", err.Error()), + ) + } else { + responderEndOnce.Do(func() { + responder.DoneWithContext(ctx) + }) + } +} + +var listExecutionPoolCount atomic.Int32 +var getExecutionPoolCount atomic.Int32 + +// ExecuteQuery Executes a single Query and returns the results without any +// linking. Will return an error if the Query couldn't be run. +// +// Items and errors will be sent to the supplied channels as they are found. +// Note that if these channels are not buffered, something will need to be +// receiving the results or this method will never finish. If results are not +// required the channels can be nil +func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses chan<- *sdp.QueryResponse) error { + span := trace.SpanFromContext(ctx) + + // Make sure we close channels once we're done + if responses != nil { + defer close(responses) + } + + if ctx.Err() != nil { + return ctx.Err() + } + + expanded := e.sh.ExpandQuery(query) + + span.SetAttributes( + attribute.Int("ovm.adapter.numExpandedQueries", len(expanded)), + ) + + if len(expanded) == 0 { + responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ + ErrorType: sdp.QueryError_NOSCOPE, + ErrorString: "no matching adapters found", + Scope: query.GetScope(), + }) + + return errors.New("no matching adapters found") + } + + // Since we need to wait for only the processing of this query's executions, we need a separate WaitGroup here + // Overall MaxParallelExecutions evaluation is handled by e.executionPool + wg := sync.WaitGroup{} + expandedMutex := sync.RWMutex{} + expandedMutex.RLock() + for q, adapter := range expanded { + wg.Add(1) + // localize values for the closure below + localQ, localAdapter := q, adapter + + var p *pool.Pool + if localQ.GetMethod() == sdp.QueryMethod_LIST { + p = e.listExecutionPool + listExecutionPoolCount.Add(1) + } else { + p = e.getExecutionPool + getExecutionPoolCount.Add(1) + } + + // push all queued items through a goroutine to avoid blocking `ExecuteQuery` from progressing + // as `executionPool.Go()` will block once the max parallelism is hit + go func() { + // queue everything into the execution pool + defer tracing.LogRecoverToReturn(ctx, "ExecuteQuery outer") + span.SetAttributes( + attribute.Int("ovm.discovery.listExecutionPoolCount", int(listExecutionPoolCount.Load())), + attribute.Int("ovm.discovery.getExecutionPoolCount", int(getExecutionPoolCount.Load())), + ) + p.Go(func() { + defer tracing.LogRecoverToReturn(ctx, "ExecuteQuery inner") + defer func() { + // Mark the work as done. This happens before we start + // waiting on `expandedMutex` below, to ensure that the + // queues can continue executing even if we are waiting on + // the mutex. + wg.Done() + + // Delete our query from the map so that we can track which + // ones are still running + expandedMutex.Lock() + defer expandedMutex.Unlock() + delete(expanded, localQ) + }() + defer func() { + if localQ.GetMethod() == sdp.QueryMethod_LIST { + listExecutionPoolCount.Add(-1) + } else { + getExecutionPoolCount.Add(-1) + } + }() + + // If the context is cancelled, don't even bother doing + // anything. Since the `p.Go` will block, it's possible that if + // the pool was exhausted, the context could be cancelled before + // the goroutine is executed + if ctx.Err() != nil { + return + } + + // Execute the query against the adapter + e.Execute(ctx, localQ, localAdapter, responses) + }) + }() + } + expandedMutex.RUnlock() + + waitGroupDone := make(chan struct{}) + go func() { + wg.Wait() + close(waitGroupDone) + }() + + select { + case <-waitGroupDone: + // All adapters have finished + case <-ctx.Done(): + // The context was cancelled, this should have propagated to all the + // adapters and therefore we should see the wait group finish very + // quickly now. We will check this though to make sure. This will wait + // until we reach Change Analysis SLO violation territory. If this is + // too quick, we are only spamming logs for nothing. + longRunningAdaptersTimeout := 2 * time.Minute + + // Wait for the wait group, but ping the logs if it's taking + // too long + func() { + for { + select { + case <-waitGroupDone: + return + case <-time.After(longRunningAdaptersTimeout): + // If we're here, then the wait group didn't finish in time + expandedMutex.RLock() + for q, adapter := range expanded { + // There is a honeycomb trigger for this message: + // + // https://ui.honeycomb.io/overmind/environments/prod/datasets/kubernetes-metrics/triggers/saWNAnCAXNb + // + // This is to ensure we are aware of any adapters that + // are taking too long to respond to a query, which + // 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.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() + // the query is already bolloxed up, we don't need continue to wait and spam the logs any more + return + } + } + }() + } + + // If the context is cancelled, return that error + if ctx.Err() != nil { + return ctx.Err() + } + + return nil +} + +// Runs a query against an adapter. Returns an error if the query fails in a +// "fatal" way that should consider the query as failed. Other non-fatal errors +// should be sent on the stream. Channels for items and errors will NOT be +// closed by this function, the caller should do that as this will likely be +// 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.query", q.GetQuery()), + )) + 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 + // a List to block all subsequent Get queries until it is done + switch q.GetMethod() { + case sdp.QueryMethod_GET: + e.gfm.GetLock(q.GetScope(), q.GetType()) + defer e.gfm.GetUnlock(q.GetScope(), q.GetType()) + case sdp.QueryMethod_LIST: + e.gfm.ListLock(q.GetScope(), q.GetType()) + defer e.gfm.ListUnlock(q.GetScope(), q.GetType()) + case sdp.QueryMethod_SEARCH: + // We don't need to lock for a search since they are independent and + // will only ever have a cache hit if the query is identical + } + + // 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 + // it. + go func() { + <-ctx.Done() + if ctx.Err() != nil { + // get a fresh copy of the span to avoid data races + span := trace.SpanFromContext(ctx) + span.RecordError(ctx.Err()) + span.SetAttributes( + attribute.Bool("ovm.discover.hang", true), + ) + span.End() + } + }() + + // Set up handling for the items and errors that are returned before they + // are passed back to the caller + var numItems atomic.Int32 + var numErrs atomic.Int32 + var itemHandler ItemHandler = func(item *sdp.Item) { + if item == nil { + return + } + + if err := item.Validate(); err != nil { + span.RecordError(err) + responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ + UUID: q.GetUUID(), + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + Scope: q.GetScope(), + ResponderName: e.EngineConfig.SourceName, + ItemType: q.GetType(), + }) + return + } + + // Store metadata + item.Metadata = &sdp.Metadata{ + Timestamp: timestamppb.New(time.Now()), + SourceName: adapter.Name(), + SourceQuery: q, + } + + // Mark the item as hidden if the adapter is hidden + if hs, ok := adapter.(HiddenAdapter); ok { + item.Metadata.Hidden = hs.Hidden() + } + + // Send the item back to the caller + numItems.Add(1) + responses <- sdp.NewQueryResponseFromItem(item) + } + var errHandler ErrHandler = func(err error) { + if err == nil { + return + } + // add a recover to prevent panic from stream error handler. + defer tracing.LogRecoverToReturn(ctx, "StreamErrorHandler") + + // Record the error in the trace + span.RecordError(err, trace.WithStackTrace(true)) + + // Send the error back to the caller + numErrs.Add(1) + responses <- queryResponseFromError(err, q, adapter, e.EngineConfig.SourceName) + } + stream := NewQueryResultStream(itemHandler, errHandler) + + // Check that our context is okay before doing anything expensive + if ctx.Err() != nil { + span.RecordError(ctx.Err()) + + responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ + UUID: q.GetUUID(), + ErrorType: sdp.QueryError_OTHER, + ErrorString: ctx.Err().Error(), + Scope: q.GetScope(), + ResponderName: e.EngineConfig.SourceName, + ItemType: q.GetType(), + }) + return + } + + switch q.GetMethod() { + case sdp.QueryMethod_GET: + span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) + newItem, err := adapter.Get(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache()) + + if newItem != nil { + stream.SendItem(newItem) + } + if err != nil { + stream.SendError(err) + } + case sdp.QueryMethod_LIST: + if listStreamingAdapter, ok := adapter.(ListStreamableAdapter); ok { + // Prefer the streaming methods if they are available + span.SetAttributes(attribute.Bool("ovm.sdp.streaming", true)) + listStreamingAdapter.ListStream(ctx, q.GetScope(), q.GetIgnoreCache(), stream) + } else if listableAdapter, ok := adapter.(ListableAdapter); ok { + // Fall back to the non-streaming methods + span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) + resultItems, err := listableAdapter.List(ctx, q.GetScope(), q.GetIgnoreCache()) + + for _, i := range resultItems { + stream.SendItem(i) + } + if err != nil { + stream.SendError(err) + } + } else { + // Log the error instead of sending it over the stream + log.WithContext(ctx).WithFields(log.Fields{ + "ovm.adapter.name": adapter.Name(), + "ovm.sdp.type": q.GetType(), + "ovm.sdp.scope": q.GetScope(), + }).Warn("adapter is not listable") + } + case sdp.QueryMethod_SEARCH: + if searchStreamingAdapter, ok := adapter.(SearchStreamableAdapter); ok { + // Prefer the streaming methods if they are available + span.SetAttributes(attribute.Bool("ovm.sdp.streaming", true)) + searchStreamingAdapter.SearchStream(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache(), stream) + } else if searchableAdapter, ok := adapter.(SearchableAdapter); ok { + // Fall back to the non-streaming methods + span.SetAttributes(attribute.Bool("ovm.sdp.streaming", false)) + resultItems, err := searchableAdapter.Search(ctx, q.GetScope(), q.GetQuery(), q.GetIgnoreCache()) + + for _, i := range resultItems { + stream.SendItem(i) + } + if err != nil { + stream.SendError(err) + } + } else { + // Log the error instead of sending it over the stream + log.WithContext(ctx).WithFields(log.Fields{ + "ovm.adapter.name": adapter.Name(), + "ovm.sdp.type": q.GetType(), + "ovm.sdp.scope": q.GetScope(), + }).Warn("adapter is not searchable") + } + } + + span.SetAttributes( + attribute.Int("ovm.adapter.numItems", int(numItems.Load())), + attribute.Int("ovm.adapter.numErrors", int(numErrs.Load())), + ) +} + +// queryResponseFromError converts an error into a QueryResponse. This takes +// care to not double-wrap `sdp.QueryError` errors. +func queryResponseFromError(err error, q *sdp.Query, adapter Adapter, sourceName string) *sdp.QueryResponse { + var sdpErr *sdp.QueryError + if !errors.As(err, &sdpErr) { + sdpErr = &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + } + } + + // Add details that might not be populated by the adapter + sdpErr.Scope = q.GetScope() + sdpErr.UUID = q.GetUUID() + sdpErr.SourceName = adapter.Name() + sdpErr.ItemType = adapter.Metadata().GetType() + sdpErr.ResponderName = sourceName + + return sdp.NewQueryResponseFromError(sdpErr) +} diff --git a/go/discovery/enginerequests_test.go b/go/discovery/enginerequests_test.go new file mode 100644 index 00000000..a8894d05 --- /dev/null +++ b/go/discovery/enginerequests_test.go @@ -0,0 +1,460 @@ +package discovery + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" + "github.com/sourcegraph/conc/pool" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// executeQuerySync Executes a Query, waiting for all results, then returns them +// along with the error, rather than using channels. The singular error sill only +// be returned if the query could not be executed, otherwise all errors will be +// in the slice +func (e *Engine) executeQuerySync(ctx context.Context, q *sdp.Query) ([]*sdp.Item, []*sdp.Edge, []*sdp.QueryError, error) { + responseChan := make(chan *sdp.QueryResponse, 100_000) + items := make([]*sdp.Item, 0) + edges := make([]*sdp.Edge, 0) + errs := make([]*sdp.QueryError, 0) + + err := e.ExecuteQuery(ctx, q, responseChan) + + for r := range responseChan { + switch r := r.GetResponseType().(type) { + case *sdp.QueryResponse_NewItem: + items = append(items, r.NewItem) + case *sdp.QueryResponse_Edge: + edges = append(edges, r.Edge) + case *sdp.QueryResponse_Error: + errs = append(errs, r.Error) + } + } + + return items, edges, errs, err +} + +func TestExecuteQuery(t *testing.T) { + adapter := TestAdapter{ + ReturnType: "person", + ReturnScopes: []string{"test"}, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestExecuteQuery", + &auth.NATSOptions{ + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + }, + nil, + &adapter, + ) + + 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", + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 3, + }, + } + + items, _, errs, err := e.executeQuerySync(context.Background(), q) + if err != nil { + t.Error(err) + } + + for _, e := range errs { + t.Error(e) + } + + if x := len(adapter.GetCalls); x != 1 { + t.Errorf("expected adapter's Get() to have been called 1 time, got %v", x) + } + + if len(items) == 0 { + t.Fatal("expected 1 item, got none") + } + + if len(items) > 1 { + t.Errorf("expected 1 item, got %v", items) + } + + 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") + } + }) + + t.Run("Wrong scope Get query", func(t *testing.T) { + q := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "foo", + Scope: "wrong", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), q) + + if err == nil { + t.Error("expected error but got nil") + } + + if len(errs) == 1 { + if errs[0].GetErrorType() != sdp.QueryError_NOSCOPE { + t.Errorf("expected error type to be NOSCOPE, got %v", errs[0].GetErrorType()) + } + } else { + t.Errorf("expected 1 error, got %v", len(errs)) + } + }) + + t.Run("Wrong type Get query", func(t *testing.T) { + q := &sdp.Query{ + Type: "house", + Method: sdp.QueryMethod_GET, + Query: "foo", + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + } + + _, _, errs, err := e.executeQuerySync(context.Background(), q) + + if err == nil { + t.Error("expected error but got nil") + } + + if len(errs) == 1 { + if errs[0].GetErrorType() != sdp.QueryError_NOSCOPE { + t.Errorf("expected error type to be NOSCOPE, got %v", errs[0].GetErrorType()) + } + } else { + t.Errorf("expected 1 error, got %v", len(errs)) + } + }) + + t.Run("Basic List query", func(t *testing.T) { + q := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_LIST, + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 5, + }, + } + + items, _, errs, err := e.executeQuerySync(context.Background(), q) + if err != nil { + t.Error(err) + } + + for _, e := range errs { + t.Error(e) + } + + if len(items) < 1 { + t.Error("expected at least one item") + } + }) + + t.Run("Basic Search query", func(t *testing.T) { + q := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_SEARCH, + Query: "TEST", + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 5, + }, + } + + items, _, errs, err := e.executeQuerySync(context.Background(), q) + if err != nil { + t.Error(err) + } + + for _, e := range errs { + t.Error(e) + } + + if len(items) < 1 { + t.Error("expected at least one item") + } + }) +} + +func TestHandleQuery(t *testing.T) { + personAdapter := TestAdapter{ + ReturnType: "person", + ReturnScopes: []string{ + "test1", + "test2", + }, + cache: sdpcache.NewNoOpCache(), + } + + dogAdapter := TestAdapter{ + ReturnType: "dog", + ReturnScopes: []string{ + "test1", + "testA", + "testB", + }, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestHandleQuery", nil, nil, &personAdapter, &dogAdapter) + + t.Run("Wildcard type should be expanded", func(t *testing.T) { + t.Cleanup(func() { + personAdapter.ClearCalls() + dogAdapter.ClearCalls() + }) + + req := sdp.Query{ + Type: sdp.WILDCARD, + Method: sdp.QueryMethod_GET, + Query: "Dylan", + Scope: "test1", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + } + + // Run the handler + e.HandleQuery(context.Background(), &req) + + // I'm expecting both adapter to get a query since the type was * + if l := len(personAdapter.GetCalls); l != 1 { + t.Errorf("expected person backend to have 1 Get call, got %v", l) + } + + if l := len(dogAdapter.GetCalls); l != 1 { + t.Errorf("expected dog backend to have 1 Get call, got %v", l) + } + }) + + t.Run("Wildcard scope should be expanded", func(t *testing.T) { + t.Cleanup(func() { + personAdapter.ClearCalls() + dogAdapter.ClearCalls() + }) + + req := sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan1", + Scope: sdp.WILDCARD, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + } + + // Run the handler + e.HandleQuery(context.Background(), &req) + + if l := len(personAdapter.GetCalls); l != 2 { + t.Errorf("expected person backend to have 2 Get calls, got %v", l) + } + + if l := len(dogAdapter.GetCalls); l != 0 { + t.Errorf("expected dog backend to have 0 Get calls, got %v", l) + } + }) +} + +func TestWildcardAdapterExpansion(t *testing.T) { + personAdapter := TestAdapter{ + ReturnType: "person", + ReturnScopes: []string{ + sdp.WILDCARD, + }, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestWildcardAdapterExpansion", nil, nil, &personAdapter) + + t.Run("query scope should be preserved", func(t *testing.T) { + req := sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan1", + Scope: "something.specific", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + } + + // Run the handler + e.HandleQuery(context.Background(), &req) + + if len(personAdapter.GetCalls) != 1 { + t.Errorf("expected 1 get call got %v", len(personAdapter.GetCalls)) + } + + if len(personAdapter.GetCalls) == 0 { + t.Fatal("Can't continue without calls") + } + + call := personAdapter.GetCalls[0] + + if expected := "something.specific"; call[0] != expected { + t.Errorf("expected scope to be %v, got %v", expected, call[0]) + } + + if expected := "Dylan1"; call[1] != expected { + t.Errorf("expected query to be %v, got %v", expected, call[1]) + } + }) +} + +func TestSendQuerySync(t *testing.T) { + SkipWithoutNats(t) + + ctx := context.Background() + + ctx, span := tracing.Tracer().Start(ctx, "TestSendQuerySync") + defer span.End() + + adapter := TestAdapter{ + ReturnType: "person", + ReturnScopes: []string{ + "test", + }, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestSendQuerySync", nil, nil, &adapter) + + p := pool.New() + for range 250 { + p.Go(func() { + u := uuid.New() + t.Log("starting query: ", u) + + var items []*sdp.Item + + query := &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan", + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + IgnoreCache: false, + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(10 * time.Minute)), + } + + items, _, errs, err := sdp.RunSourceQuerySync(ctx, query, 1*time.Second, e.natsConnection) + if err != nil { + t.Error(err) + } + + if len(errs) != 0 { + for _, err := range errs { + t.Error(err) + } + } + + if len(items) != 1 { + t.Fatalf("expected 1 item, got %v: %v", len(items), items) + } + }) + } + + p.Wait() +} + +func TestExpandQuery(t *testing.T) { + t.Run("with a single adapter with a single scope", func(t *testing.T) { + simple := TestAdapter{ + ReturnScopes: []string{ + "test1", + }, + cache: sdpcache.NewNoOpCache(), + } + e := newStartedEngine(t, "TestExpandQuery", nil, nil, &simple) + + e.HandleQuery(context.Background(), &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Debby", + Scope: "*", + }) + + if expected := 1; len(simple.GetCalls) != expected { + t.Errorf("Expected %v calls, got %v", expected, len(simple.GetCalls)) + } + }) + + t.Run("with a single adapter with many scopes", func(t *testing.T) { + many := TestAdapter{ + ReturnName: "many", + ReturnScopes: []string{ + "test1", + "test2", + "test3", + }, + cache: sdpcache.NewNoOpCache(), + } + e := newStartedEngine(t, "TestExpandQuery", nil, nil, &many) + + e.HandleQuery(context.Background(), &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Debby", + Scope: "*", + }) + + if expected := 3; len(many.GetCalls) != expected { + t.Errorf("Expected %v calls, got %v", expected, many.GetCalls) + } + }) + + t.Run("with a single wildcard adapter", func(t *testing.T) { + sx := TestAdapter{ + ReturnType: "person", + ReturnName: "sx", + ReturnScopes: []string{ + sdp.WILDCARD, + }, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestExpandQuery", nil, nil, &sx) + + e.HandleQuery(context.Background(), &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_LIST, + Query: "Rachel", + Scope: "*", + }) + + if expected := 1; len(sx.ListCalls) != expected { + t.Errorf("Expected %v calls, got %v", expected, sx.ListCalls) + } + }) +} diff --git a/go/discovery/getfindmutex.go b/go/discovery/getfindmutex.go new file mode 100644 index 00000000..d7f26602 --- /dev/null +++ b/go/discovery/getfindmutex.go @@ -0,0 +1,80 @@ +package discovery + +import ( + "fmt" + "sync" +) + +// GetListMutex A modified version of a RWMutex. Many get locks can be held but +// only one List lock. A waiting List lock (even if it hasn't been locked, just +// if someone is waiting) blocks all other get locks until it unlocks. +// +// The intended usage of this is that it will allow an adapter which is trying to +// process many queries at once, to process a LIST query before any GET +// queries, since it's likely that once LIST has been run, subsequent GET +// queries will be able to be served from cache +type GetListMutex struct { + mutexMap map[string]*sync.RWMutex + mapLock sync.Mutex +} + +// GetLock Gets a lock that can be held by an unlimited number of goroutines, +// these locks are only blocked by ListLocks. A type and scope must be +// provided since a Get in one type (or scope) should not be blocked by a List +// in another +func (g *GetListMutex) GetLock(scope string, typ string) { + g.mutexFor(scope, typ).RLock() +} + +// GetUnlock Unlocks the GetLock. This must be called once for each GetLock +// otherwise it will be impossible to ever obtain a ListLock +func (g *GetListMutex) GetUnlock(scope string, typ string) { + g.mutexFor(scope, typ).RUnlock() +} + +// ListLock An exclusive lock. Ensure that all GetLocks have been unlocked and +// stops any more from being obtained. Provide a type and scope to ensure that +// the lock is only help for that type and scope combination rather than +// locking the whole engine +func (g *GetListMutex) ListLock(scope string, typ string) { + g.mutexFor(scope, typ).Lock() +} + +// ListUnlock Unlocks a ListLock +func (g *GetListMutex) ListUnlock(scope string, typ string) { + g.mutexFor(scope, typ).Unlock() +} + +// mutexFor Returns the relevant RWMutex for a given scope and type, creating +// and storing a new one if needed +func (g *GetListMutex) mutexFor(scope string, typ string) *sync.RWMutex { + var mutex *sync.RWMutex + var ok bool + + keyName := g.keyName(scope, typ) + + g.mapLock.Lock() + defer g.mapLock.Unlock() + + // Create the map if needed + if g.mutexMap == nil { + g.mutexMap = make(map[string]*sync.RWMutex) + } + + // Get the mutex from storage + mutex, ok = g.mutexMap[keyName] + + // If the mutex wasn't found for this key, create a new one + if !ok { + mutex = &sync.RWMutex{} + g.mutexMap[keyName] = mutex + } + + return mutex +} + +// keyName Returns the name of the key for a given scope and type combo for +// use with the mutexMap +func (g *GetListMutex) keyName(scope string, typ string) string { + return fmt.Sprintf("%v.%v", scope, typ) +} diff --git a/go/discovery/getfindmutex_test.go b/go/discovery/getfindmutex_test.go new file mode 100644 index 00000000..ff7b4916 --- /dev/null +++ b/go/discovery/getfindmutex_test.go @@ -0,0 +1,194 @@ +package discovery + +import ( + "context" + "sync" + "testing" + "time" +) + +func TestGetLock(t *testing.T) { + t.Run("many get locks can be held at once", func(t *testing.T) { + var gfm GetListMutex + ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) + doneChan := make(chan bool) + + go func() { + gfm.GetLock("testScope", "testType") + gfm.GetLock("testScope", "testType") + gfm.GetLock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + doneChan <- true + }() + + select { + case <-ctx.Done(): + t.Error("Timeout") + case <-doneChan: + } + + cancel() + }) + + t.Run("many find locks from different types and scopes can be held at once", func(t *testing.T) { + var gfm GetListMutex + ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) + doneChan := make(chan bool) + + go func() { + gfm.ListLock("testScope1", "testType1") + gfm.ListLock("testScope1", "testType2") + gfm.ListLock("testScope2", "testType") + gfm.ListLock("testScope3", "testType") + gfm.ListUnlock("testScope1", "testType1") + gfm.ListUnlock("testScope1", "testType2") + gfm.ListUnlock("testScope2", "testType") + gfm.ListUnlock("testScope3", "testType") + doneChan <- true + }() + + select { + case <-ctx.Done(): + t.Error("Timeout") + case <-doneChan: + } + + cancel() + }) + + t.Run("get locks are blocked by a find lock", func(t *testing.T) { + var gfm GetListMutex + ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) + getChan := make(chan bool) + findChan := make(chan bool) + + gfm.ListLock("testScope", "testType") + + go func() { + gfm.GetLock("testScope", "testType") + gfm.GetLock("testScope", "testType") + gfm.GetLock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + gfm.GetUnlock("testScope", "testType") + getChan <- true + }() + + go func() { + // Seep for long enough to allow the above goroutine to complete if not + // blocked + time.Sleep(10 * time.Millisecond) + + findChan <- true + }() + + select { + case <-ctx.Done(): + t.Error("Timeout") + case <-getChan: + t.Error("Get locks were not blocked") + case <-findChan: + // This is the expected path + } + + cancel() + }) + + t.Run("active gets block finds", func(t *testing.T) { + var gfm GetListMutex + var actionWG sync.WaitGroup + ctx, cancel := context.WithTimeout(context.Background(), (1 * time.Second)) + + order := make([]string, 0) + actionChan := make(chan string) + doneChan := make(chan bool) + var wg sync.WaitGroup + wg.Add(3) + + go func() { + defer wg.Done() + gfm.GetLock("testScope", "testType") + actionChan <- "getLock1" + + // do some work + time.Sleep(50 * time.Millisecond) + + gfm.GetUnlock("testScope", "testType") + + }() + + go func() { + defer wg.Done() + time.Sleep(10 * time.Millisecond) + + gfm.ListLock("testScope", "testType") + + actionChan <- "findLock1" + + // do some work + time.Sleep(50 * time.Millisecond) + + gfm.ListUnlock("testScope", "testType") + + }() + + go func() { + defer wg.Done() + time.Sleep(20 * time.Millisecond) + + gfm.GetLock("testScope", "testType") + + actionChan <- "getLock2" + + // do some work + time.Sleep(50 * time.Millisecond) + + gfm.GetUnlock("testScope", "testType") + + }() + + actionWG.Add(1) + + go func() { + for action := range actionChan { + order = append(order, action) + } + actionWG.Done() + }() + + go func(t *testing.T) { + wg.Wait() + close(actionChan) + actionWG.Wait() + + // The expected order is: Firstly getLock1 since nothing else is waiting + // for a lock. While this one is working there is a query for a + // findLock, then a getLock. The findLock should block the getLock until + // it is done + if order[0] != "getLock1" { + t.Errorf("expected getLock1 to be first. Order was: %v", order) + } + + if order[1] != "findLock1" { + t.Errorf("expected findLock1 to be middle. Order was: %v", order) + } + + if order[2] != "getLock2" { + t.Errorf("expected getLock2 to be last. Order was: %v", order) + } + + doneChan <- true + }(t) + + select { + case <-ctx.Done(): + t.Errorf("timeout. Completed actions were: %v", order) + case <-doneChan: + // This is good + } + + cancel() + }) +} diff --git a/go/discovery/heartbeat.go b/go/discovery/heartbeat.go new file mode 100644 index 00000000..0ae6a862 --- /dev/null +++ b/go/discovery/heartbeat.go @@ -0,0 +1,165 @@ +package discovery + +import ( + "context" + "errors" + "time" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/durationpb" +) + +const DefaultHeartbeatFrequency = 5 * time.Minute + +var ErrNoHealthcheckDefined = errors.New("no healthcheck defined") + +// HeartbeatSender sends a heartbeat to the management API, this is called at +// `DefaultHeartbeatFrequency` by default when the engine is running, or +// `StartSendingHeartbeats` has been called manually. Users can also call this +// method to immediately send a heartbeat if required. Pass non-`nil` error +// to indicate that the engine is in an error state, this will be sent to the +// management API and will be displayed in the UI. +func (e *Engine) SendHeartbeat(ctx context.Context, customErr error) error { + // Get span from context + span := trace.SpanFromContext(ctx) + + // Read memory stats and add them to the span + memStats := tracing.ReadMemoryStats() + tracing.SetMemoryAttributes(span, "ovm.heartbeat", memStats) + span.SetAttributes( + 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), + ) + + if e.EngineConfig.HeartbeatOptions == nil { + return ErrNoHealthcheckDefined + } + + // No-op when running without management API (e.g. ALLOW_UNAUTHENTICATED local dev) + if e.EngineConfig.HeartbeatOptions.ManagementClient == nil { + log.WithFields(log.Fields{ + "source_name": e.EngineConfig.SourceName, + "engine_type": e.EngineConfig.EngineType, + }).Info("Running in unauthenticated mode; no heartbeats will be sent") + return nil + } + + // Collect all health check errors + var allErrors []error + if customErr != nil { + allErrors = append(allErrors, customErr) + } + + // Check for persistent initialization errors first + if initErr := e.GetInitError(); initErr != nil { + allErrors = append(allErrors, initErr) + } + + // Check adapter readiness (ReadinessCheck) - with timeout to prevent hanging + if e.EngineConfig.HeartbeatOptions.ReadinessCheck != nil { + // Add timeout for readiness checks to prevent hanging heartbeats + readinessCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := e.EngineConfig.HeartbeatOptions.ReadinessCheck(readinessCtx); err != nil { + allErrors = append(allErrors, err) + } + } + + // Combine all errors + var heartbeatError *string + if len(allErrors) > 0 { + combinedError := errors.Join(allErrors...) + heartbeatError = new(string) + *heartbeatError = combinedError.Error() + } + + var engineUUID []byte + + if e.EngineConfig.SourceUUID != uuid.Nil { + engineUUID = e.EngineConfig.SourceUUID[:] + } + + availableScopes, adapterMetadata := e.GetAvailableScopesAndMetadata() + + // Calculate the duration for the next heartbeat, based on the current + // frequency x2.5 to give us some leeway + nextHeartbeat := time.Duration(float64(e.EngineConfig.HeartbeatOptions.Frequency) * 2.5) + + _, err := e.EngineConfig.HeartbeatOptions.ManagementClient.SubmitSourceHeartbeat(ctx, &connect.Request[sdp.SubmitSourceHeartbeatRequest]{ + Msg: &sdp.SubmitSourceHeartbeatRequest{ + UUID: engineUUID, + Version: e.EngineConfig.Version, + Name: e.EngineConfig.SourceName, + Type: e.EngineConfig.EngineType, + AvailableScopes: availableScopes, + AdapterMetadata: adapterMetadata, + Managed: e.EngineConfig.OvermindManagedSource, + Error: heartbeatError, + NextHeartbeatMax: durationpb.New(nextHeartbeat), + }, + }) + + // Update heartbeat status tracking + e.heartbeatStatusMutex.Lock() + if err != nil { + e.lastHeartbeatError = err + } else { + e.lastSuccessfulHeartbeat = time.Now() + e.lastHeartbeatError = nil + } + e.heartbeatStatusMutex.Unlock() + + return err +} + +// Starts sending heartbeats at the specified frequency. These will be sent in +// the background and this function will return immediately. Heartbeats are +// automatically started when the engine started, but if an adapter has startup +// steps that take a long time, or are liable to fail, the user may want to +// start the heartbeats first so that users can see that the adapter has failed +// to start. +// +// If this is called multiple times, nothing will happen. Heartbeats will be +// stopped when the engine is stopped, or when the provided context is canceled. +// +// This will send one heartbeat initially when the method is called, and will +// then run in a background goroutine that sends heartbeats at the specified +// frequency, and will stop when the provided context is canceled. +func (e *Engine) StartSendingHeartbeats(ctx context.Context) { + if e.EngineConfig.HeartbeatOptions == nil || e.EngineConfig.HeartbeatOptions.Frequency == 0 || e.heartbeatCancel != nil { + return + } + + var heartbeatContext context.Context + heartbeatContext, e.heartbeatCancel = context.WithCancel(ctx) + + // Send one heartbeat at the beginning + err := e.SendHeartbeat(heartbeatContext, nil) + if err != nil { + log.WithError(err).Error("Failed to send heartbeat") + } + + go func() { + ticker := time.NewTicker(e.EngineConfig.HeartbeatOptions.Frequency) + defer ticker.Stop() + + for { + select { + case <-heartbeatContext.Done(): + return + case <-ticker.C: + err := e.SendHeartbeat(heartbeatContext, nil) + if err != nil { + log.WithError(err).Error("Failed to send heartbeat") + } + } + } + }() +} diff --git a/go/discovery/heartbeat_test.go b/go/discovery/heartbeat_test.go new file mode 100644 index 00000000..94e55915 --- /dev/null +++ b/go/discovery/heartbeat_test.go @@ -0,0 +1,207 @@ +package discovery + +import ( + "context" + "slices" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" +) + +type testHeartbeatClient struct { + // Requests will be sent to this channel + Requests chan *connect.Request[sdp.SubmitSourceHeartbeatRequest] + // Responses should be sent here + Responses chan *connect.Response[sdp.SubmitSourceHeartbeatResponse] +} + +func (t testHeartbeatClient) SubmitSourceHeartbeat(ctx context.Context, req *connect.Request[sdp.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp.SubmitSourceHeartbeatResponse], error) { + t.Requests <- req + return <-t.Responses, nil +} + +func TestHeartbeats(t *testing.T) { + name := t.Name() + u := uuid.New() + version := "v0.0.0-test" + engineType := "aws" + + requests := make(chan *connect.Request[sdp.SubmitSourceHeartbeatRequest], 1) + responses := make(chan *connect.Response[sdp.SubmitSourceHeartbeatResponse], 1) + + heartbeatOptions := HeartbeatOptions{ + ManagementClient: testHeartbeatClient{ + Requests: requests, + Responses: responses, + }, + } + ec := EngineConfig{ + SourceName: name, + SourceUUID: u, + Version: version, + EngineType: engineType, + HeartbeatOptions: &heartbeatOptions, + } + e, _ := NewEngine(&ec) + + if err := e.AddAdapters( + &TestAdapter{ + ReturnScopes: []string{"test"}, + ReturnType: "test-type", + ReturnName: "test-name", + }, + &TestAdapter{ + ReturnScopes: []string{"test"}, + ReturnType: "test-type2", + ReturnName: "test-name2", + }, + ); err != nil { + t.Fatalf("unexpected error adding adapters: %v", err) + } + + t.Run("sendHeartbeat when healthy", func(t *testing.T) { + ec.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { + return nil + } + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + err := e.SendHeartbeat(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + req := <-requests + + if reqUUID, err := uuid.FromBytes(req.Msg.GetUUID()); err == nil { + if reqUUID != u { + t.Errorf("expected uuid %v, got %v", u, reqUUID) + } + } else { + t.Errorf("error parsing uuid: %v", err) + } + + if req.Msg.GetVersion() != version { + t.Errorf("expected version %v, got %v", version, req.Msg.GetVersion()) + } + + if req.Msg.GetName() != name { + t.Errorf("expected name %v, got %v", name, req.Msg.GetName()) + } + + if req.Msg.GetType() != engineType { + t.Errorf("expected type %v, got %v", engineType, req.Msg.GetType()) + } + + if req.Msg.GetManaged() != sdp.SourceManaged_LOCAL { + t.Errorf("expected managed %v, got %v", sdp.SourceManaged_LOCAL, req.Msg.GetManaged()) + } + + if req.Msg.GetError() != "" { + t.Errorf("expected no error, got %v", req.Msg.GetError()) + } + + reqAvailableScopes := req.Msg.GetAvailableScopes() + + if len(reqAvailableScopes) != 1 { + t.Errorf("expected 1 scope, got %v", len(reqAvailableScopes)) + } + + if !slices.Contains(reqAvailableScopes, "test") { + t.Errorf("expected scope 'test' to be present in the response") + } + + reqAdapterMetadata := req.Msg.GetAdapterMetadata() + + if len(reqAdapterMetadata) != 2 { + t.Errorf("expected 2 adapter metadata, got %v", len(reqAdapterMetadata)) + } + }) + + t.Run("sendHeartbeat when unhealthy", func(t *testing.T) { + e.EngineConfig.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { + return ErrNoHealthcheckDefined + } + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + err := e.SendHeartbeat(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + req := <-requests + + // Error message is no longer wrapped (wrapping removed to avoid double-prefixing) + expectedError := "no healthcheck defined" + if req.Msg.GetError() != expectedError { + t.Errorf("expected error %q, got %q", expectedError, req.Msg.GetError()) + } + }) + + t.Run("startSendingHeartbeats", func(t *testing.T) { + e.EngineConfig.HeartbeatOptions.Frequency = time.Millisecond * 250 + e.EngineConfig.HeartbeatOptions.ReadinessCheck = func(_ context.Context) error { + return nil + } + + ctx, cancel := context.WithCancel(context.Background()) + + start := time.Now() + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + e.StartSendingHeartbeats(ctx) + + // Get the initial heartbeat + <-requests + + // Get two + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + <-requests + + cancel() + + // Make sure that took the expected amount of time + if elapsed := time.Since(start); elapsed < time.Millisecond*250 { + t.Errorf("expected to take at least 500ms, took %v", elapsed) + } + + if elapsed := time.Since(start); elapsed > time.Millisecond*500 { + t.Errorf("expected to take at most 750ms, took %v", elapsed) + } + }) +} + +// TestSendHeartbeatNilManagementClient ensures unauthenticated/local dev mode +// (HeartbeatOptions set by SetReadinessCheck but ManagementClient nil) does not error. +func TestSendHeartbeatNilManagementClient(t *testing.T) { + ec := EngineConfig{ + SourceName: t.Name(), + SourceUUID: uuid.New(), + Version: "v0.0.0-test", + EngineType: "aws", + Unauthenticated: true, + HeartbeatOptions: &HeartbeatOptions{ + ManagementClient: nil, // e.g. ALLOW_UNAUTHENTICATED - no API to send to + Frequency: time.Second * 30, + }, + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("NewEngine: %v", err) + } + err = e.SendHeartbeat(context.Background(), nil) + if err != nil { + t.Errorf("SendHeartbeat with nil ManagementClient should be no-op, got: %v", err) + } +} diff --git a/go/discovery/item_tests.go b/go/discovery/item_tests.go new file mode 100644 index 00000000..d959bf8e --- /dev/null +++ b/go/discovery/item_tests.go @@ -0,0 +1,100 @@ +// Reusable testing libraries for testing adapters +package discovery + +import ( + "regexp" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +var RFC1123 = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) + +// TestValidateItem Checks an item to ensure it is a valid SDP item. This includes +// checking that all required attributes are populated +func TestValidateItem(t *testing.T, i *sdp.Item) { + // Ensure that the item has the required fields set i.e. + // + // * Type + // * UniqueAttribute + // * Attributes + if i.GetType() == "" { + t.Errorf("Item %v has an empty Type", i.GloballyUniqueName()) + } + + // Validate that the pattern is RFC1123 + if !RFC1123.MatchString(i.GetType()) { + pattern := ` +Type names should match RFC1123 (lower case). This means the name must: + + * contain at most 63 characters + * contain only lowercase alphanumeric characters or '-' + * start with an alphanumeric character + * end with an alphanumeric character +` + + t.Errorf("Item type %v is invalid. %v", i.GetType(), pattern) + } + + if i.GetUniqueAttribute() == "" { + t.Errorf("Item %v has an empty UniqueAttribute", i.GloballyUniqueName()) + } + + attrMap := i.GetAttributes().GetAttrStruct().AsMap() + + if len(attrMap) == 0 { + t.Errorf("Attributes for item %v are empty", i.GloballyUniqueName()) + } + + // Check the attributes themselves for validity + for k := range attrMap { + if k == "" { + t.Errorf("Item %v has an attribute with an empty name", i.GloballyUniqueName()) + } + } + + // Make sure that the UniqueAttributeValue is populated + if i.UniqueAttributeValue() == "" { + t.Errorf("UniqueAttribute %v for item %v is empty", i.GetUniqueAttribute(), i.GloballyUniqueName()) + } + + // TODO(LIQs): delete this + for index, linkedItem := range i.GetLinkedItems() { + item := linkedItem.GetItem() + if item.GetType() == "" { + t.Errorf("LinkedItem %v of item %v has empty type", index, i.GloballyUniqueName()) + } + + if item.GetUniqueAttributeValue() == "" { + t.Errorf("LinkedItem %v of item %v has empty UniqueAttributeValue", index, i.GloballyUniqueName()) + } + + // We don't need to check for an empty scope here since if it's empty + // it will just inherit the scope of the parent + } + + // TODO(LIQs): delete this + for index, linkedItemQuery := range i.GetLinkedItemQueries() { + query := linkedItemQuery.GetQuery() + if query.GetType() == "" { + t.Errorf("LinkedItemQueries %v of item %v has empty type", index, i.GloballyUniqueName()) + } + + if query.GetMethod() != sdp.QueryMethod_LIST { + if query.GetQuery() == "" { + t.Errorf("LinkedItemQueries %v of item %v has empty query. This is not allowed unless the method is LIST", index, i.GloballyUniqueName()) + } + } + + if query.GetScope() == "" { + t.Errorf("LinkedItemQueries %v of item %v has empty scope", index, i.GloballyUniqueName()) + } + } +} + +// TestValidateItems Runs TestValidateItem on many items +func TestValidateItems(t *testing.T, is []*sdp.Item) { + for _, i := range is { + TestValidateItem(t, i) + } +} diff --git a/go/discovery/logs.go b/go/discovery/logs.go new file mode 100644 index 00000000..3e3ab276 --- /dev/null +++ b/go/discovery/logs.go @@ -0,0 +1,102 @@ +package discovery + +import ( + "context" + "errors" + + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/sdp-go" +) + +// LogAdapter is a singleton from the source that handles GetLogRecordsRequest +// that come in via NATS. The discovery Engine takes care of the common +// implementation details like subscribing to NATS, unpacking the request, +// framing the responses, and error handling. Implementors only need to pass +// results into the LogRecordsStream. +type LogAdapter interface { + // Get gets called when a GetLogRecordsRequest needs to be processed. To + // return data to the requestor, use the provided `stream` to send + // `GetLogRecordsResponse` messages back. + // + // If the implementation encounters an error, it should return the error as + // `SourceError`. To indicate that the error is within the source, set the + // `SourceError.Upstream` field to `false`. To indicate that the error is + // with the upstream API, set the `SourceError.Upstream` field to `true`. + // Always make sure that the error detail is set to a human-readable string + // that is helpful for debugging. + // + // Implementations must not hold on to or share the `stream` object outside + // of the scope of a single call. + // + // Concurrency: Every invocation of this method will happen in its own + // goroutine, so implementors need to take care of ensuring thread safety. + // + // Cancellation: The context passed to this method will be cancelled when + // any errors are encountered, like the NATS connection closing, the + // requestor going away, or hitting a deadline. Implementations are expected + // to timely detect the cancellation and clean up on the way out. After + // `ctx` is cancelled, the implementation should not attempt to send any + // more messages to the stream. + Get(ctx context.Context, req *sdp.GetLogRecordsRequest, stream LogRecordsStream) error + + // Scopes returns all scopes this adapter is capable of handling. This is + // used by the Engine to subscribe to the correct subjects. The Engine will + // only call this method once, so implementors don't need to cache the + // result. + Scopes() []string +} + +type LogRecordsStream interface { + // Send takes a GetLogRecordsResponse, and forwards it to the caller over + // NATS. Note that the order of responses is relevant and will be preserved. + // + // Errors returned from this method should be treated as fatal, and the + // stream should be closed. The caller should not attempt to send any more + // messages after this method returns an error. Basically, treat this like a + // context cancellation on the `LogAdapter.Get` method. + // + // Concurrency: This method is not thread safe. The caller needs to ensure + // that There is only one call of Send active at any time. + Send(ctx context.Context, r *sdp.GetLogRecordsResponse) error +} + +type LogRecordsStreamImpl struct { + // The NATS stream that is used to send messages + stream sdp.EncodedConnection + // The NATS subject that is used to send messages + subject string + // responder has gone away + responderGone bool + + responses int + records int +} + +// assert interface implementation +var _ LogRecordsStream = (*LogRecordsStreamImpl)(nil) + +func (s *LogRecordsStreamImpl) Send(ctx context.Context, r *sdp.GetLogRecordsResponse) error { + // immediately return if the gateway is gone + if s.responderGone { + return nats.ErrNoResponders + } + + s.responses += 1 + s.records += len(r.GetRecords()) + + // Send the message to the NATS stream + err := s.stream.Publish(ctx, s.subject, &sdp.NATSGetLogRecordsResponse{ + Content: &sdp.NATSGetLogRecordsResponse_Response{ + Response: r, + }, + }) + if errors.Is(err, nats.ErrNoResponders) { + s.responderGone = true + return err + } + if err != nil { + return err + } + + return nil +} diff --git a/go/discovery/logs_test.go b/go/discovery/logs_test.go new file mode 100644 index 00000000..6dfc7efe --- /dev/null +++ b/go/discovery/logs_test.go @@ -0,0 +1,398 @@ +package discovery + +import ( + "context" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type testLogAdapter struct { + t *testing.T + expected *sdp.GetLogRecordsRequest + responses []*sdp.GetLogRecordsResponse + err error +} + +// assert interface implementation +var _ LogAdapter = (*testLogAdapter)(nil) + +func (t *testLogAdapter) Get(ctx context.Context, request *sdp.GetLogRecordsRequest, stream LogRecordsStream) error { + if t.expected == nil { + t.t.Fatalf("expected LogAdapter to not get called, but got %v", request) + } + if t.expected.GetScope() != request.GetScope() { + t.t.Errorf("expected scope %s but got %s", t.expected.GetScope(), request.GetScope()) + } + if t.expected.GetQuery() != request.GetQuery() { + t.t.Errorf("expected query %s but got %s", t.expected.GetQuery(), request.GetQuery()) + } + // Compare timestamp values correctly + if (t.expected.GetFrom() == nil) != (request.GetFrom() == nil) { + t.t.Errorf("timestamp nullability mismatch: expected from is nil: %v, got from is nil: %v", t.expected.GetFrom() == nil, request.GetFrom() == nil) + } else if t.expected.GetFrom() != nil && !t.expected.GetFrom().AsTime().Equal(request.GetFrom().AsTime()) { + t.t.Errorf("expected from %s but got %s", t.expected.GetFrom().AsTime(), request.GetFrom().AsTime()) + } + + if (t.expected.GetTo() == nil) != (request.GetTo() == nil) { + t.t.Errorf("timestamp nullability mismatch: expected to is nil: %v, got to is nil: %v", t.expected.GetTo() == nil, request.GetTo() == nil) + } else if t.expected.GetTo() != nil && !t.expected.GetTo().AsTime().Equal(request.GetTo().AsTime()) { + t.t.Errorf("expected to %s but got %s", t.expected.GetTo().AsTime(), request.GetTo().AsTime()) + } + if t.expected.GetMaxRecords() != request.GetMaxRecords() { + t.t.Errorf("expected maxRecords %d but got %d", t.expected.GetMaxRecords(), request.GetMaxRecords()) + } + if t.expected.GetStartFromOldest() != request.GetStartFromOldest() { + t.t.Errorf("expected startFromOldest %v but got %v", t.expected.GetStartFromOldest(), request.GetStartFromOldest()) + } + + for _, r := range t.responses { + err := stream.Send(ctx, r) + if err != nil { + return err + } + } + return t.err +} + +func (t *testLogAdapter) Scopes() []string { + return []string{"test"} +} + +func TestLogAdapter_HappyPath(t *testing.T) { + t.Parallel() + + ts := timestamppb.Now() + tla := &testLogAdapter{ + t: t, + expected: &sdp.GetLogRecordsRequest{ + Scope: "test", + Query: "test", + From: ts, + To: ts, + MaxRecords: 10, + StartFromOldest: false, + }, + responses: []*sdp.GetLogRecordsResponse{ + { + Records: []*sdp.LogRecord{ + { + CreatedAt: timestamppb.Now(), + ObservedAt: timestamppb.Now(), + Severity: sdp.LogSeverity_INFO, + Body: "page1/record1", + }, + { + CreatedAt: timestamppb.Now(), + ObservedAt: timestamppb.Now(), + Severity: sdp.LogSeverity_INFO, + Body: "page1/record2", + }, + }, + }, + { + Records: []*sdp.LogRecord{ + { + CreatedAt: timestamppb.Now(), + ObservedAt: timestamppb.Now(), + Severity: sdp.LogSeverity_INFO, + Body: "page2/record1", + }, + { + CreatedAt: timestamppb.Now(), + ObservedAt: timestamppb.Now(), + Severity: sdp.LogSeverity_INFO, + Body: "page2/record2", + }, + }, + }, + }, + } + + tc := &sdp.TestConnection{ + Messages: make([]sdp.ResponseMessage, 0), + } + + e := newEngine(t, "logs.happyPath", nil, tc) + if e == nil { + t.Fatal("failed to create engine") + } + + err := e.SetLogAdapter(tla) + if err != nil { + t.Fatal(err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = e.Stop() + }() + + _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( + "", + func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { + t.Log("Received message:", msg) + }, + )) + + err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ + Request: &sdp.GetLogRecordsRequest{ + Scope: "test", + Query: "test", + From: ts, + To: ts, + MaxRecords: 10, + StartFromOldest: false, + }, + }) + if err != nil { + t.Log("Subscriptions:", tc.Subscriptions) + t.Fatal(err) + } + + // TODO: properly sync the test to wait for the messages to be sent + time.Sleep(1 * time.Second) + + tc.MessagesMu.Lock() + defer tc.MessagesMu.Unlock() + + if len(tc.Messages) != 5 { + t.Fatalf("expected 5 messages but got %d: %v", len(tc.Messages), tc.Messages) + } + + started := tc.Messages[1] + if started.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_STARTED { + t.Errorf("expected status STARTED but got %v", started.V) + } + + page1 := tc.Messages[2] + records := page1.V.(*sdp.NATSGetLogRecordsResponse).GetResponse().GetRecords() + if len(records) != 2 { + t.Errorf("expected 2 records but got %d: %v", len(records), records) + } + if records[0].GetBody() != "page1/record1" { + t.Errorf("expected page1/record1 but got %v", page1.V) + } + + page2 := tc.Messages[3] + records = page2.V.(*sdp.NATSGetLogRecordsResponse).GetResponse().GetRecords() + if len(records) != 2 { + t.Errorf("expected 2 records but got %d: %v", len(records), records) + } + if records[0].GetBody() != "page2/record1" { + t.Errorf("expected page2/record1 but got %v", page2.V) + } + + finished := tc.Messages[4] + if finished.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_FINISHED { + t.Errorf("expected status FINISHED but got %v", finished.V) + } +} + +func TestLogAdapter_Validation_Scope(t *testing.T) { + t.Parallel() + + ts := timestamppb.Now() + tla := &testLogAdapter{ + t: t, + expected: nil, + } + + tc := &sdp.TestConnection{ + Messages: make([]sdp.ResponseMessage, 0), + } + + e := newEngine(t, "logs.validation_scope", nil, tc) + if e == nil { + t.Fatal("failed to create engine") + } + + err := e.SetLogAdapter(tla) + if err != nil { + t.Fatal(err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = e.Stop() + }() + + _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( + "", + func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { + t.Log("Received message:", msg) + }, + )) + + err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ + Request: &sdp.GetLogRecordsRequest{ + Scope: "different-scope", + Query: "test", + From: ts, + To: ts, + MaxRecords: 10, + StartFromOldest: false, + }, + }) + if err != nil { + t.Log("Subscriptions:", tc.Subscriptions) + t.Fatal(err) + } + + // TODO: properly sync the test to wait for the messages to be sent + time.Sleep(1 * time.Second) + + tc.MessagesMu.Lock() + defer tc.MessagesMu.Unlock() + + if len(tc.Messages) == 0 { + t.Fatalf("expected messages but got none: %v", tc.Messages) + } + + msg := tc.Messages[len(tc.Messages)-1] + if msg.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_ERRORED { + t.Errorf("expected status ERRORED but got %v", msg.V) + } +} + +func TestLogAdapter_Validation_Empty(t *testing.T) { + t.Parallel() + + ts := timestamppb.Now() + tla := &testLogAdapter{ + t: t, + expected: nil, + } + + tc := &sdp.TestConnection{ + Messages: make([]sdp.ResponseMessage, 0), + } + + e := newEngine(t, "logs.validation_scope", nil, tc) + if e == nil { + t.Fatal("failed to create engine") + } + + err := e.SetLogAdapter(tla) + if err != nil { + t.Fatal(err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = e.Stop() + }() + + _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( + "", + func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { + t.Log("Received message:", msg) + }, + )) + + err = tc.PublishRequest(t.Context(), "logs.scope.test", "logs.records.test", &sdp.NATSGetLogRecordsRequest{ + Request: &sdp.GetLogRecordsRequest{ + Scope: "test", + Query: "", + From: ts, + To: ts, + MaxRecords: 10, + StartFromOldest: false, + }, + }) + if err != nil { + t.Log("Subscriptions:", tc.Subscriptions) + t.Fatal(err) + } + + // TODO: properly sync the test to wait for the messages to be sent + time.Sleep(1 * time.Second) + + tc.MessagesMu.Lock() + defer tc.MessagesMu.Unlock() + + if len(tc.Messages) == 0 { + t.Fatalf("expected messages but got none: %v", tc.Messages) + } + + msg := tc.Messages[len(tc.Messages)-1] + if msg.V.(*sdp.NATSGetLogRecordsResponse).GetStatus().GetStatus() != sdp.NATSGetLogRecordsResponseStatus_ERRORED { + t.Errorf("expected status ERRORED but got %v", msg.V) + } +} + +func TestLogAdapter_Validation_NoReplyTo(t *testing.T) { + t.Parallel() + + ts := timestamppb.Now() + tla := &testLogAdapter{ + t: t, + expected: nil, + } + + tc := &sdp.TestConnection{ + Messages: make([]sdp.ResponseMessage, 0), + } + + e := newEngine(t, "logs.validation_scope", nil, tc) + if e == nil { + t.Fatal("failed to create engine") + } + + err := e.SetLogAdapter(tla) + if err != nil { + t.Fatal(err) + } + + err = e.Start(t.Context()) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = e.Stop() + }() + + _, _ = tc.Subscribe("logs.records.test", sdp.NewNATSGetLogRecordsResponseHandler( + "", + func(ctx context.Context, msg *sdp.NATSGetLogRecordsResponse) { + t.Log("Received message:", msg) + }, + )) + + err = tc.Publish(t.Context(), "logs.scope.test", &sdp.NATSGetLogRecordsRequest{ + Request: &sdp.GetLogRecordsRequest{ + Scope: "test", + Query: "test", + From: ts, + To: ts, + MaxRecords: 10, + StartFromOldest: false, + }, + }) + if err != nil { + t.Log("Subscriptions:", tc.Subscriptions) + t.Fatal(err) + } + + // TODO: properly sync the test to wait for the messages to be sent + time.Sleep(1 * time.Second) + + tc.MessagesMu.Lock() + defer tc.MessagesMu.Unlock() + + // only the Request message should be sent, no responses + if len(tc.Messages) != 1 { + t.Fatalf("expected 1 message but got %d: %v", len(tc.Messages), tc.Messages) + } +} diff --git a/go/discovery/main_test.go b/go/discovery/main_test.go new file mode 100644 index 00000000..9b2a75ca --- /dev/null +++ b/go/discovery/main_test.go @@ -0,0 +1,24 @@ +package discovery + +import ( + "context" + "log" + "os" + "testing" + + "github.com/overmindtech/cli/go/tracing" +) + +func TestMain(m *testing.M) { + exitCode := func() int { + defer tracing.ShutdownTracer(context.Background()) + + if err := tracing.InitTracerWithUpstreams("discovery-tests", os.Getenv("HONEYCOMB_API_KEY"), ""); err != nil { + log.Fatal(err) + } + + return m.Run() + }() + + os.Exit(exitCode) +} diff --git a/go/discovery/nats_shared_test.go b/go/discovery/nats_shared_test.go new file mode 100644 index 00000000..ecb8ce05 --- /dev/null +++ b/go/discovery/nats_shared_test.go @@ -0,0 +1,111 @@ +package discovery + +import ( + "context" + "fmt" + "net" + "net/url" + "testing" + "time" +) + +var NatsTestURLs = []string{ + "nats://nats:4222", + "nats://localhost:4222", +} + +var NatsAuthTestURLs = []string{ + "nats://nats-auth:4222", + "nats://localhost:4223", +} + +var tokenExchangeURLs = []string{ + "http://api-server:8080/api", + "http://localhost:8080/api", +} + +// SkipWithoutNats Skips a test if NATS is not available +func SkipWithoutNats(t *testing.T) { + var err error + + for _, url := range NatsTestURLs { + err = testURL(url) + + if err == nil { + return + } + } + + if err != nil { + t.Error(err) + t.Skip("NATS not available") + } +} + +// SkipWithoutNatsAuth Skips a test if authenticated NATS is not available +func SkipWithoutNatsAuth(t *testing.T) { + var err error + + for _, url := range NatsAuthTestURLs { + err = testURL(url) + + if err == nil { + return + } + } + + if err != nil { + t.Error(err) + t.Skip("NATS not available") + } +} + +// SkipWithoutTokenExchange Skips a test if the token exchange API server is not available +func SkipWithoutTokenExchange(t *testing.T) { + var err error + + for _, url := range tokenExchangeURLs { + err = testURL(url) + + if err == nil { + return + } + } + + if err != nil { + t.Error(err) + t.Skip("Token exchange API server not available") + } +} + +func GetWorkingTokenExchange() (string, error) { + var err error + + for _, url := range tokenExchangeURLs { + if err = testURL(url); err == nil { + return url, nil + } + } + + return "", fmt.Errorf("no working token exchanges found: %w", err) +} + +func testURL(testURL string) error { + url, err := url.Parse(testURL) + + if err != nil { + return fmt.Errorf("could not parse NATS URL: %v. Error: %w", testURL, err) + } + + dialer := &net.Dialer{ + Timeout: time.Second, + } + conn, err := dialer.DialContext(context.Background(), "tcp", net.JoinHostPort(url.Hostname(), url.Port())) + + if err == nil { + conn.Close() + return nil + } + + return err +} diff --git a/go/discovery/nats_watcher.go b/go/discovery/nats_watcher.go new file mode 100644 index 00000000..3a68daea --- /dev/null +++ b/go/discovery/nats_watcher.go @@ -0,0 +1,126 @@ +package discovery + +import ( + "context" + "sync" + "time" + + "github.com/nats-io/nats.go" + log "github.com/sirupsen/logrus" +) + +// WatchableConnection Is ususally a *nats.Conn, we are using an interface here +// to allow easier testing +type WatchableConnection interface { + Status() nats.Status + Stats() nats.Statistics + LastError() error +} + +type NATSWatcher struct { + // Connection The NATS connection to watch + Connection WatchableConnection + + // FailureHandler will be called when the connection has been closed and is + // no longer trying to reconnect, or when the connection has been in a + // non-CONNECTED state for longer than ReconnectionTimeout. + FailureHandler func() + + // ReconnectionTimeout is the maximum duration to wait for a reconnection + // before triggering the FailureHandler. If set to 0, no timeout is applied + // and the watcher only triggers on CLOSED status (legacy behavior). + // Recommended value: 5 minutes. + ReconnectionTimeout time.Duration + + watcherContext context.Context + watcherCancel context.CancelFunc + watcherTicker *time.Ticker + watchingMutex sync.Mutex + disconnectedSince time.Time + hasBeenDisconnected bool + failureHandlerTriggered bool +} + +func (w *NATSWatcher) Start(checkInterval time.Duration) { + if w == nil || w.Connection == nil { + return + } + + w.watcherContext, w.watcherCancel = context.WithCancel(context.Background()) + w.watcherTicker = time.NewTicker(checkInterval) + w.watchingMutex.Lock() + + go func(ctx context.Context) { + defer w.watchingMutex.Unlock() + for { + select { + case <-w.watcherTicker.C: + status := w.Connection.Status() + if status != nats.CONNECTED { + // Track when we first became disconnected + if !w.hasBeenDisconnected { + w.disconnectedSince = time.Now() + w.hasBeenDisconnected = true + w.failureHandlerTriggered = false + } + + disconnectedDuration := time.Since(w.disconnectedSince) + + log.WithFields(log.Fields{ + "status": status.String(), + "inBytes": w.Connection.Stats().InBytes, + "outBytes": w.Connection.Stats().OutBytes, + "reconnects": w.Connection.Stats().Reconnects, + "lastError": w.Connection.LastError(), + "disconnectedDuration": disconnectedDuration.String(), + }).Warn("NATS not connected") + + // Trigger failure handler if connection is CLOSED (won't retry) + // or if we've been disconnected for too long. Only trigger once + // per disconnection period to avoid repeated calls while the + // handler is working on reconnection. + if !w.failureHandlerTriggered { + shouldTriggerFailure := false + if status == nats.CLOSED { + log.Warn("NATS connection is CLOSED, triggering failure handler") + shouldTriggerFailure = true + } else if w.ReconnectionTimeout > 0 && disconnectedDuration > w.ReconnectionTimeout { + log.WithFields(log.Fields{ + "disconnectedDuration": disconnectedDuration.String(), + "reconnectionTimeout": w.ReconnectionTimeout.String(), + }).Error("NATS connection has been disconnected for too long, triggering failure handler") + shouldTriggerFailure = true + } + + if shouldTriggerFailure { + // Mark that we've triggered the handler for this disconnection + // period to prevent repeated calls + w.failureHandlerTriggered = true + w.FailureHandler() + } + } + } else { + // Reset the disconnection tracking when we're connected + w.hasBeenDisconnected = false + w.failureHandlerTriggered = false + } + case <-ctx.Done(): + w.watcherTicker.Stop() + + return + } + } + }(w.watcherContext) +} + +func (w *NATSWatcher) Stop() { + if w.watcherCancel != nil { + w.watcherCancel() + + // Once we have sent the signal, wait until it's unlocked so we know + // it's completely stopped + w.watchingMutex.Lock() + defer w.watchingMutex.Unlock() + + } +} diff --git a/go/discovery/nats_watcher_test.go b/go/discovery/nats_watcher_test.go new file mode 100644 index 00000000..ade303ab --- /dev/null +++ b/go/discovery/nats_watcher_test.go @@ -0,0 +1,572 @@ +package discovery + +import ( + "sync" + "testing" + "time" + + "github.com/nats-io/nats.go" +) + +type TestConnection struct { + ReturnStatus nats.Status + ReturnStats nats.Statistics + ReturnError error + Mutex sync.Mutex +} + +func (t *TestConnection) Status() nats.Status { + t.Mutex.Lock() + defer t.Mutex.Unlock() + return t.ReturnStatus +} + +func (t *TestConnection) Stats() nats.Statistics { + t.Mutex.Lock() + defer t.Mutex.Unlock() + return t.ReturnStats +} + +func (t *TestConnection) LastError() error { + t.Mutex.Lock() + defer t.Mutex.Unlock() + return t.ReturnError +} + +func TestNATSWatcher(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTING, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + time.Sleep(interval * 2) + + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTED + c.Mutex.Unlock() + + time.Sleep(interval * 2) + + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + time.Sleep(interval * 2) + + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTED + c.Mutex.Unlock() + + time.Sleep(interval * 2) + + c.Mutex.Lock() + c.ReturnStatus = nats.CLOSED + c.Mutex.Unlock() + + select { + case <-time.After(interval * 2): + t.Errorf("FailureHandler not called in %v", (interval * 2).String()) + case <-fail: + // The fail handler has been called! + t.Log("Fail handler called successfully 🥳") + } +} + +func TestFailureHandler(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTING, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + var w *NATSWatcher + done := make(chan bool, 1024) + + w = &NATSWatcher{ + Connection: &c, + FailureHandler: func() { + go w.Stop() + done <- true + }, + } + + interval := 100 * time.Millisecond + + w.Start(interval) + + time.Sleep(interval * 2) + + c.Mutex.Lock() + c.ReturnStatus = nats.CLOSED + c.Mutex.Unlock() + + time.Sleep(interval * 2) + + select { + case <-time.After(interval * 2): + t.Errorf("FailureHandler not completed in %v", (interval * 2).String()) + case <-done: + if len(done) != 0 { + t.Errorf("Handler was called more than once") + } + // The fail handler has been called! + t.Log("Fail handler called successfully 🥳") + } +} + +func TestReconnectionTimeout(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 100 * time.Millisecond, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Start connected + time.Sleep(interval * 2) + + // Transition to RECONNECTING state + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for the timeout to trigger (100ms + some buffer) + select { + case <-time.After(200 * time.Millisecond): + t.Error("FailureHandler not called after reconnection timeout") + case <-fail: + t.Log("Fail handler called successfully after reconnection timeout 🥳") + } + + w.Stop() +} + +func TestReconnectionTimeoutNotTriggeredWhenConnected(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 50 * time.Millisecond, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Briefly go to RECONNECTING state + time.Sleep(interval * 2) + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // But reconnect before timeout + time.Sleep(20 * time.Millisecond) + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTED + c.Mutex.Unlock() + + // Wait longer than the timeout to ensure it doesn't trigger + select { + case <-time.After(100 * time.Millisecond): + t.Log("Timeout not triggered as expected when connection recovered 🥳") + case <-fail: + t.Error("FailureHandler should not be called when connection recovers before timeout") + } + + w.Stop() +} + +func TestReconnectionTimeoutDisabled(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + // No timeout set (0 means disabled) + ReconnectionTimeout: 0, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Transition to RECONNECTING state + time.Sleep(interval * 2) + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for a while - should not trigger failure handler + select { + case <-time.After(100 * time.Millisecond): + t.Log("Timeout correctly disabled, failure handler not called 🥳") + case <-fail: + t.Error("FailureHandler should not be called when timeout is disabled") + } + + w.Stop() +} + +func TestFailureHandlerNotCalledRepeatedly(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + failCount := 0 + var mu sync.Mutex + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 50 * time.Millisecond, + FailureHandler: func() { + mu.Lock() + failCount++ + mu.Unlock() + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Transition to RECONNECTING state + time.Sleep(interval * 2) + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for timeout to trigger (50ms timeout + buffer) + time.Sleep(80 * time.Millisecond) + + // Give it more time to ensure handler isn't called again + time.Sleep(50 * time.Millisecond) + + w.Stop() + + mu.Lock() + count := failCount + mu.Unlock() + + if count != 1 { + t.Errorf("FailureHandler should be called exactly once, but was called %d times", count) + } else { + t.Log("Failure handler called exactly once as expected 🥳") + } +} + +func TestStartWithNilConnection(t *testing.T) { + w := NATSWatcher{ + Connection: nil, + FailureHandler: func() { + t.Error("FailureHandler should not be called when connection is nil") + }, + } + + // Should not panic and should return early + w.Start(10 * time.Millisecond) + time.Sleep(20 * time.Millisecond) + + // If we get here without panicking, the test passes + t.Log("Start with nil connection handled gracefully 🥳") +} + +func TestStartWithNilWatcher(t *testing.T) { + var w *NATSWatcher + + // Should not panic + w.Start(10 * time.Millisecond) + time.Sleep(20 * time.Millisecond) + + // If we get here without panicking, the test passes + t.Log("Start with nil watcher handled gracefully 🥳") +} + +func TestReconnectionTimeoutWithConnectingState(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 100 * time.Millisecond, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Start connected + time.Sleep(interval * 2) + + // Transition to CONNECTING state (not just RECONNECTING) + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTING + c.Mutex.Unlock() + + // Wait for the timeout to trigger (100ms + some buffer) + select { + case <-time.After(200 * time.Millisecond): + t.Error("FailureHandler not called after reconnection timeout with CONNECTING state") + case <-fail: + t.Log("Fail handler called successfully after reconnection timeout with CONNECTING state 🥳") + } + + w.Stop() +} + +func TestMultipleDisconnectionCycles(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + failCount := 0 + var mu sync.Mutex + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 50 * time.Millisecond, + FailureHandler: func() { + mu.Lock() + failCount++ + mu.Unlock() + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // First disconnection cycle + time.Sleep(interval * 2) + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for timeout to trigger + time.Sleep(80 * time.Millisecond) + + // Reconnect + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTED + c.Mutex.Unlock() + time.Sleep(interval * 2) + + // Second disconnection cycle - should reset and allow handler to be called again + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for timeout to trigger again + time.Sleep(80 * time.Millisecond) + + w.Stop() + + mu.Lock() + count := failCount + mu.Unlock() + + if count != 2 { + t.Errorf("FailureHandler should be called twice (once per disconnection cycle), but was called %d times", count) + } else { + t.Log("Failure handler called correctly for multiple disconnection cycles 🥳") + } +} + +func TestStopBeforeStart(t *testing.T) { + w := NATSWatcher{ + Connection: &TestConnection{ + ReturnStatus: nats.CONNECTED, + }, + } + + // Should not panic if Stop is called before Start + w.Stop() + t.Log("Stop before Start handled gracefully 🥳") +} + +func TestStopMultipleTimes(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + w := NATSWatcher{ + Connection: &c, + FailureHandler: func() {}, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + time.Sleep(interval * 2) + + // Stop multiple times should not panic + w.Stop() + w.Stop() + w.Stop() + + t.Log("Multiple Stop calls handled gracefully 🥳") +} + +func TestHandlerResetAfterReconnection(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + failCount := 0 + var mu sync.Mutex + + w := NATSWatcher{ + Connection: &c, + // Set a short timeout for testing + ReconnectionTimeout: 50 * time.Millisecond, + FailureHandler: func() { + mu.Lock() + failCount++ + mu.Unlock() + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // First disconnection - trigger timeout + time.Sleep(interval * 2) + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for timeout + time.Sleep(80 * time.Millisecond) + + // Reconnect - this should reset the tracking + c.Mutex.Lock() + c.ReturnStatus = nats.CONNECTED + c.Mutex.Unlock() + time.Sleep(interval * 2) + + // Disconnect again - should be able to trigger handler again + c.Mutex.Lock() + c.ReturnStatus = nats.RECONNECTING + c.Mutex.Unlock() + + // Wait for timeout again + time.Sleep(80 * time.Millisecond) + + w.Stop() + + mu.Lock() + count := failCount + mu.Unlock() + + if count != 2 { + t.Errorf("FailureHandler should be called twice after reconnection reset, but was called %d times", count) + } else { + t.Log("Handler reset correctly after reconnection 🥳") + } +} + +func TestCLOSEDStatusTriggersImmediately(t *testing.T) { + c := TestConnection{ + ReturnStatus: nats.CONNECTED, + ReturnStats: nats.Statistics{}, + ReturnError: nil, + } + + fail := make(chan bool) + + w := NATSWatcher{ + Connection: &c, + // Even with timeout set, CLOSED should trigger immediately + ReconnectionTimeout: 100 * time.Millisecond, + FailureHandler: func() { + fail <- true + }, + } + + interval := 10 * time.Millisecond + + w.Start(interval) + + // Start connected + time.Sleep(interval * 2) + + // Transition directly to CLOSED (should trigger immediately, not wait for timeout) + c.Mutex.Lock() + c.ReturnStatus = nats.CLOSED + c.Mutex.Unlock() + + // Should trigger much faster than the timeout + select { + case <-time.After(50 * time.Millisecond): + t.Error("FailureHandler not called immediately for CLOSED status") + case <-fail: + t.Log("Fail handler called immediately for CLOSED status 🥳") + } + + w.Stop() +} diff --git a/go/discovery/nil_publisher.go b/go/discovery/nil_publisher.go new file mode 100644 index 00000000..5e64d34b --- /dev/null +++ b/go/discovery/nil_publisher.go @@ -0,0 +1,111 @@ +package discovery + +import ( + "context" + "fmt" + + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// When testing this library, or running without a real NATS connection, it is +// necessary to create a fake publisher rather than pass in a nil pointer. This +// is due to the fact that the NATS libraries will panic if a method is called +// on a nil pointer +type NilConnection struct{} + +// assert interface implementation +var _ sdp.EncodedConnection = (*NilConnection)(nil) + +// Publish Does nothing except log an error +func (n NilConnection) Publish(ctx context.Context, subj string, m proto.Message) error { + log.WithFields(log.Fields{ + "subject": subj, + "message": fmt.Sprint(m), + }).Error("Could not publish NATS message due to no connection") + + return nil +} + +// PublishRequest Does nothing except log an error +func (n NilConnection) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { + log.WithFields(log.Fields{ + "subject": subj, + "replyTo": replyTo, + "message": fmt.Sprint(m), + }).Error("Could not publish NATS message request due to no connection") + + return nil +} + +// PublishMsg Does nothing except log an error +func (n NilConnection) PublishMsg(ctx context.Context, msg *nats.Msg) error { + log.WithFields(log.Fields{ + "subject": msg.Subject, + "message": fmt.Sprint(msg), + }).Error("Could not publish NATS message due to no connection") + + return nil +} + +// Subscribe Does nothing except log an error +func (n NilConnection) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { + log.WithFields(log.Fields{ + "subject": subj, + }).Error("Could not subscribe to NAT subject due to no connection") + + return nil, nil +} + +// QueueSubscribe Does nothing except log an error +func (n NilConnection) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { + log.WithFields(log.Fields{ + "subject": subj, + "queue": queue, + }).Error("Could not subscribe to NAT subject queue due to no connection") + + return nil, nil +} + +// Request Does nothing except log an error +func (n NilConnection) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { + log.WithFields(log.Fields{ + "subject": msg.Subject, + "message": fmt.Sprint(msg), + }).Error("Could not publish NATS request due to no connection") + + return nil, nil +} + +// Status Always returns nats.CONNECTED +func (n NilConnection) Status() nats.Status { + return nats.CONNECTED +} + +// Stats Always returns empty/zero nats.Statistics +func (n NilConnection) Stats() nats.Statistics { + return nats.Statistics{} +} + +// LastError Always returns nil +func (n NilConnection) LastError() error { + return nil +} + +// Drain Always returns nil +func (n NilConnection) Drain() error { + return nil +} + +// Close Does nothing +func (n NilConnection) Close() {} + +// Underlying Always returns nil +func (n NilConnection) Underlying() *nats.Conn { + return nil +} + +// Drop Does nothing +func (n NilConnection) Drop() {} diff --git a/go/discovery/performance_test.go b/go/discovery/performance_test.go new file mode 100644 index 00000000..fd521c71 --- /dev/null +++ b/go/discovery/performance_test.go @@ -0,0 +1,216 @@ +package discovery + +import ( + "context" + "math" + "os" + "sync" + "testing" + "time" + + "github.com/overmindtech/cli/go/auth" + "github.com/overmindtech/cli/go/sdp-go" +) + +type SlowAdapter struct { + QueryDuration time.Duration +} + +func (s *SlowAdapter) Type() string { + return "person" +} + +func (s *SlowAdapter) Name() string { + return "slow-adapter" +} + +func (s *SlowAdapter) DefaultCacheDuration() time.Duration { + return 10 * time.Minute +} + +func (s *SlowAdapter) Metadata() *sdp.AdapterMetadata { + return &sdp.AdapterMetadata{} +} + +func (s *SlowAdapter) Scopes() []string { + return []string{"test"} +} + +func (s *SlowAdapter) Hidden() bool { + return false +} + +func (s *SlowAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + end := time.Now().Add(s.QueryDuration) + attributes, _ := sdp.ToAttributes(map[string]interface{}{ + "name": query, + }) + + item := sdp.Item{ + Type: "person", + UniqueAttribute: "name", + Attributes: attributes, + Scope: "test", + // TODO(LIQs): delete this + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // TODO(LIQs): convert to returning edges + for i := 0; i != 2; i++ { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: RandomName(), + Scope: "test", + }}) + } + + time.Sleep(time.Until(end)) + + return &item, nil +} + +func (s *SlowAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + return []*sdp.Item{}, nil +} + +func (s *SlowAdapter) Weight() int { + return 100 +} + +func TestParallelQueryPerformance(t *testing.T) { + if os.Getenv("GITHUB_ACTIONS") != "" { + t.Skip("Performance tests under github actions are too unreliable") + } + + // This test is designed to ensure that query duration is linear up to a + // certain point. Above that point the overhead caused by having so many + // goroutines running will start to make the response times non-linear which + // maybe isn't ideal but given realistic loads we probably don't care. + t.Run("Without linking", func(t *testing.T) { + RunLinearPerformanceTest(t, "1 query", 1, 0, 1) + RunLinearPerformanceTest(t, "10 queries", 10, 0, 1) + RunLinearPerformanceTest(t, "100 queries", 100, 0, 10) + RunLinearPerformanceTest(t, "1,000 queries", 1000, 0, 100) + }) +} + +// RunLinearPerformanceTest Runs a test with a given number in input queries, +// link depth and parallelization limit. Expected results and expected duration +// are determined automatically meaning all this is testing for is the fact that +// the performance continues to be linear and predictable +func RunLinearPerformanceTest(t *testing.T, name string, numQueries int, linkDepth int, numParallel int) { + t.Helper() + + t.Run(name, func(t *testing.T) { + result := TimeQueries(t, numQueries, linkDepth, numParallel) + + if len(result.Results) != result.ExpectedItems { + t.Errorf("Expected %v items, got %v (%v errors)", result.ExpectedItems, len(result.Results), len(result.Errors)) + } + + if result.TimeTaken > result.MaxTime { + t.Errorf("Queries took too long: %v Max: %v", result.TimeTaken.String(), result.MaxTime.String()) + } + }) +} + +type TimedResults struct { + ExpectedItems int + MaxTime time.Duration + TimeTaken time.Duration + Results []*sdp.Item + Errors []*sdp.QueryError +} + +func TimeQueries(t *testing.T, numQueries int, linkDepth int, numParallel int) TimedResults { + ec := EngineConfig{ + MaxParallelExecutions: numParallel, + Unauthenticated: true, + NATSOptions: &auth.NATSOptions{ + NumRetries: 5, + RetryDelay: time.Second, + Servers: NatsTestURLs, + ConnectionName: "test-connection", + ConnectionTimeout: time.Second, + MaxReconnects: 5, + }, + } + e, err := NewEngine(&ec) + if err != nil { + t.Fatalf("Error initializing Engine: %v", err) + } + err = e.AddAdapters(&SlowAdapter{ + QueryDuration: 100 * time.Millisecond, + }) + if err != nil { + t.Fatalf("Error adding adapter: %v", err) + } + err = e.Start(t.Context()) + if err != nil { + t.Fatalf("Error starting Engine: %v", err) + } + defer func() { + err = e.Stop() + if err != nil { + t.Fatalf("Error stopping Engine: %v", err) + } + }() + + // Calculate how many items to expect and the expected duration + var expectedItems int + var expectedDuration time.Duration + for i := 0; i <= linkDepth; i++ { + thisLayer := int(math.Pow(2, float64(i))) * numQueries + + // Expect that it'll take no longer that 120% of the sleep time. + thisDuration := 120 * math.Ceil(float64(thisLayer)/float64(numParallel)) + expectedDuration = expectedDuration + (time.Duration(thisDuration) * time.Millisecond) + expectedItems = expectedItems + thisLayer + } + + results := make([]*sdp.Item, 0) + errors := make([]*sdp.QueryError, 0) + resultsMutex := sync.Mutex{} + wg := sync.WaitGroup{} + + start := time.Now() + + for range numQueries { + qt := QueryTracker{ + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: RandomName(), + Scope: "test", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: uint32(linkDepth), + }, + }, + Engine: e, + } + + wg.Add(1) + + go func(qt *QueryTracker) { + defer wg.Done() + + items, _, errs, _ := qt.Execute(context.Background()) + + resultsMutex.Lock() + results = append(results, items...) + errors = append(errors, errs...) + resultsMutex.Unlock() + }(&qt) + } + + wg.Wait() + + return TimedResults{ + ExpectedItems: expectedItems, + MaxTime: expectedDuration, + TimeTaken: time.Since(start), + Results: results, + Errors: errors, + } +} diff --git a/go/discovery/querytracker.go b/go/discovery/querytracker.go new file mode 100644 index 00000000..4cb30c97 --- /dev/null +++ b/go/discovery/querytracker.go @@ -0,0 +1,92 @@ +package discovery + +import ( + "context" + "errors" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// QueryTracker is used for tracking the progress of a single query. This +// is used because a single query could have a link depth that results in many +// additional queries being executed meaning that we need to not only track the first +// query, but also all other queries and items that result from linking +type QueryTracker struct { + // The query to track + Query *sdp.Query + + Context context.Context // The context that this query is running in + Cancel context.CancelFunc // The cancel function for the context + + // The engine that this is connected to, used for sending NATS messages + Engine *Engine +} + +// Execute Executes a given item query and publishes results and errors on the +// relevant nats subjects. Returns the full list of items, errors, and a final +// error. The final error will be populated if all adapters failed, or some other +// error was encountered while trying run the query +// +// If the context is cancelled, all query work will stop +func (qt *QueryTracker) Execute(ctx context.Context) ([]*sdp.Item, []*sdp.Edge, []*sdp.QueryError, error) { + if qt.Query == nil { + return nil, nil, nil, nil + } + + if qt.Engine == nil { + return nil, nil, nil, errors.New("no engine supplied, cannot execute") + } + + span := trace.SpanFromContext(ctx) + span.SetAttributes( + attribute.String("ovm.sdp.source_name", qt.Engine.EngineConfig.SourceName), + attribute.String("ovm.engine.type", qt.Engine.EngineConfig.EngineType), + attribute.String("ovm.engine.version", qt.Engine.EngineConfig.Version), + ) + + responses := make(chan *sdp.QueryResponse) + errChan := make(chan error, 1) + + sdpItems := make([]*sdp.Item, 0) + sdpEdges := make([]*sdp.Edge, 0) + sdpErrs := make([]*sdp.QueryError, 0) + + // Run the query in the background + go func(e chan error) { + defer tracing.LogRecoverToReturn(ctx, "Execute -> ExecuteQuery") + defer close(e) + e <- qt.Engine.ExecuteQuery(ctx, qt.Query, responses) + }(errChan) + + // Process the responses as they come in + for response := range responses { + if qt.Query.Subject() != "" && qt.Engine.natsConnection != nil { + err := qt.Engine.natsConnection.Publish(ctx, qt.Query.Subject(), response) + if err != nil { + span.RecordError(err) + log.WithError(err).Error("Response publishing error") + } + } + + switch response := response.GetResponseType().(type) { + case *sdp.QueryResponse_NewItem: + sdpItems = append(sdpItems, response.NewItem) + case *sdp.QueryResponse_Edge: + sdpEdges = append(sdpEdges, response.Edge) + case *sdp.QueryResponse_Error: + sdpErrs = append(sdpErrs, response.Error) + } + } + + // Get the result of the execution + err := <-errChan + if err != nil { + return sdpItems, sdpEdges, sdpErrs, err + } + + return sdpItems, sdpEdges, sdpErrs, ctx.Err() +} diff --git a/go/discovery/querytracker_test.go b/go/discovery/querytracker_test.go new file mode 100644 index 00000000..338e29f8 --- /dev/null +++ b/go/discovery/querytracker_test.go @@ -0,0 +1,299 @@ +package discovery + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "google.golang.org/protobuf/types/known/structpb" +) + +type SpeedTestAdapter struct { + QueryDelay time.Duration + ReturnType string + ReturnScopes []string +} + +func (s *SpeedTestAdapter) Type() string { + if s.ReturnType != "" { + return s.ReturnType + } + + return "person" +} + +func (s *SpeedTestAdapter) Name() string { + return "SpeedTestAdapter" +} + +func (s *SpeedTestAdapter) Scopes() []string { + if len(s.ReturnScopes) > 0 { + return s.ReturnScopes + } + + return []string{"test"} +} + +func (s *SpeedTestAdapter) Metadata() *sdp.AdapterMetadata { + return &sdp.AdapterMetadata{} +} + +func (s *SpeedTestAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + select { + case <-time.After(s.QueryDelay): + return &sdp.Item{ + Type: s.Type(), + UniqueAttribute: "name", + Attributes: &sdp.ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "name": { + Kind: &structpb.Value_StringValue{ + StringValue: query, + }, + }, + }, + }, + }, + // TODO(LIQs): convert to returning edges + LinkedItemQueries: []*sdp.LinkedItemQuery{ + { + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: query + time.Now().String(), + Scope: scope, + }, + }, + }, + Scope: scope, + }, nil + case <-ctx.Done(): + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_TIMEOUT, + ErrorString: ctx.Err().Error(), + Scope: scope, + } + } +} + +func (s *SpeedTestAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + item, err := s.Get(ctx, scope, "dylan", ignoreCache) + + return []*sdp.Item{item}, err +} + +func (s *SpeedTestAdapter) Weight() int { + return 10 +} + +func TestExecute(t *testing.T) { + adapter := TestAdapter{ + ReturnType: "person", + ReturnScopes: []string{ + "test", + }, + cache: sdpcache.NewNoOpCache(), + } + + e := newStartedEngine(t, "TestExecute", nil, nil, &adapter) + + t.Run("Without linking", func(t *testing.T) { + t.Parallel() + + qt := QueryTracker{ + Engine: e, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + }, + } + + items, edges, errs, err := qt.Execute(context.Background()) + if err != nil { + t.Error(err) + } + + for _, e := range errs { + t.Error(e) + } + + if l := len(items); l != 1 { + t.Errorf("expected 1 items, got %v: %v", l, items) + } + + if l := len(edges); l != 0 { + t.Errorf("expected 0 items, got %v: %v", l, edges) + } + }) + + t.Run("With no engine", func(t *testing.T) { + t.Parallel() + + qt := QueryTracker{ + Engine: nil, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 10, + }, + Scope: "test", + }, + } + + _, _, _, err := qt.Execute(context.Background()) + + if err == nil { + t.Error("expected error but got nil") + } + }) + + t.Run("With no queries", func(t *testing.T) { + t.Parallel() + + qt := QueryTracker{ + Engine: e, + } + + _, _, _, err := qt.Execute(context.Background()) + if err != nil { + t.Error(err) + } + }) +} + +func TestTimeout(t *testing.T) { + adapter := SpeedTestAdapter{ + QueryDelay: 100 * time.Millisecond, + } + e := newStartedEngine(t, "TestTimeout", nil, nil, &adapter) + + t.Run("With a timeout, but not exceeding it", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + + qt := QueryTracker{ + Engine: e, + Context: ctx, + Cancel: cancel, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + }, + } + + items, edges, errs, err := qt.Execute(context.Background()) + if err != nil { + t.Error(err) + } + + for _, e := range errs { + t.Error(e) + } + + if l := len(items); l != 1 { + t.Errorf("expected 1 items, got %v: %v", l, items) + } + + if l := len(edges); l != 0 { + t.Errorf("expected 0 edges, got %v: %v", l, edges) + } + }) + + t.Run("With a timeout that is exceeded", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + + qt := QueryTracker{ + Engine: e, + Context: ctx, + Cancel: cancel, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "somethingElse", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + }, + } + + _, _, _, err := qt.Execute(ctx) + + if err == nil { + t.Error("Expected timeout but got no error") + } + }) +} + +func TestCancel(t *testing.T) { + e := newStartedEngine(t, "TestCancel", nil, nil) + + u := uuid.New() + ctx, cancel := context.WithCancel(context.Background()) + + qt := QueryTracker{ + Engine: e, + Context: ctx, + Cancel: cancel, + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: "somethingElse1", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 10, + }, + Scope: "test", + UUID: u[:], + }, + } + + items := make([]*sdp.Item, 0) + edges := make([]*sdp.Edge, 0) + var wg sync.WaitGroup + + var err error + wg.Add(1) + go func() { + items, edges, _, err = qt.Execute(context.Background()) + wg.Done() + }() + + // Give it some time to populate the cancelFunc + time.Sleep(100 * time.Millisecond) + + qt.Cancel() + + wg.Wait() + + if err == nil { + t.Error("expected error but got none") + } + + if len(items) != 0 { + t.Errorf("Expected no items but got %v", items) + } + + if len(edges) != 0 { + t.Errorf("Expected no edges but got %v", edges) + } +} diff --git a/go/discovery/shared_test.go b/go/discovery/shared_test.go new file mode 100644 index 00000000..b4e42d99 --- /dev/null +++ b/go/discovery/shared_test.go @@ -0,0 +1,275 @@ +package discovery + +import ( + "context" + "fmt" + "math/rand" + "sync" + "sync/atomic" + "time" + + "github.com/goombaio/namegenerator" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "google.golang.org/protobuf/types/known/structpb" +) + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func randString(length int) string { + var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func RandomName() string { + seed := time.Now().UTC().UnixNano() + nameGenerator := namegenerator.NewNameGenerator(seed) + name := nameGenerator.Generate() + randGarbage := randString(10) + return fmt.Sprintf("%v-%v", name, randGarbage) +} + +var generation atomic.Int32 + +func (s *TestAdapter) NewTestItem(scope string, query string) *sdp.Item { + gen := generation.Add(1) + return &sdp.Item{ + Type: s.Type(), + Scope: scope, + UniqueAttribute: "name", + Attributes: &sdp.ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "name": structpb.NewStringValue(query), + "age": structpb.NewNumberValue(28), + "generation": structpb.NewNumberValue(float64(gen)), + }, + }, + }, + // TODO(LIQs): convert to returning edges + LinkedItemQueries: []*sdp.LinkedItemQuery{ + { + Query: &sdp.Query{ + Type: "person", + Method: sdp.QueryMethod_GET, + Query: RandomName(), + Scope: scope, + }, + }, + }, + } +} + +type TestAdapter struct { + ReturnScopes []string + ReturnType string + GetCalls [][]string + ListCalls [][]string + SearchCalls [][]string + IsHidden bool + ReturnWeight int // Weight to be returned + ReturnName string // The name of the Adapter + mutex sync.Mutex + + CacheDuration time.Duration // How long to cache items for + cache sdpcache.Cache // This is mandatory +} + +// NewTestAdapter creates a new TestAdapter with cache initialized +func NewTestAdapter() *TestAdapter { + return &TestAdapter{ + cache: sdpcache.NewNoOpCache(), // Initialize with NoOpCache to avoid nil pointer dereferences + } +} + +// ClearCalls Clears the call counters between tests +func (s *TestAdapter) ClearCalls() { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.ListCalls = make([][]string, 0) + s.SearchCalls = make([][]string, 0) + s.GetCalls = make([][]string, 0) + if s.cache != nil { + s.cache.Clear() + } +} + +func (s *TestAdapter) Type() string { + if s.ReturnType != "" { + return s.ReturnType + } + + return "person" +} + +func (s *TestAdapter) Name() string { + return fmt.Sprintf("testAdapter-%v", s.ReturnName) +} + +func (s *TestAdapter) DefaultCacheDuration() time.Duration { + return 100 * time.Millisecond +} + +func (s *TestAdapter) Metadata() *sdp.AdapterMetadata { + return &sdp.AdapterMetadata{ + Type: s.Type(), + DescriptiveName: "Person", + } +} + +func (s *TestAdapter) Scopes() []string { + if len(s.ReturnScopes) > 0 { + return s.ReturnScopes + } + + return []string{"test"} +} + +func (s *TestAdapter) Hidden() bool { + return s.IsHidden +} + +func (s *TestAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + var cacheHit bool + var ck sdpcache.CacheKey + var cachedItems []*sdp.Item + var qErr *sdp.QueryError + var done func() + + cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_GET, scope, s.Type(), query, ignoreCache) + defer done() + if qErr != nil { + return nil, qErr + } + if cacheHit { + if len(cachedItems) > 0 { + return cachedItems[0], nil + } else { + return nil, nil + } + } + + s.GetCalls = append(s.GetCalls, []string{scope, query}) + + switch scope { + case "empty": + err := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no items found", + Scope: scope, + } + s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) + return nil, err + case "error": + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Error for testing", + Scope: scope, + } + default: + item := s.NewTestItem(scope, query) + s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) + return item, nil + } +} + +func (s *TestAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + var cacheHit bool + var ck sdpcache.CacheKey + var cachedItems []*sdp.Item + var qErr *sdp.QueryError + var done func() + + cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_LIST, scope, s.Type(), "", ignoreCache) + defer done() + if qErr != nil { + return nil, qErr + } + if cacheHit { + return cachedItems, nil + } + + s.ListCalls = append(s.ListCalls, []string{scope}) + + switch scope { + case "empty": + err := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no items found", + Scope: scope, + } + s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) + return nil, err + case "error": + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Error for testing", + Scope: scope, + } + default: + item := s.NewTestItem(scope, "Dylan") + items := []*sdp.Item{item} + s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) + return items, nil + } +} + +func (s *TestAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + var cacheHit bool + var ck sdpcache.CacheKey + var cachedItems []*sdp.Item + var qErr *sdp.QueryError + var done func() + + cacheHit, ck, cachedItems, qErr, done = s.cache.Lookup(ctx, s.Name(), sdp.QueryMethod_SEARCH, scope, s.Type(), query, ignoreCache) + defer done() + if qErr != nil { + return nil, qErr + } + if cacheHit { + return cachedItems, nil + } + + s.SearchCalls = append(s.SearchCalls, []string{scope, query}) + + switch scope { + case "empty": + err := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "no items found", + Scope: scope, + } + s.cache.StoreError(ctx, err, s.DefaultCacheDuration(), ck) + return nil, err + case "error": + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Error for testing", + Scope: scope, + } + default: + item := s.NewTestItem(scope, "Dylan") + items := []*sdp.Item{item} + s.cache.StoreItem(ctx, item, s.DefaultCacheDuration(), ck) + return items, nil + } +} + +func (s *TestAdapter) Weight() int { + return s.ReturnWeight +} diff --git a/go/discovery/tracing.go b/go/discovery/tracing.go new file mode 100644 index 00000000..9a5783de --- /dev/null +++ b/go/discovery/tracing.go @@ -0,0 +1,20 @@ +package discovery + +import ( + "go.opentelemetry.io/otel" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" +) + +const ( + instrumentationName = "github.com/overmindtech/cli/go/discovery/discovery" + instrumentationVersion = "0.0.1" +) + +var ( + tracer = otel.GetTracerProvider().Tracer( + instrumentationName, + trace.WithInstrumentationVersion(instrumentationVersion), + trace.WithSchemaURL(semconv.SchemaURL), + ) +) diff --git a/go/logging/logging.go b/go/logging/logging.go new file mode 100644 index 00000000..f6da0438 --- /dev/null +++ b/go/logging/logging.go @@ -0,0 +1,53 @@ +package logging + +import ( + log "github.com/sirupsen/logrus" +) + +// ConfigureLogrusJSON sets the logger to emit JSON logs with a GCP severity field. +func ConfigureLogrusJSON(logger *log.Logger) { + if logger == nil { + return + } + + logger.SetFormatter(&log.JSONFormatter{}) + logger.AddHook(OtelSeverityHook{}) +} + +// OtelSeverityHook adds a GCP-compatible severity field to log entries. +type OtelSeverityHook struct{} + +func (OtelSeverityHook) Levels() []log.Level { + return log.AllLevels +} + +func (OtelSeverityHook) Fire(entry *log.Entry) error { + if entry == nil { + return nil + } + if _, ok := entry.Data["severity"]; ok { + return nil + } + + entry.Data["severity"] = severityForLevel(entry.Level) + return nil +} + +func severityForLevel(level log.Level) string { + switch level { + case log.PanicLevel: + return "emergency" + case log.FatalLevel: + return "critical" + case log.ErrorLevel: + return "error" + case log.WarnLevel: + return "warning" + case log.InfoLevel: + return "info" + case log.DebugLevel, log.TraceLevel: + return "debug" + default: + return "default" + } +} diff --git a/go/logging/logging_test.go b/go/logging/logging_test.go new file mode 100644 index 00000000..a53a52bc --- /dev/null +++ b/go/logging/logging_test.go @@ -0,0 +1,86 @@ +package logging + +import ( + "bytes" + "encoding/json" + "testing" + + log "github.com/sirupsen/logrus" +) + +func TestSeverityForLevel(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + level log.Level + want string + }{ + {name: "panic", level: log.PanicLevel, want: "emergency"}, + {name: "fatal", level: log.FatalLevel, want: "critical"}, + {name: "error", level: log.ErrorLevel, want: "error"}, + {name: "warn", level: log.WarnLevel, want: "warning"}, + {name: "info", level: log.InfoLevel, want: "info"}, + {name: "debug", level: log.DebugLevel, want: "debug"}, + {name: "trace", level: log.TraceLevel, want: "debug"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := severityForLevel(tt.level) + if got != tt.want { + t.Errorf("severityForLevel(%v) = %q, want %q", tt.level, got, tt.want) + } + }) + } +} + +func TestConfigureLogrusJSONAddsSeverity(t *testing.T) { + t.Parallel() + + logger := log.New() + var buf bytes.Buffer + logger.SetOutput(&buf) + + ConfigureLogrusJSON(logger) + logger.WithField("component", "test").Info("hello") + + var payload map[string]any + if err := json.Unmarshal(buf.Bytes(), &payload); err != nil { + t.Fatalf("unmarshal log payload: %v", err) + } + + got, ok := payload["severity"] + if !ok { + t.Fatalf("expected severity field in log payload, got: %#v", payload) + } + if got != "info" { + t.Fatalf("expected severity %q, got %v", "info", got) + } +} + +func TestConfigureLogrusJSONRespectsExistingSeverity(t *testing.T) { + t.Parallel() + + logger := log.New() + var buf bytes.Buffer + logger.SetOutput(&buf) + + ConfigureLogrusJSON(logger) + logger.WithField("severity", "SPECIAL").Info("hello") + + var payload map[string]any + if err := json.Unmarshal(buf.Bytes(), &payload); err != nil { + t.Fatalf("unmarshal log payload: %v", err) + } + + got, ok := payload["severity"] + if !ok { + t.Fatalf("expected severity field in log payload, got: %#v", payload) + } + if got != "SPECIAL" { + t.Fatalf("expected severity %q, got %v", "SPECIAL", got) + } +} diff --git a/go/sdp-go/.gitignore b/go/sdp-go/.gitignore new file mode 100644 index 00000000..85d346aa --- /dev/null +++ b/go/sdp-go/.gitignore @@ -0,0 +1,2 @@ +vendor +.DS_Store diff --git a/go/sdp-go/account.go b/go/sdp-go/account.go new file mode 100644 index 00000000..a5cda679 --- /dev/null +++ b/go/sdp-go/account.go @@ -0,0 +1,19 @@ +package sdp + +import "github.com/google/uuid" + +func (a *SourceMetadata) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} + +func (a *SourceHealth) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} diff --git a/go/sdp-go/account.pb.go b/go/sdp-go/account.pb.go new file mode 100644 index 00000000..ea051d50 --- /dev/null +++ b/go/sdp-go/account.pb.go @@ -0,0 +1,4649 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: account.proto + +package sdp + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SourceStatus int32 + +const ( + SourceStatus_STATUS_UNSPECIFIED SourceStatus = 0 + // The source is starting or updating. This is only applicable to managed + // sources where Overmind manages the source's lifecycle + SourceStatus_STATUS_PROGRESSING SourceStatus = 1 + // The source is healthy + SourceStatus_STATUS_HEALTHY SourceStatus = 2 + // The source is unhealthy + SourceStatus_STATUS_UNHEALTHY SourceStatus = 3 + // The source is sleeping due to inactivity. It will be woken up before it + // is needed. This is only applicable to managed sources where Overmind + // manages the source's lifecycle + SourceStatus_STATUS_SLEEPING SourceStatus = 4 + // The source is disconnected and therefore not able to handle requests. + // This will only be returned for non-managed sources that have recently + // stopped sending heartbeats such as a user running the CLI that has + // recently disconnected + SourceStatus_STATUS_DISCONNECTED SourceStatus = 5 +) + +// Enum value maps for SourceStatus. +var ( + SourceStatus_name = map[int32]string{ + 0: "STATUS_UNSPECIFIED", + 1: "STATUS_PROGRESSING", + 2: "STATUS_HEALTHY", + 3: "STATUS_UNHEALTHY", + 4: "STATUS_SLEEPING", + 5: "STATUS_DISCONNECTED", + } + SourceStatus_value = map[string]int32{ + "STATUS_UNSPECIFIED": 0, + "STATUS_PROGRESSING": 1, + "STATUS_HEALTHY": 2, + "STATUS_UNHEALTHY": 3, + "STATUS_SLEEPING": 4, + "STATUS_DISCONNECTED": 5, + } +) + +func (x SourceStatus) Enum() *SourceStatus { + p := new(SourceStatus) + *p = x + return p +} + +func (x SourceStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SourceStatus) Descriptor() protoreflect.EnumDescriptor { + return file_account_proto_enumTypes[0].Descriptor() +} + +func (SourceStatus) Type() protoreflect.EnumType { + return &file_account_proto_enumTypes[0] +} + +func (x SourceStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SourceStatus.Descriptor instead. +func (SourceStatus) EnumDescriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{0} +} + +type RepositoryStatus int32 + +const ( + RepositoryStatus_REPOSITORY_STATUS_UNSPECIFIED RepositoryStatus = 0 + // Repository has had changes within the defined activity window + RepositoryStatus_REPOSITORY_STATUS_ACTIVE RepositoryStatus = 1 + // Repository has not had changes within the defined activity window + RepositoryStatus_REPOSITORY_STATUS_INACTIVE RepositoryStatus = 2 +) + +// Enum value maps for RepositoryStatus. +var ( + RepositoryStatus_name = map[int32]string{ + 0: "REPOSITORY_STATUS_UNSPECIFIED", + 1: "REPOSITORY_STATUS_ACTIVE", + 2: "REPOSITORY_STATUS_INACTIVE", + } + RepositoryStatus_value = map[string]int32{ + "REPOSITORY_STATUS_UNSPECIFIED": 0, + "REPOSITORY_STATUS_ACTIVE": 1, + "REPOSITORY_STATUS_INACTIVE": 2, + } +) + +func (x RepositoryStatus) Enum() *RepositoryStatus { + p := new(RepositoryStatus) + *p = x + return p +} + +func (x RepositoryStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RepositoryStatus) Descriptor() protoreflect.EnumDescriptor { + return file_account_proto_enumTypes[1].Descriptor() +} + +func (RepositoryStatus) Type() protoreflect.EnumType { + return &file_account_proto_enumTypes[1] +} + +func (x RepositoryStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RepositoryStatus.Descriptor instead. +func (RepositoryStatus) EnumDescriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{1} +} + +type AccountPlan int32 + +const ( + AccountPlan_ACCOUNT_PLAN_UNSPECIFIED AccountPlan = 0 + // Free plan with one repo + AccountPlan_ACCOUNT_PLAN_FREE AccountPlan = 1 + // Enterprise plan with unlimited repos + AccountPlan_ACCOUNT_PLAN_ENTERPRISE AccountPlan = 2 +) + +// Enum value maps for AccountPlan. +var ( + AccountPlan_name = map[int32]string{ + 0: "ACCOUNT_PLAN_UNSPECIFIED", + 1: "ACCOUNT_PLAN_FREE", + 2: "ACCOUNT_PLAN_ENTERPRISE", + } + AccountPlan_value = map[string]int32{ + "ACCOUNT_PLAN_UNSPECIFIED": 0, + "ACCOUNT_PLAN_FREE": 1, + "ACCOUNT_PLAN_ENTERPRISE": 2, + } +) + +func (x AccountPlan) Enum() *AccountPlan { + p := new(AccountPlan) + *p = x + return p +} + +func (x AccountPlan) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccountPlan) Descriptor() protoreflect.EnumDescriptor { + return file_account_proto_enumTypes[2].Descriptor() +} + +func (AccountPlan) Type() protoreflect.EnumType { + return &file_account_proto_enumTypes[2] +} + +func (x AccountPlan) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccountPlan.Descriptor instead. +func (AccountPlan) EnumDescriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{2} +} + +// Whether the source is managed by srcman or was created by the user locally +type SourceManaged int32 + +const ( + SourceManaged_LOCAL SourceManaged = 0 // Local is the default + SourceManaged_MANAGED SourceManaged = 1 +) + +// Enum value maps for SourceManaged. +var ( + SourceManaged_name = map[int32]string{ + 0: "LOCAL", + 1: "MANAGED", + } + SourceManaged_value = map[string]int32{ + "LOCAL": 0, + "MANAGED": 1, + } +) + +func (x SourceManaged) Enum() *SourceManaged { + p := new(SourceManaged) + *p = x + return p +} + +func (x SourceManaged) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SourceManaged) Descriptor() protoreflect.EnumDescriptor { + return file_account_proto_enumTypes[3].Descriptor() +} + +func (SourceManaged) Type() protoreflect.EnumType { + return &file_account_proto_enumTypes[3] +} + +func (x SourceManaged) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SourceManaged.Descriptor instead. +func (SourceManaged) EnumDescriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{3} +} + +type AdapterCategory int32 + +const ( + // Fall-back category for resources that do not fit into any other category + AdapterCategory_ADAPTER_CATEGORY_OTHER AdapterCategory = 0 + // This category includes resources that provide processing power and host + // applications or services. Examples are virtual machines, containers, + // serverless functions, and application hosting platforms. If the primary + // purpose of a resource is to execute workloads, run code, or host + // applications, it should belong here. + AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION AdapterCategory = 1 + // Encompassing resources designed to store, archive, and manage data, this + // category includes object storage, block storage, file storage, and data + // backup solutions. Select this category when the core function of a + // resource is persistent data storage or management + AdapterCategory_ADAPTER_CATEGORY_STORAGE AdapterCategory = 2 + // This category covers resources that facilitate connectivity and + // communication within cloud environments. Typical resources include + // virtual networks, load balancers, VPNs, and DNS services. Assign + // resources here if their primary role is related to communication, + // connectivity, or traffic management + AdapterCategory_ADAPTER_CATEGORY_NETWORK AdapterCategory = 3 + // Resources in this category focus on safeguarding data, applications, and + // cloud infrastructure. Examples include firewalls, identity and access + // management, encryption services, and security monitoring tools. Choose + // this category if a resource's main function is security, access control, + // or compliance + AdapterCategory_ADAPTER_CATEGORY_SECURITY AdapterCategory = 4 + // This category includes resources aimed at monitoring, tracing, and + // logging applications and cloud infrastructure. Examples are monitoring + // tools, logging services, and performance management solutions. Use this + // category for resources that provide insights into system performance and + // health + AdapterCategory_ADAPTER_CATEGORY_OBSERVABILITY AdapterCategory = 5 + // Focused on structured data storage and management, this category includes + // relational, NoSQL, and in-memory databases, along with data warehousing + // solutions. Choose this category for resources specifically designed for + // data querying, transaction processing, or complex data operations. This + // differs from "storage" in that "databases" have compute associated with + // them rather than just storing data. + AdapterCategory_ADAPTER_CATEGORY_DATABASE AdapterCategory = 6 + // This category includes resources designed for managing configurations and + // deployments. Examples are infrastructure as code tools, configuration + // management services, and deployment orchestration solutions. Classify + // resources here if they primarily handle configuration, environment + // management, or automated deployment + AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION AdapterCategory = 7 + // This category is dedicated to resources for developing, training, and + // deploying artificial intelligence models and machine learning + // applications. Include machine learning platforms, AI services, and data + // labeling tools here. Select this category if a resource's principal + // function involves AI or machine learning processes + AdapterCategory_ADAPTER_CATEGORY_AI AdapterCategory = 8 +) + +// Enum value maps for AdapterCategory. +var ( + AdapterCategory_name = map[int32]string{ + 0: "ADAPTER_CATEGORY_OTHER", + 1: "ADAPTER_CATEGORY_COMPUTE_APPLICATION", + 2: "ADAPTER_CATEGORY_STORAGE", + 3: "ADAPTER_CATEGORY_NETWORK", + 4: "ADAPTER_CATEGORY_SECURITY", + 5: "ADAPTER_CATEGORY_OBSERVABILITY", + 6: "ADAPTER_CATEGORY_DATABASE", + 7: "ADAPTER_CATEGORY_CONFIGURATION", + 8: "ADAPTER_CATEGORY_AI", + } + AdapterCategory_value = map[string]int32{ + "ADAPTER_CATEGORY_OTHER": 0, + "ADAPTER_CATEGORY_COMPUTE_APPLICATION": 1, + "ADAPTER_CATEGORY_STORAGE": 2, + "ADAPTER_CATEGORY_NETWORK": 3, + "ADAPTER_CATEGORY_SECURITY": 4, + "ADAPTER_CATEGORY_OBSERVABILITY": 5, + "ADAPTER_CATEGORY_DATABASE": 6, + "ADAPTER_CATEGORY_CONFIGURATION": 7, + "ADAPTER_CATEGORY_AI": 8, + } +) + +func (x AdapterCategory) Enum() *AdapterCategory { + p := new(AdapterCategory) + *p = x + return p +} + +func (x AdapterCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AdapterCategory) Descriptor() protoreflect.EnumDescriptor { + return file_account_proto_enumTypes[4].Descriptor() +} + +func (AdapterCategory) Type() protoreflect.EnumType { + return &file_account_proto_enumTypes[4] +} + +func (x AdapterCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AdapterCategory.Descriptor instead. +func (AdapterCategory) EnumDescriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{4} +} + +type ListAccountsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAccountsRequest) Reset() { + *x = ListAccountsRequest{} + mi := &file_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAccountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAccountsRequest) ProtoMessage() {} + +func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[0] + 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 ListAccountsRequest.ProtoReflect.Descriptor instead. +func (*ListAccountsRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{0} +} + +type ListAccountsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accounts []*Account `protobuf:"bytes,1,rep,name=accounts,proto3" json:"accounts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAccountsResponse) Reset() { + *x = ListAccountsResponse{} + mi := &file_account_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAccountsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAccountsResponse) ProtoMessage() {} + +func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[1] + 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 ListAccountsResponse.ProtoReflect.Descriptor instead. +func (*ListAccountsResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{1} +} + +func (x *ListAccountsResponse) GetAccounts() []*Account { + if x != nil { + return x.Accounts + } + return nil +} + +type CreateAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *AccountProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAccountRequest) Reset() { + *x = CreateAccountRequest{} + mi := &file_account_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAccountRequest) ProtoMessage() {} + +func (x *CreateAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[2] + 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 CreateAccountRequest.ProtoReflect.Descriptor instead. +func (*CreateAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateAccountRequest) GetProperties() *AccountProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAccountResponse) Reset() { + *x = CreateAccountResponse{} + mi := &file_account_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAccountResponse) ProtoMessage() {} + +func (x *CreateAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[3] + 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 CreateAccountResponse.ProtoReflect.Descriptor instead. +func (*CreateAccountResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateAccountResponse) GetAccount() *Account { + if x != nil { + return x.Account + } + return nil +} + +type UpdateAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *AccountProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAccountRequest) Reset() { + *x = UpdateAccountRequest{} + mi := &file_account_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAccountRequest) ProtoMessage() {} + +func (x *UpdateAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[4] + 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 UpdateAccountRequest.ProtoReflect.Descriptor instead. +func (*UpdateAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateAccountRequest) GetProperties() *AccountProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAccountResponse) Reset() { + *x = UpdateAccountResponse{} + mi := &file_account_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAccountResponse) ProtoMessage() {} + +func (x *UpdateAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[5] + 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 UpdateAccountResponse.ProtoReflect.Descriptor instead. +func (*UpdateAccountResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateAccountResponse) GetAccount() *Account { + if x != nil { + return x.Account + } + return nil +} + +type AdminUpdateAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the account to update + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Request *UpdateAccountRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminUpdateAccountRequest) Reset() { + *x = AdminUpdateAccountRequest{} + mi := &file_account_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminUpdateAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminUpdateAccountRequest) ProtoMessage() {} + +func (x *AdminUpdateAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[6] + 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 AdminUpdateAccountRequest.ProtoReflect.Descriptor instead. +func (*AdminUpdateAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{6} +} + +func (x *AdminUpdateAccountRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AdminUpdateAccountRequest) GetRequest() *UpdateAccountRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminGetAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the account to get + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminGetAccountRequest) Reset() { + *x = AdminGetAccountRequest{} + mi := &file_account_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminGetAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminGetAccountRequest) ProtoMessage() {} + +func (x *AdminGetAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[7] + 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 AdminGetAccountRequest.ProtoReflect.Descriptor instead. +func (*AdminGetAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{7} +} + +func (x *AdminGetAccountRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type AdminDeleteAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the account to delete + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminDeleteAccountRequest) Reset() { + *x = AdminDeleteAccountRequest{} + mi := &file_account_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminDeleteAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminDeleteAccountRequest) ProtoMessage() {} + +func (x *AdminDeleteAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[8] + 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 AdminDeleteAccountRequest.ProtoReflect.Descriptor instead. +func (*AdminDeleteAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{8} +} + +func (x *AdminDeleteAccountRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type AdminDeleteAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminDeleteAccountResponse) Reset() { + *x = AdminDeleteAccountResponse{} + mi := &file_account_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminDeleteAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminDeleteAccountResponse) ProtoMessage() {} + +func (x *AdminDeleteAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[9] + 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 AdminDeleteAccountResponse.ProtoReflect.Descriptor instead. +func (*AdminDeleteAccountResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{9} +} + +type AdminListSourcesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *ListSourcesRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminListSourcesRequest) Reset() { + *x = AdminListSourcesRequest{} + mi := &file_account_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminListSourcesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminListSourcesRequest) ProtoMessage() {} + +func (x *AdminListSourcesRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[10] + 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 AdminListSourcesRequest.ProtoReflect.Descriptor instead. +func (*AdminListSourcesRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{10} +} + +func (x *AdminListSourcesRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminListSourcesRequest) GetRequest() *ListSourcesRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminCreateSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *CreateSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminCreateSourceRequest) Reset() { + *x = AdminCreateSourceRequest{} + mi := &file_account_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminCreateSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminCreateSourceRequest) ProtoMessage() {} + +func (x *AdminCreateSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[11] + 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 AdminCreateSourceRequest.ProtoReflect.Descriptor instead. +func (*AdminCreateSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{11} +} + +func (x *AdminCreateSourceRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminCreateSourceRequest) GetRequest() *CreateSourceRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminGetSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *GetSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminGetSourceRequest) Reset() { + *x = AdminGetSourceRequest{} + mi := &file_account_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminGetSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminGetSourceRequest) ProtoMessage() {} + +func (x *AdminGetSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[12] + 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 AdminGetSourceRequest.ProtoReflect.Descriptor instead. +func (*AdminGetSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{12} +} + +func (x *AdminGetSourceRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminGetSourceRequest) GetRequest() *GetSourceRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminUpdateSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *UpdateSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminUpdateSourceRequest) Reset() { + *x = AdminUpdateSourceRequest{} + mi := &file_account_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminUpdateSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminUpdateSourceRequest) ProtoMessage() {} + +func (x *AdminUpdateSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminUpdateSourceRequest.ProtoReflect.Descriptor instead. +func (*AdminUpdateSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{13} +} + +func (x *AdminUpdateSourceRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminUpdateSourceRequest) GetRequest() *UpdateSourceRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminDeleteSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *DeleteSourceRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminDeleteSourceRequest) Reset() { + *x = AdminDeleteSourceRequest{} + mi := &file_account_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminDeleteSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminDeleteSourceRequest) ProtoMessage() {} + +func (x *AdminDeleteSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminDeleteSourceRequest.ProtoReflect.Descriptor instead. +func (*AdminDeleteSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{14} +} + +func (x *AdminDeleteSourceRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminDeleteSourceRequest) GetRequest() *DeleteSourceRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminKeepaliveSourcesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *KeepaliveSourcesRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminKeepaliveSourcesRequest) Reset() { + *x = AdminKeepaliveSourcesRequest{} + mi := &file_account_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminKeepaliveSourcesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminKeepaliveSourcesRequest) ProtoMessage() {} + +func (x *AdminKeepaliveSourcesRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[15] + 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 AdminKeepaliveSourcesRequest.ProtoReflect.Descriptor instead. +func (*AdminKeepaliveSourcesRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{15} +} + +func (x *AdminKeepaliveSourcesRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminKeepaliveSourcesRequest) GetRequest() *KeepaliveSourcesRequest { + if x != nil { + return x.Request + } + return nil +} + +type AdminCreateTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + Request *CreateTokenRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminCreateTokenRequest) Reset() { + *x = AdminCreateTokenRequest{} + mi := &file_account_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminCreateTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminCreateTokenRequest) ProtoMessage() {} + +func (x *AdminCreateTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[16] + 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 AdminCreateTokenRequest.ProtoReflect.Descriptor instead. +func (*AdminCreateTokenRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{16} +} + +func (x *AdminCreateTokenRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *AdminCreateTokenRequest) GetRequest() *CreateTokenRequest { + if x != nil { + return x.Request + } + return nil +} + +type Source struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *SourceMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *SourceProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Source) Reset() { + *x = Source{} + mi := &file_account_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Source) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Source) ProtoMessage() {} + +func (x *Source) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Source.ProtoReflect.Descriptor instead. +func (*Source) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{17} +} + +func (x *Source) GetMetadata() *SourceMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Source) GetProperties() *SourceProperties { + if x != nil { + return x.Properties + } + return nil +} + +type SourceMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` // TODO: Change to ID along with everything else + // The name of the NATS JWT that has been generated for this source + TokenName string `protobuf:"bytes,2,opt,name=TokenName,proto3" json:"TokenName,omitempty"` + // When the NATS JWT expires (unix time) + TokenExpiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=TokenExpiry,proto3" json:"TokenExpiry,omitempty"` + // The public NKey associated with the NATS JWT + PublicNkey string `protobuf:"bytes,5,opt,name=PublicNkey,proto3" json:"PublicNkey,omitempty"` + // Status of the source + Status SourceStatus `protobuf:"varint,9,opt,name=Status,proto3,enum=account.SourceStatus" json:"Status,omitempty"` + // The error message if the source is unhealthy + Error string `protobuf:"bytes,10,opt,name=Error,proto3" json:"Error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceMetadata) Reset() { + *x = SourceMetadata{} + mi := &file_account_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceMetadata) ProtoMessage() {} + +func (x *SourceMetadata) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceMetadata.ProtoReflect.Descriptor instead. +func (*SourceMetadata) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{18} +} + +func (x *SourceMetadata) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *SourceMetadata) GetTokenName() string { + if x != nil { + return x.TokenName + } + return "" +} + +func (x *SourceMetadata) GetTokenExpiry() *timestamppb.Timestamp { + if x != nil { + return x.TokenExpiry + } + return nil +} + +func (x *SourceMetadata) GetPublicNkey() string { + if x != nil { + return x.PublicNkey + } + return "" +} + +func (x *SourceMetadata) GetStatus() SourceStatus { + if x != nil { + return x.Status + } + return SourceStatus_STATUS_UNSPECIFIED +} + +func (x *SourceMetadata) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// A source that is capable of discovering items +type SourceProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The descriptive name of the source + DescriptiveName string `protobuf:"bytes,1,opt,name=DescriptiveName,proto3" json:"DescriptiveName,omitempty"` + // What source to configure. Can be "stdlib", "aws", or "gcp". + Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` + // Config for this source. See the source documentation for what + // source-specific config is available/required. This will be supplied + // directly to viper via a config file at `/etc/srcman/config/source.yaml` + Config *structpb.Struct `protobuf:"bytes,3,opt,name=Config,proto3" json:"Config,omitempty"` + // Additional config options that should be passed to the source. The keys + // of this object should be file names, and the values should be their + // content. These files will be made available to the source at runtime. + // Check the source's documentation for what to configure here if required + AdditionalConfig *structpb.Struct `protobuf:"bytes,4,opt,name=AdditionalConfig,proto3" json:"AdditionalConfig,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceProperties) Reset() { + *x = SourceProperties{} + mi := &file_account_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceProperties) ProtoMessage() {} + +func (x *SourceProperties) ProtoReflect() protoreflect.Message { + mi := &file_account_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 SourceProperties.ProtoReflect.Descriptor instead. +func (*SourceProperties) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{19} +} + +func (x *SourceProperties) GetDescriptiveName() string { + if x != nil { + return x.DescriptiveName + } + return "" +} + +func (x *SourceProperties) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SourceProperties) GetConfig() *structpb.Struct { + if x != nil { + return x.Config + } + return nil +} + +func (x *SourceProperties) GetAdditionalConfig() *structpb.Struct { + if x != nil { + return x.AdditionalConfig + } + return nil +} + +type Account struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *AccountMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *AccountProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Account) Reset() { + *x = Account{} + mi := &file_account_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[20] + 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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{20} +} + +func (x *Account) GetMetadata() *AccountMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Account) GetProperties() *AccountProperties { + if x != nil { + return x.Properties + } + return nil +} + +type AccountMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The public Nkey which signs all NATS "user" tokens + PublicNkey string `protobuf:"bytes,2,opt,name=PublicNkey,proto3" json:"PublicNkey,omitempty"` + // Repositories that have been used in this account + Repositories []*Repository `protobuf:"bytes,3,rep,name=repositories,proto3" json:"repositories,omitempty"` + // The total number of repositories associated with this account + TotalRepositories uint32 `protobuf:"varint,4,opt,name=totalRepositories,proto3" json:"totalRepositories,omitempty"` + // The number of active repositories (for billing purposes) + ActiveRepositories uint32 `protobuf:"varint,5,opt,name=activeRepositories,proto3" json:"activeRepositories,omitempty"` + // The billing plan for this account + Plan AccountPlan `protobuf:"varint,6,opt,name=Plan,proto3,enum=account.AccountPlan" json:"Plan,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccountMetadata) Reset() { + *x = AccountMetadata{} + mi := &file_account_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccountMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountMetadata) ProtoMessage() {} + +func (x *AccountMetadata) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[21] + 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 AccountMetadata.ProtoReflect.Descriptor instead. +func (*AccountMetadata) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{21} +} + +func (x *AccountMetadata) GetPublicNkey() string { + if x != nil { + return x.PublicNkey + } + return "" +} + +func (x *AccountMetadata) GetRepositories() []*Repository { + if x != nil { + return x.Repositories + } + return nil +} + +func (x *AccountMetadata) GetTotalRepositories() uint32 { + if x != nil { + return x.TotalRepositories + } + return 0 +} + +func (x *AccountMetadata) GetActiveRepositories() uint32 { + if x != nil { + return x.ActiveRepositories + } + return 0 +} + +func (x *AccountMetadata) GetPlan() AccountPlan { + if x != nil { + return x.Plan + } + return AccountPlan_ACCOUNT_PLAN_UNSPECIFIED +} + +type Repository struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Repository identifier; can be a URL, name, or any string identifier. Not necessarily a URL. CLI attempts auto-population, but users can override. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The number of changes that have been recorded in this repository + NumChanges int64 `protobuf:"varint,2,opt,name=numChanges,proto3" json:"numChanges,omitempty"` + // The last time a change was recorded in this repository + LastChangeAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastChangeAt,proto3" json:"lastChangeAt,omitempty"` + // The status of the repository (active or inactive). This is determined + // based on the last change that was recorded. + Status RepositoryStatus `protobuf:"varint,4,opt,name=status,proto3,enum=account.RepositoryStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Repository) Reset() { + *x = Repository{} + mi := &file_account_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Repository) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Repository) ProtoMessage() {} + +func (x *Repository) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[22] + 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 Repository.ProtoReflect.Descriptor instead. +func (*Repository) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{22} +} + +func (x *Repository) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Repository) GetNumChanges() int64 { + if x != nil { + return x.NumChanges + } + return 0 +} + +func (x *Repository) GetLastChangeAt() *timestamppb.Timestamp { + if x != nil { + return x.LastChangeAt + } + return nil +} + +func (x *Repository) GetStatus() RepositoryStatus { + if x != nil { + return x.Status + } + return RepositoryStatus_REPOSITORY_STATUS_UNSPECIFIED +} + +type AccountProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the account + Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` + // The Customer ID within Stripe + StripeCustomerID string `protobuf:"bytes,2,opt,name=StripeCustomerID,proto3" json:"StripeCustomerID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccountProperties) Reset() { + *x = AccountProperties{} + mi := &file_account_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccountProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountProperties) ProtoMessage() {} + +func (x *AccountProperties) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[23] + 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 AccountProperties.ProtoReflect.Descriptor instead. +func (*AccountProperties) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{23} +} + +func (x *AccountProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AccountProperties) GetStripeCustomerID() string { + if x != nil { + return x.StripeCustomerID + } + return "" +} + +type GetAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAccountRequest) Reset() { + *x = GetAccountRequest{} + mi := &file_account_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAccountRequest) ProtoMessage() {} + +func (x *GetAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_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 GetAccountRequest.ProtoReflect.Descriptor instead. +func (*GetAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{24} +} + +type GetAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Account *Account `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAccountResponse) Reset() { + *x = GetAccountResponse{} + mi := &file_account_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAccountResponse) ProtoMessage() {} + +func (x *GetAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[25] + 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 GetAccountResponse.ProtoReflect.Descriptor instead. +func (*GetAccountResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{25} +} + +func (x *GetAccountResponse) GetAccount() *Account { + if x != nil { + return x.Account + } + return nil +} + +type DeleteAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Set to true to confirm that the user is sure they want to delete their + // account. This is to prevent accidental deletions + IAmSure bool `protobuf:"varint,1,opt,name=iAmSure,proto3" json:"iAmSure,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAccountRequest) Reset() { + *x = DeleteAccountRequest{} + mi := &file_account_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAccountRequest) ProtoMessage() {} + +func (x *DeleteAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[26] + 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 DeleteAccountRequest.ProtoReflect.Descriptor instead. +func (*DeleteAccountRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{26} +} + +func (x *DeleteAccountRequest) GetIAmSure() bool { + if x != nil { + return x.IAmSure + } + return false +} + +type DeleteAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAccountResponse) Reset() { + *x = DeleteAccountResponse{} + mi := &file_account_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAccountResponse) ProtoMessage() {} + +func (x *DeleteAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[27] + 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 DeleteAccountResponse.ProtoReflect.Descriptor instead. +func (*DeleteAccountResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{27} +} + +type ListSourcesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSourcesRequest) Reset() { + *x = ListSourcesRequest{} + mi := &file_account_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSourcesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSourcesRequest) ProtoMessage() {} + +func (x *ListSourcesRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[28] + 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 ListSourcesRequest.ProtoReflect.Descriptor instead. +func (*ListSourcesRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{28} +} + +type ListSourcesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sources []*Source `protobuf:"bytes,1,rep,name=Sources,proto3" json:"Sources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSourcesResponse) Reset() { + *x = ListSourcesResponse{} + mi := &file_account_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSourcesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSourcesResponse) ProtoMessage() {} + +func (x *ListSourcesResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[29] + 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 ListSourcesResponse.ProtoReflect.Descriptor instead. +func (*ListSourcesResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{29} +} + +func (x *ListSourcesResponse) GetSources() []*Source { + if x != nil { + return x.Sources + } + return nil +} + +type CreateSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *SourceProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSourceRequest) Reset() { + *x = CreateSourceRequest{} + mi := &file_account_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSourceRequest) ProtoMessage() {} + +func (x *CreateSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[30] + 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 CreateSourceRequest.ProtoReflect.Descriptor instead. +func (*CreateSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{30} +} + +func (x *CreateSourceRequest) GetProperties() *SourceProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateSourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSourceResponse) Reset() { + *x = CreateSourceResponse{} + mi := &file_account_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSourceResponse) ProtoMessage() {} + +func (x *CreateSourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[31] + 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 CreateSourceResponse.ProtoReflect.Descriptor instead. +func (*CreateSourceResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{31} +} + +func (x *CreateSourceResponse) GetSource() *Source { + if x != nil { + return x.Source + } + return nil +} + +type GetSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSourceRequest) Reset() { + *x = GetSourceRequest{} + mi := &file_account_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSourceRequest) ProtoMessage() {} + +func (x *GetSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[32] + 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 GetSourceRequest.ProtoReflect.Descriptor instead. +func (*GetSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{32} +} + +func (x *GetSourceRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type GetSourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSourceResponse) Reset() { + *x = GetSourceResponse{} + mi := &file_account_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSourceResponse) ProtoMessage() {} + +func (x *GetSourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[33] + 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 GetSourceResponse.ProtoReflect.Descriptor instead. +func (*GetSourceResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{33} +} + +func (x *GetSourceResponse) GetSource() *Source { + if x != nil { + return x.Source + } + return nil +} + +type UpdateSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ID of the source to update + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Properties to update + Properties *SourceProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSourceRequest) Reset() { + *x = UpdateSourceRequest{} + mi := &file_account_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSourceRequest) ProtoMessage() {} + +func (x *UpdateSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[34] + 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 UpdateSourceRequest.ProtoReflect.Descriptor instead. +func (*UpdateSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{34} +} + +func (x *UpdateSourceRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *UpdateSourceRequest) GetProperties() *SourceProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateSourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source *Source `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSourceResponse) Reset() { + *x = UpdateSourceResponse{} + mi := &file_account_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSourceResponse) ProtoMessage() {} + +func (x *UpdateSourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[35] + 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 UpdateSourceResponse.ProtoReflect.Descriptor instead. +func (*UpdateSourceResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{35} +} + +func (x *UpdateSourceResponse) GetSource() *Source { + if x != nil { + return x.Source + } + return nil +} + +type DeleteSourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ID if the source to delete + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSourceRequest) Reset() { + *x = DeleteSourceRequest{} + mi := &file_account_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSourceRequest) ProtoMessage() {} + +func (x *DeleteSourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[36] + 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 DeleteSourceRequest.ProtoReflect.Descriptor instead. +func (*DeleteSourceRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{36} +} + +func (x *DeleteSourceRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type DeleteSourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSourceResponse) Reset() { + *x = DeleteSourceResponse{} + mi := &file_account_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSourceResponse) ProtoMessage() {} + +func (x *DeleteSourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[37] + 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 DeleteSourceResponse.ProtoReflect.Descriptor instead. +func (*DeleteSourceResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{37} +} + +type SourceKeepaliveResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the source that was kept alive + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The status of the source + Status SourceStatus `protobuf:"varint,2,opt,name=Status,proto3,enum=account.SourceStatus" json:"Status,omitempty"` + // The error message if the source is unhealthy + Error string `protobuf:"bytes,3,opt,name=Error,proto3" json:"Error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceKeepaliveResult) Reset() { + *x = SourceKeepaliveResult{} + mi := &file_account_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceKeepaliveResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceKeepaliveResult) ProtoMessage() {} + +func (x *SourceKeepaliveResult) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[38] + 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 SourceKeepaliveResult.ProtoReflect.Descriptor instead. +func (*SourceKeepaliveResult) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{38} +} + +func (x *SourceKeepaliveResult) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *SourceKeepaliveResult) GetStatus() SourceStatus { + if x != nil { + return x.Status + } + return SourceStatus_STATUS_UNSPECIFIED +} + +func (x *SourceKeepaliveResult) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type ListAllSourcesStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAllSourcesStatusRequest) Reset() { + *x = ListAllSourcesStatusRequest{} + mi := &file_account_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAllSourcesStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllSourcesStatusRequest) ProtoMessage() {} + +func (x *ListAllSourcesStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[39] + 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 ListAllSourcesStatusRequest.ProtoReflect.Descriptor instead. +func (*ListAllSourcesStatusRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{39} +} + +type SourceHealth struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the source + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The version of the source + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // The name of the source + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // The error message if the source is unhealthy + Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` + // The status of the source, this is calculated based on the last heartbeat received and if there is an error + Status SourceStatus `protobuf:"varint,5,opt,name=status,proto3,enum=account.SourceStatus" json:"status,omitempty"` + // Created at time + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + // The last time we received a heartbeat from the source + LastHeartbeat *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=lastHeartbeat,proto3" json:"lastHeartbeat,omitempty"` + // The next time we expect to receive a heartbeat from the source + NextHeartbeat *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=nextHeartbeat,proto3" json:"nextHeartbeat,omitempty"` + // The type of the source, AWS or Stdlib or Kubernetes + Type string `protobuf:"bytes,9,opt,name=type,proto3" json:"type,omitempty"` + // Whether the source is managed, or local + Managed SourceManaged `protobuf:"varint,10,opt,name=managed,proto3,enum=account.SourceManaged" json:"managed,omitempty"` + // The types of sources that this source can discover + AvailableTypes []string `protobuf:"bytes,11,rep,name=availableTypes,proto3" json:"availableTypes,omitempty"` + // The scopes that this source can discover + AvailableScopes []string `protobuf:"bytes,12,rep,name=availableScopes,proto3" json:"availableScopes,omitempty"` + // AdapterMetadata is a map of metadata that the source can send to the API + AdapterMetadata []*AdapterMetadata `protobuf:"bytes,13,rep,name=adapterMetadata,proto3" json:"adapterMetadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceHealth) Reset() { + *x = SourceHealth{} + mi := &file_account_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceHealth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceHealth) ProtoMessage() {} + +func (x *SourceHealth) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[40] + 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 SourceHealth.ProtoReflect.Descriptor instead. +func (*SourceHealth) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{40} +} + +func (x *SourceHealth) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *SourceHealth) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *SourceHealth) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SourceHealth) GetError() string { + if x != nil && x.Error != nil { + return *x.Error + } + return "" +} + +func (x *SourceHealth) GetStatus() SourceStatus { + if x != nil { + return x.Status + } + return SourceStatus_STATUS_UNSPECIFIED +} + +func (x *SourceHealth) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *SourceHealth) GetLastHeartbeat() *timestamppb.Timestamp { + if x != nil { + return x.LastHeartbeat + } + return nil +} + +func (x *SourceHealth) GetNextHeartbeat() *timestamppb.Timestamp { + if x != nil { + return x.NextHeartbeat + } + return nil +} + +func (x *SourceHealth) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SourceHealth) GetManaged() SourceManaged { + if x != nil { + return x.Managed + } + return SourceManaged_LOCAL +} + +func (x *SourceHealth) GetAvailableTypes() []string { + if x != nil { + return x.AvailableTypes + } + return nil +} + +func (x *SourceHealth) GetAvailableScopes() []string { + if x != nil { + return x.AvailableScopes + } + return nil +} + +func (x *SourceHealth) GetAdapterMetadata() []*AdapterMetadata { + if x != nil { + return x.AdapterMetadata + } + return nil +} + +type ListAllSourcesStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sources []*SourceHealth `protobuf:"bytes,1,rep,name=sources,proto3" json:"sources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAllSourcesStatusResponse) Reset() { + *x = ListAllSourcesStatusResponse{} + mi := &file_account_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAllSourcesStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllSourcesStatusResponse) ProtoMessage() {} + +func (x *ListAllSourcesStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[41] + 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 ListAllSourcesStatusResponse.ProtoReflect.Descriptor instead. +func (*ListAllSourcesStatusResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{41} +} + +func (x *ListAllSourcesStatusResponse) GetSources() []*SourceHealth { + if x != nil { + return x.Sources + } + return nil +} + +// The source sends a heartbeat to the API to let it know that it is still alive, note it does not give a status. +type SubmitSourceHeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the source that is sending the heartbeat + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The version of the source + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // The name of the source + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // The error message if the source is unhealthy + Error *string `protobuf:"bytes,4,opt,name=error,proto3,oneof" json:"error,omitempty"` + // The maximum time between heartbeats that the source can send to the api-server. Otherwise, the source will be marked as unhealthy. eg 30s + NextHeartbeatMax *durationpb.Duration `protobuf:"bytes,5,opt,name=nextHeartbeatMax,proto3" json:"nextHeartbeatMax,omitempty"` + // The type of the source, AWS or Stdlib or Kubernetes + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + // Whether the source is managed, or local + Managed SourceManaged `protobuf:"varint,7,opt,name=managed,proto3,enum=account.SourceManaged" json:"managed,omitempty"` + // The scopes that this source can discover + AvailableScopes []string `protobuf:"bytes,9,rep,name=availableScopes,proto3" json:"availableScopes,omitempty"` + // AdapterMetadata is a map of metadata that the source can send to the API + AdapterMetadata []*AdapterMetadata `protobuf:"bytes,10,rep,name=adapterMetadata,proto3" json:"adapterMetadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitSourceHeartbeatRequest) Reset() { + *x = SubmitSourceHeartbeatRequest{} + mi := &file_account_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitSourceHeartbeatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitSourceHeartbeatRequest) ProtoMessage() {} + +func (x *SubmitSourceHeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[42] + 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 SubmitSourceHeartbeatRequest.ProtoReflect.Descriptor instead. +func (*SubmitSourceHeartbeatRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{42} +} + +func (x *SubmitSourceHeartbeatRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *SubmitSourceHeartbeatRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *SubmitSourceHeartbeatRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SubmitSourceHeartbeatRequest) GetError() string { + if x != nil && x.Error != nil { + return *x.Error + } + return "" +} + +func (x *SubmitSourceHeartbeatRequest) GetNextHeartbeatMax() *durationpb.Duration { + if x != nil { + return x.NextHeartbeatMax + } + return nil +} + +func (x *SubmitSourceHeartbeatRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SubmitSourceHeartbeatRequest) GetManaged() SourceManaged { + if x != nil { + return x.Managed + } + return SourceManaged_LOCAL +} + +func (x *SubmitSourceHeartbeatRequest) GetAvailableScopes() []string { + if x != nil { + return x.AvailableScopes + } + return nil +} + +func (x *SubmitSourceHeartbeatRequest) GetAdapterMetadata() []*AdapterMetadata { + if x != nil { + return x.AdapterMetadata + } + return nil +} + +type AdapterMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of item that this adapter returns e.g. eks-cluster + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // The category that these items fall under + Category AdapterCategory `protobuf:"varint,2,opt,name=category,proto3,enum=account.AdapterCategory" json:"category,omitempty"` + // The list of other types that this can be linked to, eg eks-cluster -> + // eks-node-group + PotentialLinks []string `protobuf:"bytes,3,rep,name=potentialLinks,proto3" json:"potentialLinks,omitempty"` + // A descriptive name of the types of items that are returned by this + // adapter e.g. "EKS Cluster" + DescriptiveName string `protobuf:"bytes,4,opt,name=descriptiveName,proto3" json:"descriptiveName,omitempty"` + // The supported query methods for this adapter + SupportedQueryMethods *AdapterSupportedQueryMethods `protobuf:"bytes,5,opt,name=supportedQueryMethods,proto3" json:"supportedQueryMethods,omitempty"` + // The terraform mappings for this adapter, this is optional + TerraformMappings []*TerraformMapping `protobuf:"bytes,6,rep,name=terraformMappings,proto3" json:"terraformMappings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdapterMetadata) Reset() { + *x = AdapterMetadata{} + mi := &file_account_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdapterMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdapterMetadata) ProtoMessage() {} + +func (x *AdapterMetadata) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[43] + 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 AdapterMetadata.ProtoReflect.Descriptor instead. +func (*AdapterMetadata) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{43} +} + +func (x *AdapterMetadata) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *AdapterMetadata) GetCategory() AdapterCategory { + if x != nil { + return x.Category + } + return AdapterCategory_ADAPTER_CATEGORY_OTHER +} + +func (x *AdapterMetadata) GetPotentialLinks() []string { + if x != nil { + return x.PotentialLinks + } + return nil +} + +func (x *AdapterMetadata) GetDescriptiveName() string { + if x != nil { + return x.DescriptiveName + } + return "" +} + +func (x *AdapterMetadata) GetSupportedQueryMethods() *AdapterSupportedQueryMethods { + if x != nil { + return x.SupportedQueryMethods + } + return nil +} + +func (x *AdapterMetadata) GetTerraformMappings() []*TerraformMapping { + if x != nil { + return x.TerraformMappings + } + return nil +} + +// The methods that this adapter supports, and the description of how to use +// them +type AdapterSupportedQueryMethods struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether or not the GET method is supported + Get bool `protobuf:"varint,1,opt,name=get,proto3" json:"get,omitempty"` + // Description of what data the GET query expects. + GetDescription string `protobuf:"bytes,2,opt,name=getDescription,proto3" json:"getDescription,omitempty"` + // Whether or not the LIST method is supported + List bool `protobuf:"varint,3,opt,name=list,proto3" json:"list,omitempty"` + // Description of how the LIST method works + ListDescription string `protobuf:"bytes,4,opt,name=listDescription,proto3" json:"listDescription,omitempty"` + // Whether or not the SEARCH method is supported + Search bool `protobuf:"varint,5,opt,name=search,proto3" json:"search,omitempty"` + // Description of the query that should be passed to the SEARCH method + SearchDescription string `protobuf:"bytes,6,opt,name=searchDescription,proto3" json:"searchDescription,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdapterSupportedQueryMethods) Reset() { + *x = AdapterSupportedQueryMethods{} + mi := &file_account_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdapterSupportedQueryMethods) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdapterSupportedQueryMethods) ProtoMessage() {} + +func (x *AdapterSupportedQueryMethods) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[44] + 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 AdapterSupportedQueryMethods.ProtoReflect.Descriptor instead. +func (*AdapterSupportedQueryMethods) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{44} +} + +func (x *AdapterSupportedQueryMethods) GetGet() bool { + if x != nil { + return x.Get + } + return false +} + +func (x *AdapterSupportedQueryMethods) GetGetDescription() string { + if x != nil { + return x.GetDescription + } + return "" +} + +func (x *AdapterSupportedQueryMethods) GetList() bool { + if x != nil { + return x.List + } + return false +} + +func (x *AdapterSupportedQueryMethods) GetListDescription() string { + if x != nil { + return x.ListDescription + } + return "" +} + +func (x *AdapterSupportedQueryMethods) GetSearch() bool { + if x != nil { + return x.Search + } + return false +} + +func (x *AdapterSupportedQueryMethods) GetSearchDescription() string { + if x != nil { + return x.SearchDescription + } + return "" +} + +// When Overmind ingests Terraform changes, it needs to be able to map from a +// given Terraform resource, to that same resource in Overmind. This is achieved +// by using the TerraformMapping object. It translates the details of a Terraform +// resource into a query that Overmind can run. +// +// NOTE: The queries that are generated by this mapping use the wildcard scope +// `*` and therefore could return multiple items. Overmind will compare the +// attributes of these items to determine the most likely candidate for a mch +// and select that. +type TerraformMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The method that the query should use + TerraformMethod QueryMethod `protobuf:"varint,1,opt,name=terraformMethod,proto3,enum=QueryMethod" json:"terraformMethod,omitempty"` + // How to map data from the terraform resource to the "query" field in the + // resulting mapping query. This uses HCL syntax e.g. + // resource_type.attribute_name + // + // Usually this will be the attribute that uniquely identifies the resource + // such as `aws_instance.id` or `aws_iam_role.arn`. You can also index into + // arrays e.g. `kubernetes_replication_controller.metadata[0].name` + TerraformQueryMap string `protobuf:"bytes,2,opt,name=terraformQueryMap,proto3" json:"terraformQueryMap,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerraformMapping) Reset() { + *x = TerraformMapping{} + mi := &file_account_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerraformMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerraformMapping) ProtoMessage() {} + +func (x *TerraformMapping) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[45] + 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 TerraformMapping.ProtoReflect.Descriptor instead. +func (*TerraformMapping) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{45} +} + +func (x *TerraformMapping) GetTerraformMethod() QueryMethod { + if x != nil { + return x.TerraformMethod + } + return QueryMethod_GET +} + +func (x *TerraformMapping) GetTerraformQueryMap() string { + if x != nil { + return x.TerraformQueryMap + } + return "" +} + +type SubmitSourceHeartbeatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitSourceHeartbeatResponse) Reset() { + *x = SubmitSourceHeartbeatResponse{} + mi := &file_account_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitSourceHeartbeatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitSourceHeartbeatResponse) ProtoMessage() {} + +func (x *SubmitSourceHeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[46] + 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 SubmitSourceHeartbeatResponse.ProtoReflect.Descriptor instead. +func (*SubmitSourceHeartbeatResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{46} +} + +type KeepaliveSourcesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Set to true to have the API call wait until the source is up and healthy + WaitForHealthy bool `protobuf:"varint,1,opt,name=waitForHealthy,proto3" json:"waitForHealthy,omitempty"` + // Maximum time to wait for sources to reach a final state. Only used when + // waitForHealthy is true. If not specified, defaults to 4 minutes. + // After this timeout, the API will return the current state of all sources + // regardless of whether they have reached a final state. + Timeout *durationpb.Duration `protobuf:"bytes,2,opt,name=timeout,proto3" json:"timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KeepaliveSourcesRequest) Reset() { + *x = KeepaliveSourcesRequest{} + mi := &file_account_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KeepaliveSourcesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeepaliveSourcesRequest) ProtoMessage() {} + +func (x *KeepaliveSourcesRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[47] + 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 KeepaliveSourcesRequest.ProtoReflect.Descriptor instead. +func (*KeepaliveSourcesRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{47} +} + +func (x *KeepaliveSourcesRequest) GetWaitForHealthy() bool { + if x != nil { + return x.WaitForHealthy + } + return false +} + +func (x *KeepaliveSourcesRequest) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +type KeepaliveSourcesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // If the user requested to wait for the sources to be healthy, this will + // contain information about the sources that came up. If the user did not + // request to wait, this will be empty + Sources []*SourceKeepaliveResult `protobuf:"bytes,1,rep,name=sources,proto3" json:"sources,omitempty"` + // If all sources are healthy, this will be true. If any source is unhealthy, + // this will be false. If the user did not request to wait for sources to + // become healthy, this will be false. + AllSourcesHealthy bool `protobuf:"varint,2,opt,name=allSourcesHealthy,proto3" json:"allSourcesHealthy,omitempty"` + // If any source is healthy, this will be true. If all sources are unhealthy, + // this will be false. If the user did not request to wait for sources to + // become healthy, this will be false. + AnySourcesHealthy bool `protobuf:"varint,3,opt,name=anySourcesHealthy,proto3" json:"anySourcesHealthy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KeepaliveSourcesResponse) Reset() { + *x = KeepaliveSourcesResponse{} + mi := &file_account_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KeepaliveSourcesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeepaliveSourcesResponse) ProtoMessage() {} + +func (x *KeepaliveSourcesResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[48] + 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 KeepaliveSourcesResponse.ProtoReflect.Descriptor instead. +func (*KeepaliveSourcesResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{48} +} + +func (x *KeepaliveSourcesResponse) GetSources() []*SourceKeepaliveResult { + if x != nil { + return x.Sources + } + return nil +} + +func (x *KeepaliveSourcesResponse) GetAllSourcesHealthy() bool { + if x != nil { + return x.AllSourcesHealthy + } + return false +} + +func (x *KeepaliveSourcesResponse) GetAnySourcesHealthy() bool { + if x != nil { + return x.AnySourcesHealthy + } + return false +} + +type CreateTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The Public NKey of the user that is requesting a token + UserPublicNkey string `protobuf:"bytes,1,opt,name=userPublicNkey,proto3" json:"userPublicNkey,omitempty"` + // Friendly user name + UserName string `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTokenRequest) Reset() { + *x = CreateTokenRequest{} + mi := &file_account_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTokenRequest) ProtoMessage() {} + +func (x *CreateTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[49] + 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 CreateTokenRequest.ProtoReflect.Descriptor instead. +func (*CreateTokenRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{49} +} + +func (x *CreateTokenRequest) GetUserPublicNkey() string { + if x != nil { + return x.UserPublicNkey + } + return "" +} + +func (x *CreateTokenRequest) GetUserName() string { + if x != nil { + return x.UserName + } + return "" +} + +type CreateTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The JWT as a raw string + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTokenResponse) Reset() { + *x = CreateTokenResponse{} + mi := &file_account_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTokenResponse) ProtoMessage() {} + +func (x *CreateTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[50] + 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 CreateTokenResponse.ProtoReflect.Descriptor instead. +func (*CreateTokenResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{50} +} + +func (x *CreateTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type RevlinkWarmupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevlinkWarmupRequest) Reset() { + *x = RevlinkWarmupRequest{} + mi := &file_account_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevlinkWarmupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevlinkWarmupRequest) ProtoMessage() {} + +func (x *RevlinkWarmupRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[51] + 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 RevlinkWarmupRequest.ProtoReflect.Descriptor instead. +func (*RevlinkWarmupRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{51} +} + +type RevlinkWarmupResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + Items int32 `protobuf:"varint,2,opt,name=items,proto3" json:"items,omitempty"` + Edges int32 `protobuf:"varint,3,opt,name=edges,proto3" json:"edges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevlinkWarmupResponse) Reset() { + *x = RevlinkWarmupResponse{} + mi := &file_account_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevlinkWarmupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevlinkWarmupResponse) ProtoMessage() {} + +func (x *RevlinkWarmupResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[52] + 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 RevlinkWarmupResponse.ProtoReflect.Descriptor instead. +func (*RevlinkWarmupResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{52} +} + +func (x *RevlinkWarmupResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RevlinkWarmupResponse) GetItems() int32 { + if x != nil { + return x.Items + } + return 0 +} + +func (x *RevlinkWarmupResponse) GetEdges() int32 { + if x != nil { + return x.Edges + } + return 0 +} + +type AvailableItemType struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of item that this adapter returns e.g. eks-cluster + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // The category that these items fall under + Category AdapterCategory `protobuf:"varint,2,opt,name=category,proto3,enum=account.AdapterCategory" json:"category,omitempty"` + // A descriptive name of the types of items that are returned by this + // adapter e.g. "EKS Cluster" + DescriptiveName string `protobuf:"bytes,3,opt,name=descriptiveName,proto3" json:"descriptiveName,omitempty"` + // The supported query methods for this adapter + SupportedQueryMethods *AdapterSupportedQueryMethods `protobuf:"bytes,4,opt,name=supportedQueryMethods,proto3" json:"supportedQueryMethods,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AvailableItemType) Reset() { + *x = AvailableItemType{} + mi := &file_account_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AvailableItemType) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AvailableItemType) ProtoMessage() {} + +func (x *AvailableItemType) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[53] + 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 AvailableItemType.ProtoReflect.Descriptor instead. +func (*AvailableItemType) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{53} +} + +func (x *AvailableItemType) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *AvailableItemType) GetCategory() AdapterCategory { + if x != nil { + return x.Category + } + return AdapterCategory_ADAPTER_CATEGORY_OTHER +} + +func (x *AvailableItemType) GetDescriptiveName() string { + if x != nil { + return x.DescriptiveName + } + return "" +} + +func (x *AvailableItemType) GetSupportedQueryMethods() *AdapterSupportedQueryMethods { + if x != nil { + return x.SupportedQueryMethods + } + return nil +} + +type ListAvailableItemTypesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAvailableItemTypesRequest) Reset() { + *x = ListAvailableItemTypesRequest{} + mi := &file_account_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAvailableItemTypesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAvailableItemTypesRequest) ProtoMessage() {} + +func (x *ListAvailableItemTypesRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[54] + 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 ListAvailableItemTypesRequest.ProtoReflect.Descriptor instead. +func (*ListAvailableItemTypesRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{54} +} + +type ListAvailableItemTypesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Types []*AvailableItemType `protobuf:"bytes,1,rep,name=types,proto3" json:"types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAvailableItemTypesResponse) Reset() { + *x = ListAvailableItemTypesResponse{} + mi := &file_account_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAvailableItemTypesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAvailableItemTypesResponse) ProtoMessage() {} + +func (x *ListAvailableItemTypesResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[55] + 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 ListAvailableItemTypesResponse.ProtoReflect.Descriptor instead. +func (*ListAvailableItemTypesResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{55} +} + +func (x *ListAvailableItemTypesResponse) GetTypes() []*AvailableItemType { + if x != nil { + return x.Types + } + return nil +} + +type GetSourceStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UUID of the source to get status for + SourceUuid []byte `protobuf:"bytes,1,opt,name=source_uuid,json=sourceUuid,proto3" json:"source_uuid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSourceStatusRequest) Reset() { + *x = GetSourceStatusRequest{} + mi := &file_account_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSourceStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSourceStatusRequest) ProtoMessage() {} + +func (x *GetSourceStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[56] + 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 GetSourceStatusRequest.ProtoReflect.Descriptor instead. +func (*GetSourceStatusRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{56} +} + +func (x *GetSourceStatusRequest) GetSourceUuid() []byte { + if x != nil { + return x.SourceUuid + } + return nil +} + +type GetSourceStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source *SourceHealth `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSourceStatusResponse) Reset() { + *x = GetSourceStatusResponse{} + mi := &file_account_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSourceStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSourceStatusResponse) ProtoMessage() {} + +func (x *GetSourceStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[57] + 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 GetSourceStatusResponse.ProtoReflect.Descriptor instead. +func (*GetSourceStatusResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{57} +} + +func (x *GetSourceStatusResponse) GetSource() *SourceHealth { + if x != nil { + return x.Source + } + return nil +} + +type GetUserOnboardingStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserOnboardingStatusRequest) Reset() { + *x = GetUserOnboardingStatusRequest{} + mi := &file_account_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserOnboardingStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserOnboardingStatusRequest) ProtoMessage() {} + +func (x *GetUserOnboardingStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[58] + 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 GetUserOnboardingStatusRequest.ProtoReflect.Descriptor instead. +func (*GetUserOnboardingStatusRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{58} +} + +type GetUserOnboardingStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OnboardingComplete bool `protobuf:"varint,1,opt,name=onboarding_complete,json=onboardingComplete,proto3" json:"onboarding_complete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserOnboardingStatusResponse) Reset() { + *x = GetUserOnboardingStatusResponse{} + mi := &file_account_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserOnboardingStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserOnboardingStatusResponse) ProtoMessage() {} + +func (x *GetUserOnboardingStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[59] + 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 GetUserOnboardingStatusResponse.ProtoReflect.Descriptor instead. +func (*GetUserOnboardingStatusResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{59} +} + +func (x *GetUserOnboardingStatusResponse) GetOnboardingComplete() bool { + if x != nil { + return x.OnboardingComplete + } + return false +} + +type SetUserOnboardingStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OnboardingComplete bool `protobuf:"varint,1,opt,name=onboarding_complete,json=onboardingComplete,proto3" json:"onboarding_complete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetUserOnboardingStatusRequest) Reset() { + *x = SetUserOnboardingStatusRequest{} + mi := &file_account_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetUserOnboardingStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetUserOnboardingStatusRequest) ProtoMessage() {} + +func (x *SetUserOnboardingStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[60] + 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 SetUserOnboardingStatusRequest.ProtoReflect.Descriptor instead. +func (*SetUserOnboardingStatusRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{60} +} + +func (x *SetUserOnboardingStatusRequest) GetOnboardingComplete() bool { + if x != nil { + return x.OnboardingComplete + } + return false +} + +type SetUserOnboardingStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetUserOnboardingStatusResponse) Reset() { + *x = SetUserOnboardingStatusResponse{} + mi := &file_account_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetUserOnboardingStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetUserOnboardingStatusResponse) ProtoMessage() {} + +func (x *SetUserOnboardingStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[61] + 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 SetUserOnboardingStatusResponse.ProtoReflect.Descriptor instead. +func (*SetUserOnboardingStatusResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{61} +} + +type SetGithubInstallationIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GithubInstallationId int64 `protobuf:"varint,1,opt,name=github_installation_id,json=githubInstallationId,proto3" json:"github_installation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetGithubInstallationIDRequest) Reset() { + *x = SetGithubInstallationIDRequest{} + mi := &file_account_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetGithubInstallationIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetGithubInstallationIDRequest) ProtoMessage() {} + +func (x *SetGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[62] + 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 SetGithubInstallationIDRequest.ProtoReflect.Descriptor instead. +func (*SetGithubInstallationIDRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{62} +} + +func (x *SetGithubInstallationIDRequest) GetGithubInstallationId() int64 { + if x != nil { + return x.GithubInstallationId + } + return 0 +} + +type SetGithubInstallationIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetGithubInstallationIDResponse) Reset() { + *x = SetGithubInstallationIDResponse{} + mi := &file_account_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetGithubInstallationIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetGithubInstallationIDResponse) ProtoMessage() {} + +func (x *SetGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[63] + 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 SetGithubInstallationIDResponse.ProtoReflect.Descriptor instead. +func (*SetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{63} +} + +type UnsetGithubInstallationIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnsetGithubInstallationIDRequest) Reset() { + *x = UnsetGithubInstallationIDRequest{} + mi := &file_account_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetGithubInstallationIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetGithubInstallationIDRequest) ProtoMessage() {} + +func (x *UnsetGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[64] + 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 UnsetGithubInstallationIDRequest.ProtoReflect.Descriptor instead. +func (*UnsetGithubInstallationIDRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{64} +} + +type UnsetGithubInstallationIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnsetGithubInstallationIDResponse) Reset() { + *x = UnsetGithubInstallationIDResponse{} + mi := &file_account_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnsetGithubInstallationIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnsetGithubInstallationIDResponse) ProtoMessage() {} + +func (x *UnsetGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[65] + 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 UnsetGithubInstallationIDResponse.ProtoReflect.Descriptor instead. +func (*UnsetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{65} +} + +// Team member related messages +type ListTeamMembersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamMembersRequest) Reset() { + *x = ListTeamMembersRequest{} + mi := &file_account_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamMembersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamMembersRequest) ProtoMessage() {} + +func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTeamMembersRequest.ProtoReflect.Descriptor instead. +func (*ListTeamMembersRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{66} +} + +type ListTeamMembersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Members []*TeamMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamMembersResponse) Reset() { + *x = ListTeamMembersResponse{} + mi := &file_account_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamMembersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamMembersResponse) ProtoMessage() {} + +func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTeamMembersResponse.ProtoReflect.Descriptor instead. +func (*ListTeamMembersResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{67} +} + +func (x *ListTeamMembersResponse) GetMembers() []*TeamMember { + if x != nil { + return x.Members + } + return nil +} + +type TeamMember struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique identifier for the team member + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Team member's display name + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // URL to the team member's profile picture + PictureUrl string `protobuf:"bytes,3,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TeamMember) Reset() { + *x = TeamMember{} + mi := &file_account_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TeamMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TeamMember) ProtoMessage() {} + +func (x *TeamMember) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[68] + 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 TeamMember.ProtoReflect.Descriptor instead. +func (*TeamMember) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{68} +} + +func (x *TeamMember) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *TeamMember) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TeamMember) GetPictureUrl() string { + if x != nil { + return x.PictureUrl + } + return "" +} + +type GetWelcomeScreenInformationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWelcomeScreenInformationRequest) Reset() { + *x = GetWelcomeScreenInformationRequest{} + mi := &file_account_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWelcomeScreenInformationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWelcomeScreenInformationRequest) ProtoMessage() {} + +func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[69] + 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 GetWelcomeScreenInformationRequest.ProtoReflect.Descriptor instead. +func (*GetWelcomeScreenInformationRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{69} +} + +type InviterInformation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + PictureUrl string `protobuf:"bytes,3,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InviterInformation) Reset() { + *x = InviterInformation{} + mi := &file_account_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InviterInformation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InviterInformation) ProtoMessage() {} + +func (x *InviterInformation) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[70] + 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 InviterInformation.ProtoReflect.Descriptor instead. +func (*InviterInformation) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{70} +} + +func (x *InviterInformation) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *InviterInformation) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *InviterInformation) GetPictureUrl() string { + if x != nil { + return x.PictureUrl + } + return "" +} + +type GetWelcomeScreenInformationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + InviterInformation *InviterInformation `protobuf:"bytes,1,opt,name=inviter_information,json=inviterInformation,proto3" json:"inviter_information,omitempty"` // potentially we can return account / organisation information here + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWelcomeScreenInformationResponse) Reset() { + *x = GetWelcomeScreenInformationResponse{} + mi := &file_account_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWelcomeScreenInformationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWelcomeScreenInformationResponse) ProtoMessage() {} + +func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[71] + 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 GetWelcomeScreenInformationResponse.ProtoReflect.Descriptor instead. +func (*GetWelcomeScreenInformationResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{71} +} + +func (x *GetWelcomeScreenInformationResponse) GetInviterInformation() *InviterInformation { + if x != nil { + return x.InviterInformation + } + return nil +} + +var File_account_proto protoreflect.FileDescriptor + +const file_account_proto_rawDesc = "" + + "\n" + + "\raccount.proto\x12\aaccount\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vitems.proto\x1a\x1bbuf/validate/validate.proto\"\x15\n" + + "\x13ListAccountsRequest\"D\n" + + "\x14ListAccountsResponse\x12,\n" + + "\baccounts\x18\x01 \x03(\v2\x10.account.AccountR\baccounts\"R\n" + + "\x14CreateAccountRequest\x12:\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1a.account.AccountPropertiesR\n" + + "properties\"C\n" + + "\x15CreateAccountResponse\x12*\n" + + "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"R\n" + + "\x14UpdateAccountRequest\x12:\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1a.account.AccountPropertiesR\n" + + "properties\"C\n" + + "\x15UpdateAccountResponse\x12*\n" + + "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"h\n" + + "\x19AdminUpdateAccountRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x127\n" + + "\arequest\x18\x02 \x01(\v2\x1d.account.UpdateAccountRequestR\arequest\",\n" + + "\x16AdminGetAccountRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"/\n" + + "\x19AdminDeleteAccountRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x1c\n" + + "\x1aAdminDeleteAccountResponse\"j\n" + + "\x17AdminListSourcesRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x125\n" + + "\arequest\x18\x02 \x01(\v2\x1b.account.ListSourcesRequestR\arequest\"l\n" + + "\x18AdminCreateSourceRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + + "\arequest\x18\x02 \x01(\v2\x1c.account.CreateSourceRequestR\arequest\"f\n" + + "\x15AdminGetSourceRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x123\n" + + "\arequest\x18\x02 \x01(\v2\x19.account.GetSourceRequestR\arequest\"l\n" + + "\x18AdminUpdateSourceRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + + "\arequest\x18\x02 \x01(\v2\x1c.account.UpdateSourceRequestR\arequest\"l\n" + + "\x18AdminDeleteSourceRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x126\n" + + "\arequest\x18\x02 \x01(\v2\x1c.account.DeleteSourceRequestR\arequest\"t\n" + + "\x1cAdminKeepaliveSourcesRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x12:\n" + + "\arequest\x18\x02 \x01(\v2 .account.KeepaliveSourcesRequestR\arequest\"j\n" + + "\x17AdminCreateTokenRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x125\n" + + "\arequest\x18\x02 \x01(\v2\x1b.account.CreateTokenRequestR\arequest\"x\n" + + "\x06Source\x123\n" + + "\bmetadata\x18\x01 \x01(\v2\x17.account.SourceMetadataR\bmetadata\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.account.SourcePropertiesR\n" + + "properties\"\xe5\x01\n" + + "\x0eSourceMetadata\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x1c\n" + + "\tTokenName\x18\x02 \x01(\tR\tTokenName\x12<\n" + + "\vTokenExpiry\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\vTokenExpiry\x12\x1e\n" + + "\n" + + "PublicNkey\x18\x05 \x01(\tR\n" + + "PublicNkey\x12-\n" + + "\x06Status\x18\t \x01(\x0e2\x15.account.SourceStatusR\x06Status\x12\x14\n" + + "\x05Error\x18\n" + + " \x01(\tR\x05Error\"\xc6\x01\n" + + "\x10SourceProperties\x12(\n" + + "\x0fDescriptiveName\x18\x01 \x01(\tR\x0fDescriptiveName\x12\x12\n" + + "\x04Type\x18\x02 \x01(\tR\x04Type\x12/\n" + + "\x06Config\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x06Config\x12C\n" + + "\x10AdditionalConfig\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x10AdditionalConfig\"{\n" + + "\aAccount\x124\n" + + "\bmetadata\x18\x01 \x01(\v2\x18.account.AccountMetadataR\bmetadata\x12:\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1a.account.AccountPropertiesR\n" + + "properties\"\xfc\x01\n" + + "\x0fAccountMetadata\x12\x1e\n" + + "\n" + + "PublicNkey\x18\x02 \x01(\tR\n" + + "PublicNkey\x127\n" + + "\frepositories\x18\x03 \x03(\v2\x13.account.RepositoryR\frepositories\x12,\n" + + "\x11totalRepositories\x18\x04 \x01(\rR\x11totalRepositories\x12.\n" + + "\x12activeRepositories\x18\x05 \x01(\rR\x12activeRepositories\x122\n" + + "\x04Plan\x18\x06 \x01(\x0e2\x14.account.AccountPlanB\b\xbaH\x05\x82\x01\x02\x10\x01R\x04Plan\"\xbd\x01\n" + + "\n" + + "Repository\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" + + "\n" + + "numChanges\x18\x02 \x01(\x03R\n" + + "numChanges\x12>\n" + + "\flastChangeAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\flastChangeAt\x12;\n" + + "\x06status\x18\x04 \x01(\x0e2\x19.account.RepositoryStatusB\b\xbaH\x05\x82\x01\x02\x10\x01R\x06status\"S\n" + + "\x11AccountProperties\x12\x12\n" + + "\x04Name\x18\x01 \x01(\tR\x04Name\x12*\n" + + "\x10StripeCustomerID\x18\x02 \x01(\tR\x10StripeCustomerID\"\x13\n" + + "\x11GetAccountRequest\"@\n" + + "\x12GetAccountResponse\x12*\n" + + "\aaccount\x18\x01 \x01(\v2\x10.account.AccountR\aaccount\"0\n" + + "\x14DeleteAccountRequest\x12\x18\n" + + "\aiAmSure\x18\x01 \x01(\bR\aiAmSure\"\x17\n" + + "\x15DeleteAccountResponse\"\x14\n" + + "\x12ListSourcesRequest\"@\n" + + "\x13ListSourcesResponse\x12)\n" + + "\aSources\x18\x01 \x03(\v2\x0f.account.SourceR\aSources\"P\n" + + "\x13CreateSourceRequest\x129\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x19.account.SourcePropertiesR\n" + + "properties\"?\n" + + "\x14CreateSourceResponse\x12'\n" + + "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\"&\n" + + "\x10GetSourceRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"<\n" + + "\x11GetSourceResponse\x12'\n" + + "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\"d\n" + + "\x13UpdateSourceRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.account.SourcePropertiesR\n" + + "properties\"?\n" + + "\x14UpdateSourceResponse\x12'\n" + + "\x06source\x18\x01 \x01(\v2\x0f.account.SourceR\x06source\")\n" + + "\x13DeleteSourceRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x16\n" + + "\x14DeleteSourceResponse\"p\n" + + "\x15SourceKeepaliveResult\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12-\n" + + "\x06Status\x18\x02 \x01(\x0e2\x15.account.SourceStatusR\x06Status\x12\x14\n" + + "\x05Error\x18\x03 \x01(\tR\x05Error\"\x1d\n" + + "\x1bListAllSourcesStatusRequest\"\xbe\x04\n" + + "\fSourceHealth\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + + "\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01\x12-\n" + + "\x06status\x18\x05 \x01(\x0e2\x15.account.SourceStatusR\x06status\x128\n" + + "\tcreatedAt\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12@\n" + + "\rlastHeartbeat\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\rlastHeartbeat\x12@\n" + + "\rnextHeartbeat\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\rnextHeartbeat\x12\x12\n" + + "\x04type\x18\t \x01(\tR\x04type\x120\n" + + "\amanaged\x18\n" + + " \x01(\x0e2\x16.account.SourceManagedR\amanaged\x12&\n" + + "\x0eavailableTypes\x18\v \x03(\tR\x0eavailableTypes\x12(\n" + + "\x0favailableScopes\x18\f \x03(\tR\x0favailableScopes\x12B\n" + + "\x0fadapterMetadata\x18\r \x03(\v2\x18.account.AdapterMetadataR\x0fadapterMetadataB\b\n" + + "\x06_error\"O\n" + + "\x1cListAllSourcesStatusResponse\x12/\n" + + "\asources\x18\x01 \x03(\v2\x15.account.SourceHealthR\asources\"\x86\x03\n" + + "\x1cSubmitSourceHeartbeatRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + + "\x05error\x18\x04 \x01(\tH\x00R\x05error\x88\x01\x01\x12E\n" + + "\x10nextHeartbeatMax\x18\x05 \x01(\v2\x19.google.protobuf.DurationR\x10nextHeartbeatMax\x12\x12\n" + + "\x04type\x18\x06 \x01(\tR\x04type\x120\n" + + "\amanaged\x18\a \x01(\x0e2\x16.account.SourceManagedR\amanaged\x12(\n" + + "\x0favailableScopes\x18\t \x03(\tR\x0favailableScopes\x12B\n" + + "\x0fadapterMetadata\x18\n" + + " \x03(\v2\x18.account.AdapterMetadataR\x0fadapterMetadataB\b\n" + + "\x06_errorJ\x04\b\b\x10\t\"\x9d\x05\n" + + "\x0fAdapterMetadata\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12>\n" + + "\bcategory\x18\x02 \x01(\x0e2\x18.account.AdapterCategoryB\b\xbaH\x05\x82\x01\x02\x10\x01R\bcategory\x12\xc9\x01\n" + + "\x0epotentialLinks\x18\x03 \x03(\tB\xa0\x01\xbaH\x9c\x01\xba\x01\x98\x01\n" + + "\x18potentialLinksValidation\x12MIf 'potentialLinks' is not empty, none of its members should be empty strings\x1a-this.size() == 0 || this.all(x, x.size() > 0)R\x0epotentialLinks\x124\n" + + "\x0fdescriptiveName\x18\x04 \x01(\tB\n" + + "\xbaH\a\xc8\x01\x01r\x02\x10\x01R\x0fdescriptiveName\x12c\n" + + "\x15supportedQueryMethods\x18\x05 \x01(\v2%.account.AdapterSupportedQueryMethodsB\x06\xbaH\x03\xc8\x01\x01R\x15supportedQueryMethods\x12\xce\x01\n" + + "\x11terraformMappings\x18\x06 \x03(\v2\x19.account.TerraformMappingB\x84\x01\xbaH\x80\x01\xba\x01}\n" + + "\x1bterraformMappingsValidation\x12FIf 'terraformMappings' is not empty, none of its members should be nil\x1a\x16this.all(x, x != null)R\x11terraformMappings\"\xd7\x05\n" + + "\x1cAdapterSupportedQueryMethods\x12\x10\n" + + "\x03get\x18\x01 \x01(\bR\x03get\x12&\n" + + "\x0egetDescription\x18\x02 \x01(\tR\x0egetDescription\x12\x12\n" + + "\x04list\x18\x03 \x01(\bR\x04list\x12(\n" + + "\x0flistDescription\x18\x04 \x01(\tR\x0flistDescription\x12\x16\n" + + "\x06search\x18\x05 \x01(\bR\x06search\x12,\n" + + "\x11searchDescription\x18\x06 \x01(\tR\x11searchDescription:\xf8\x03\xbaH\xf4\x03\x1a\x9d\x01\n" + + "*AdapterSupportedQueryMethods.getValidation\x12BIf 'get' is true, 'getDescription' must have more than 1 character\x1a+!this.get || this.getDescription.size() > 1\x1a\xa2\x01\n" + + "+AdapterSupportedQueryMethods.listValidation\x12DIf 'list' is true, 'listDescription' must have more than 1 character\x1a-!this.list || this.listDescription.size() > 1\x1a\xac\x01\n" + + "-AdapterSupportedQueryMethods.searchValidation\x12HIf 'search' is true, 'searchDescription' must have more than 1 character\x1a1!this.search || this.searchDescription.size() > 1\"\xad\x02\n" + + "\x10TerraformMapping\x12@\n" + + "\x0fterraformMethod\x18\x01 \x01(\x0e2\f.QueryMethodB\b\xbaH\x05\x82\x01\x02\x10\x01R\x0fterraformMethod\x12\xd0\x01\n" + + "\x11terraformQueryMap\x18\x02 \x01(\tB\xa1\x01\xbaH\x9d\x01\xba\x01\x92\x01\n" + + "\x17terraformQueryMapFormat\x12ZThe value must be in the format '.' (dot notation with exactly two items)\x1a\x1bthis.split('.').size() == 2\xc8\x01\x01r\x02\x10\x03R\x11terraformQueryMapJ\x04\b\x03\x10\x04\"\x1f\n" + + "\x1dSubmitSourceHeartbeatResponse\"v\n" + + "\x17KeepaliveSourcesRequest\x12&\n" + + "\x0ewaitForHealthy\x18\x01 \x01(\bR\x0ewaitForHealthy\x123\n" + + "\atimeout\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xb0\x01\n" + + "\x18KeepaliveSourcesResponse\x128\n" + + "\asources\x18\x01 \x03(\v2\x1e.account.SourceKeepaliveResultR\asources\x12,\n" + + "\x11allSourcesHealthy\x18\x02 \x01(\bR\x11allSourcesHealthy\x12,\n" + + "\x11anySourcesHealthy\x18\x03 \x01(\bR\x11anySourcesHealthy\"X\n" + + "\x12CreateTokenRequest\x12&\n" + + "\x0euserPublicNkey\x18\x01 \x01(\tR\x0euserPublicNkey\x12\x1a\n" + + "\buserName\x18\x02 \x01(\tR\buserName\"+\n" + + "\x13CreateTokenResponse\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\x16\n" + + "\x14RevlinkWarmupRequest\"[\n" + + "\x15RevlinkWarmupResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + + "\x05items\x18\x02 \x01(\x05R\x05items\x12\x14\n" + + "\x05edges\x18\x03 \x01(\x05R\x05edges\"\xe4\x01\n" + + "\x11AvailableItemType\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x124\n" + + "\bcategory\x18\x02 \x01(\x0e2\x18.account.AdapterCategoryR\bcategory\x12(\n" + + "\x0fdescriptiveName\x18\x03 \x01(\tR\x0fdescriptiveName\x12[\n" + + "\x15supportedQueryMethods\x18\x04 \x01(\v2%.account.AdapterSupportedQueryMethodsR\x15supportedQueryMethods\"\x1f\n" + + "\x1dListAvailableItemTypesRequest\"R\n" + + "\x1eListAvailableItemTypesResponse\x120\n" + + "\x05types\x18\x01 \x03(\v2\x1a.account.AvailableItemTypeR\x05types\"9\n" + + "\x16GetSourceStatusRequest\x12\x1f\n" + + "\vsource_uuid\x18\x01 \x01(\fR\n" + + "sourceUuid\"H\n" + + "\x17GetSourceStatusResponse\x12-\n" + + "\x06source\x18\x01 \x01(\v2\x15.account.SourceHealthR\x06source\" \n" + + "\x1eGetUserOnboardingStatusRequest\"R\n" + + "\x1fGetUserOnboardingStatusResponse\x12/\n" + + "\x13onboarding_complete\x18\x01 \x01(\bR\x12onboardingComplete\"Q\n" + + "\x1eSetUserOnboardingStatusRequest\x12/\n" + + "\x13onboarding_complete\x18\x01 \x01(\bR\x12onboardingComplete\"!\n" + + "\x1fSetUserOnboardingStatusResponse\"V\n" + + "\x1eSetGithubInstallationIDRequest\x124\n" + + "\x16github_installation_id\x18\x01 \x01(\x03R\x14githubInstallationId\"!\n" + + "\x1fSetGithubInstallationIDResponse\"\"\n" + + " UnsetGithubInstallationIDRequest\"#\n" + + "!UnsetGithubInstallationIDResponse\"\x18\n" + + "\x16ListTeamMembersRequest\"H\n" + + "\x17ListTeamMembersResponse\x12-\n" + + "\amembers\x18\x01 \x03(\v2\x13.account.TeamMemberR\amembers\"U\n" + + "\n" + + "TeamMember\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + + "\vpicture_url\x18\x03 \x01(\tR\n" + + "pictureUrl\"$\n" + + "\"GetWelcomeScreenInformationRequest\"_\n" + + "\x12InviterInformation\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1f\n" + + "\vpicture_url\x18\x03 \x01(\tR\n" + + "pictureUrl\"s\n" + + "#GetWelcomeScreenInformationResponse\x12L\n" + + "\x13inviter_information\x18\x01 \x01(\v2\x1b.account.InviterInformationR\x12inviterInformation*\x96\x01\n" + + "\fSourceStatus\x12\x16\n" + + "\x12STATUS_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12STATUS_PROGRESSING\x10\x01\x12\x12\n" + + "\x0eSTATUS_HEALTHY\x10\x02\x12\x14\n" + + "\x10STATUS_UNHEALTHY\x10\x03\x12\x13\n" + + "\x0fSTATUS_SLEEPING\x10\x04\x12\x17\n" + + "\x13STATUS_DISCONNECTED\x10\x05*s\n" + + "\x10RepositoryStatus\x12!\n" + + "\x1dREPOSITORY_STATUS_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18REPOSITORY_STATUS_ACTIVE\x10\x01\x12\x1e\n" + + "\x1aREPOSITORY_STATUS_INACTIVE\x10\x02*_\n" + + "\vAccountPlan\x12\x1c\n" + + "\x18ACCOUNT_PLAN_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11ACCOUNT_PLAN_FREE\x10\x01\x12\x1b\n" + + "\x17ACCOUNT_PLAN_ENTERPRISE\x10\x02*'\n" + + "\rSourceManaged\x12\t\n" + + "\x05LOCAL\x10\x00\x12\v\n" + + "\aMANAGED\x10\x01*\xb2\x02\n" + + "\x0fAdapterCategory\x12\x1a\n" + + "\x16ADAPTER_CATEGORY_OTHER\x10\x00\x12(\n" + + "$ADAPTER_CATEGORY_COMPUTE_APPLICATION\x10\x01\x12\x1c\n" + + "\x18ADAPTER_CATEGORY_STORAGE\x10\x02\x12\x1c\n" + + "\x18ADAPTER_CATEGORY_NETWORK\x10\x03\x12\x1d\n" + + "\x19ADAPTER_CATEGORY_SECURITY\x10\x04\x12\"\n" + + "\x1eADAPTER_CATEGORY_OBSERVABILITY\x10\x05\x12\x1d\n" + + "\x19ADAPTER_CATEGORY_DATABASE\x10\x06\x12\"\n" + + "\x1eADAPTER_CATEGORY_CONFIGURATION\x10\a\x12\x17\n" + + "\x13ADAPTER_CATEGORY_AI\x10\b2\xe1\a\n" + + "\fAdminService\x12K\n" + + "\fListAccounts\x12\x1c.account.ListAccountsRequest\x1a\x1d.account.ListAccountsResponse\x12N\n" + + "\rCreateAccount\x12\x1d.account.CreateAccountRequest\x1a\x1e.account.CreateAccountResponse\x12S\n" + + "\rUpdateAccount\x12\".account.AdminUpdateAccountRequest\x1a\x1e.account.UpdateAccountResponse\x12J\n" + + "\n" + + "GetAccount\x12\x1f.account.AdminGetAccountRequest\x1a\x1b.account.GetAccountResponse\x12X\n" + + "\rDeleteAccount\x12\".account.AdminDeleteAccountRequest\x1a#.account.AdminDeleteAccountResponse\x12M\n" + + "\vListSources\x12 .account.AdminListSourcesRequest\x1a\x1c.account.ListSourcesResponse\x12P\n" + + "\fCreateSource\x12!.account.AdminCreateSourceRequest\x1a\x1d.account.CreateSourceResponse\x12G\n" + + "\tGetSource\x12\x1e.account.AdminGetSourceRequest\x1a\x1a.account.GetSourceResponse\x12P\n" + + "\fUpdateSource\x12!.account.AdminUpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12P\n" + + "\fDeleteSource\x12!.account.AdminDeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12\\\n" + + "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12M\n" + + "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x98\x0f\n" + + "\x11ManagementService\x12E\n" + + "\n" + + "GetAccount\x12\x1a.account.GetAccountRequest\x1a\x1b.account.GetAccountResponse\x12N\n" + + "\rDeleteAccount\x12\x1d.account.DeleteAccountRequest\x1a\x1e.account.DeleteAccountResponse\x12H\n" + + "\vListSources\x12\x1b.account.ListSourcesRequest\x1a\x1c.account.ListSourcesResponse\x12K\n" + + "\fCreateSource\x12\x1c.account.CreateSourceRequest\x1a\x1d.account.CreateSourceResponse\x12B\n" + + "\tGetSource\x12\x19.account.GetSourceRequest\x1a\x1a.account.GetSourceResponse\x12K\n" + + "\fUpdateSource\x12\x1c.account.UpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12K\n" + + "\fDeleteSource\x12\x1c.account.DeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12c\n" + + "\x14ListAllSourcesStatus\x12$.account.ListAllSourcesStatusRequest\x1a%.account.ListAllSourcesStatusResponse\x12f\n" + + "\x17ListActiveSourcesStatus\x12$.account.ListAllSourcesStatusRequest\x1a%.account.ListAllSourcesStatusResponse\x12f\n" + + "\x15SubmitSourceHeartbeat\x12%.account.SubmitSourceHeartbeatRequest\x1a&.account.SubmitSourceHeartbeatResponse\x12W\n" + + "\x10KeepaliveSources\x12 .account.KeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12H\n" + + "\vCreateToken\x12\x1b.account.CreateTokenRequest\x1a\x1c.account.CreateTokenResponse\x12P\n" + + "\rRevlinkWarmup\x12\x1d.account.RevlinkWarmupRequest\x1a\x1e.account.RevlinkWarmupResponse0\x01\x12i\n" + + "\x16ListAvailableItemTypes\x12&.account.ListAvailableItemTypesRequest\x1a'.account.ListAvailableItemTypesResponse\x12T\n" + + "\x0fGetSourceStatus\x12\x1f.account.GetSourceStatusRequest\x1a .account.GetSourceStatusResponse\x12l\n" + + "\x17GetUserOnboardingStatus\x12'.account.GetUserOnboardingStatusRequest\x1a(.account.GetUserOnboardingStatusResponse\x12l\n" + + "\x17SetUserOnboardingStatus\x12'.account.SetUserOnboardingStatusRequest\x1a(.account.SetUserOnboardingStatusResponse\x12T\n" + + "\x0fListTeamMembers\x12\x1f.account.ListTeamMembersRequest\x1a .account.ListTeamMembersResponse\x12x\n" + + "\x1bGetWelcomeScreenInformation\x12+.account.GetWelcomeScreenInformationRequest\x1a,.account.GetWelcomeScreenInformationResponse\x12l\n" + + "\x17SetGithubInstallationID\x12'.account.SetGithubInstallationIDRequest\x1a(.account.SetGithubInstallationIDResponse\x12r\n" + + "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_account_proto_rawDescOnce sync.Once + file_account_proto_rawDescData []byte +) + +func file_account_proto_rawDescGZIP() []byte { + file_account_proto_rawDescOnce.Do(func() { + file_account_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc))) + }) + return file_account_proto_rawDescData +} + +var file_account_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 72) +var file_account_proto_goTypes = []any{ + (SourceStatus)(0), // 0: account.SourceStatus + (RepositoryStatus)(0), // 1: account.RepositoryStatus + (AccountPlan)(0), // 2: account.AccountPlan + (SourceManaged)(0), // 3: account.SourceManaged + (AdapterCategory)(0), // 4: account.AdapterCategory + (*ListAccountsRequest)(nil), // 5: account.ListAccountsRequest + (*ListAccountsResponse)(nil), // 6: account.ListAccountsResponse + (*CreateAccountRequest)(nil), // 7: account.CreateAccountRequest + (*CreateAccountResponse)(nil), // 8: account.CreateAccountResponse + (*UpdateAccountRequest)(nil), // 9: account.UpdateAccountRequest + (*UpdateAccountResponse)(nil), // 10: account.UpdateAccountResponse + (*AdminUpdateAccountRequest)(nil), // 11: account.AdminUpdateAccountRequest + (*AdminGetAccountRequest)(nil), // 12: account.AdminGetAccountRequest + (*AdminDeleteAccountRequest)(nil), // 13: account.AdminDeleteAccountRequest + (*AdminDeleteAccountResponse)(nil), // 14: account.AdminDeleteAccountResponse + (*AdminListSourcesRequest)(nil), // 15: account.AdminListSourcesRequest + (*AdminCreateSourceRequest)(nil), // 16: account.AdminCreateSourceRequest + (*AdminGetSourceRequest)(nil), // 17: account.AdminGetSourceRequest + (*AdminUpdateSourceRequest)(nil), // 18: account.AdminUpdateSourceRequest + (*AdminDeleteSourceRequest)(nil), // 19: account.AdminDeleteSourceRequest + (*AdminKeepaliveSourcesRequest)(nil), // 20: account.AdminKeepaliveSourcesRequest + (*AdminCreateTokenRequest)(nil), // 21: account.AdminCreateTokenRequest + (*Source)(nil), // 22: account.Source + (*SourceMetadata)(nil), // 23: account.SourceMetadata + (*SourceProperties)(nil), // 24: account.SourceProperties + (*Account)(nil), // 25: account.Account + (*AccountMetadata)(nil), // 26: account.AccountMetadata + (*Repository)(nil), // 27: account.Repository + (*AccountProperties)(nil), // 28: account.AccountProperties + (*GetAccountRequest)(nil), // 29: account.GetAccountRequest + (*GetAccountResponse)(nil), // 30: account.GetAccountResponse + (*DeleteAccountRequest)(nil), // 31: account.DeleteAccountRequest + (*DeleteAccountResponse)(nil), // 32: account.DeleteAccountResponse + (*ListSourcesRequest)(nil), // 33: account.ListSourcesRequest + (*ListSourcesResponse)(nil), // 34: account.ListSourcesResponse + (*CreateSourceRequest)(nil), // 35: account.CreateSourceRequest + (*CreateSourceResponse)(nil), // 36: account.CreateSourceResponse + (*GetSourceRequest)(nil), // 37: account.GetSourceRequest + (*GetSourceResponse)(nil), // 38: account.GetSourceResponse + (*UpdateSourceRequest)(nil), // 39: account.UpdateSourceRequest + (*UpdateSourceResponse)(nil), // 40: account.UpdateSourceResponse + (*DeleteSourceRequest)(nil), // 41: account.DeleteSourceRequest + (*DeleteSourceResponse)(nil), // 42: account.DeleteSourceResponse + (*SourceKeepaliveResult)(nil), // 43: account.SourceKeepaliveResult + (*ListAllSourcesStatusRequest)(nil), // 44: account.ListAllSourcesStatusRequest + (*SourceHealth)(nil), // 45: account.SourceHealth + (*ListAllSourcesStatusResponse)(nil), // 46: account.ListAllSourcesStatusResponse + (*SubmitSourceHeartbeatRequest)(nil), // 47: account.SubmitSourceHeartbeatRequest + (*AdapterMetadata)(nil), // 48: account.AdapterMetadata + (*AdapterSupportedQueryMethods)(nil), // 49: account.AdapterSupportedQueryMethods + (*TerraformMapping)(nil), // 50: account.TerraformMapping + (*SubmitSourceHeartbeatResponse)(nil), // 51: account.SubmitSourceHeartbeatResponse + (*KeepaliveSourcesRequest)(nil), // 52: account.KeepaliveSourcesRequest + (*KeepaliveSourcesResponse)(nil), // 53: account.KeepaliveSourcesResponse + (*CreateTokenRequest)(nil), // 54: account.CreateTokenRequest + (*CreateTokenResponse)(nil), // 55: account.CreateTokenResponse + (*RevlinkWarmupRequest)(nil), // 56: account.RevlinkWarmupRequest + (*RevlinkWarmupResponse)(nil), // 57: account.RevlinkWarmupResponse + (*AvailableItemType)(nil), // 58: account.AvailableItemType + (*ListAvailableItemTypesRequest)(nil), // 59: account.ListAvailableItemTypesRequest + (*ListAvailableItemTypesResponse)(nil), // 60: account.ListAvailableItemTypesResponse + (*GetSourceStatusRequest)(nil), // 61: account.GetSourceStatusRequest + (*GetSourceStatusResponse)(nil), // 62: account.GetSourceStatusResponse + (*GetUserOnboardingStatusRequest)(nil), // 63: account.GetUserOnboardingStatusRequest + (*GetUserOnboardingStatusResponse)(nil), // 64: account.GetUserOnboardingStatusResponse + (*SetUserOnboardingStatusRequest)(nil), // 65: account.SetUserOnboardingStatusRequest + (*SetUserOnboardingStatusResponse)(nil), // 66: account.SetUserOnboardingStatusResponse + (*SetGithubInstallationIDRequest)(nil), // 67: account.SetGithubInstallationIDRequest + (*SetGithubInstallationIDResponse)(nil), // 68: account.SetGithubInstallationIDResponse + (*UnsetGithubInstallationIDRequest)(nil), // 69: account.UnsetGithubInstallationIDRequest + (*UnsetGithubInstallationIDResponse)(nil), // 70: account.UnsetGithubInstallationIDResponse + (*ListTeamMembersRequest)(nil), // 71: account.ListTeamMembersRequest + (*ListTeamMembersResponse)(nil), // 72: account.ListTeamMembersResponse + (*TeamMember)(nil), // 73: account.TeamMember + (*GetWelcomeScreenInformationRequest)(nil), // 74: account.GetWelcomeScreenInformationRequest + (*InviterInformation)(nil), // 75: account.InviterInformation + (*GetWelcomeScreenInformationResponse)(nil), // 76: account.GetWelcomeScreenInformationResponse + (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 78: google.protobuf.Struct + (*durationpb.Duration)(nil), // 79: google.protobuf.Duration + (QueryMethod)(0), // 80: QueryMethod +} +var file_account_proto_depIdxs = []int32{ + 25, // 0: account.ListAccountsResponse.accounts:type_name -> account.Account + 28, // 1: account.CreateAccountRequest.properties:type_name -> account.AccountProperties + 25, // 2: account.CreateAccountResponse.account:type_name -> account.Account + 28, // 3: account.UpdateAccountRequest.properties:type_name -> account.AccountProperties + 25, // 4: account.UpdateAccountResponse.account:type_name -> account.Account + 9, // 5: account.AdminUpdateAccountRequest.request:type_name -> account.UpdateAccountRequest + 33, // 6: account.AdminListSourcesRequest.request:type_name -> account.ListSourcesRequest + 35, // 7: account.AdminCreateSourceRequest.request:type_name -> account.CreateSourceRequest + 37, // 8: account.AdminGetSourceRequest.request:type_name -> account.GetSourceRequest + 39, // 9: account.AdminUpdateSourceRequest.request:type_name -> account.UpdateSourceRequest + 41, // 10: account.AdminDeleteSourceRequest.request:type_name -> account.DeleteSourceRequest + 52, // 11: account.AdminKeepaliveSourcesRequest.request:type_name -> account.KeepaliveSourcesRequest + 54, // 12: account.AdminCreateTokenRequest.request:type_name -> account.CreateTokenRequest + 23, // 13: account.Source.metadata:type_name -> account.SourceMetadata + 24, // 14: account.Source.properties:type_name -> account.SourceProperties + 77, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp + 0, // 16: account.SourceMetadata.Status:type_name -> account.SourceStatus + 78, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct + 78, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct + 26, // 19: account.Account.metadata:type_name -> account.AccountMetadata + 28, // 20: account.Account.properties:type_name -> account.AccountProperties + 27, // 21: account.AccountMetadata.repositories:type_name -> account.Repository + 2, // 22: account.AccountMetadata.Plan:type_name -> account.AccountPlan + 77, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp + 1, // 24: account.Repository.status:type_name -> account.RepositoryStatus + 25, // 25: account.GetAccountResponse.account:type_name -> account.Account + 22, // 26: account.ListSourcesResponse.Sources:type_name -> account.Source + 24, // 27: account.CreateSourceRequest.properties:type_name -> account.SourceProperties + 22, // 28: account.CreateSourceResponse.source:type_name -> account.Source + 22, // 29: account.GetSourceResponse.source:type_name -> account.Source + 24, // 30: account.UpdateSourceRequest.properties:type_name -> account.SourceProperties + 22, // 31: account.UpdateSourceResponse.source:type_name -> account.Source + 0, // 32: account.SourceKeepaliveResult.Status:type_name -> account.SourceStatus + 0, // 33: account.SourceHealth.status:type_name -> account.SourceStatus + 77, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp + 77, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp + 77, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp + 3, // 37: account.SourceHealth.managed:type_name -> account.SourceManaged + 48, // 38: account.SourceHealth.adapterMetadata:type_name -> account.AdapterMetadata + 45, // 39: account.ListAllSourcesStatusResponse.sources:type_name -> account.SourceHealth + 79, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration + 3, // 41: account.SubmitSourceHeartbeatRequest.managed:type_name -> account.SourceManaged + 48, // 42: account.SubmitSourceHeartbeatRequest.adapterMetadata:type_name -> account.AdapterMetadata + 4, // 43: account.AdapterMetadata.category:type_name -> account.AdapterCategory + 49, // 44: account.AdapterMetadata.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods + 50, // 45: account.AdapterMetadata.terraformMappings:type_name -> account.TerraformMapping + 80, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod + 79, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration + 43, // 48: account.KeepaliveSourcesResponse.sources:type_name -> account.SourceKeepaliveResult + 4, // 49: account.AvailableItemType.category:type_name -> account.AdapterCategory + 49, // 50: account.AvailableItemType.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods + 58, // 51: account.ListAvailableItemTypesResponse.types:type_name -> account.AvailableItemType + 45, // 52: account.GetSourceStatusResponse.source:type_name -> account.SourceHealth + 73, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember + 75, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation + 5, // 55: account.AdminService.ListAccounts:input_type -> account.ListAccountsRequest + 7, // 56: account.AdminService.CreateAccount:input_type -> account.CreateAccountRequest + 11, // 57: account.AdminService.UpdateAccount:input_type -> account.AdminUpdateAccountRequest + 12, // 58: account.AdminService.GetAccount:input_type -> account.AdminGetAccountRequest + 13, // 59: account.AdminService.DeleteAccount:input_type -> account.AdminDeleteAccountRequest + 15, // 60: account.AdminService.ListSources:input_type -> account.AdminListSourcesRequest + 16, // 61: account.AdminService.CreateSource:input_type -> account.AdminCreateSourceRequest + 17, // 62: account.AdminService.GetSource:input_type -> account.AdminGetSourceRequest + 18, // 63: account.AdminService.UpdateSource:input_type -> account.AdminUpdateSourceRequest + 19, // 64: account.AdminService.DeleteSource:input_type -> account.AdminDeleteSourceRequest + 20, // 65: account.AdminService.KeepaliveSources:input_type -> account.AdminKeepaliveSourcesRequest + 21, // 66: account.AdminService.CreateToken:input_type -> account.AdminCreateTokenRequest + 29, // 67: account.ManagementService.GetAccount:input_type -> account.GetAccountRequest + 31, // 68: account.ManagementService.DeleteAccount:input_type -> account.DeleteAccountRequest + 33, // 69: account.ManagementService.ListSources:input_type -> account.ListSourcesRequest + 35, // 70: account.ManagementService.CreateSource:input_type -> account.CreateSourceRequest + 37, // 71: account.ManagementService.GetSource:input_type -> account.GetSourceRequest + 39, // 72: account.ManagementService.UpdateSource:input_type -> account.UpdateSourceRequest + 41, // 73: account.ManagementService.DeleteSource:input_type -> account.DeleteSourceRequest + 44, // 74: account.ManagementService.ListAllSourcesStatus:input_type -> account.ListAllSourcesStatusRequest + 44, // 75: account.ManagementService.ListActiveSourcesStatus:input_type -> account.ListAllSourcesStatusRequest + 47, // 76: account.ManagementService.SubmitSourceHeartbeat:input_type -> account.SubmitSourceHeartbeatRequest + 52, // 77: account.ManagementService.KeepaliveSources:input_type -> account.KeepaliveSourcesRequest + 54, // 78: account.ManagementService.CreateToken:input_type -> account.CreateTokenRequest + 56, // 79: account.ManagementService.RevlinkWarmup:input_type -> account.RevlinkWarmupRequest + 59, // 80: account.ManagementService.ListAvailableItemTypes:input_type -> account.ListAvailableItemTypesRequest + 61, // 81: account.ManagementService.GetSourceStatus:input_type -> account.GetSourceStatusRequest + 63, // 82: account.ManagementService.GetUserOnboardingStatus:input_type -> account.GetUserOnboardingStatusRequest + 65, // 83: account.ManagementService.SetUserOnboardingStatus:input_type -> account.SetUserOnboardingStatusRequest + 71, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest + 74, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest + 67, // 86: account.ManagementService.SetGithubInstallationID:input_type -> account.SetGithubInstallationIDRequest + 69, // 87: account.ManagementService.UnsetGithubInstallationID:input_type -> account.UnsetGithubInstallationIDRequest + 6, // 88: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse + 8, // 89: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse + 10, // 90: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse + 30, // 91: account.AdminService.GetAccount:output_type -> account.GetAccountResponse + 14, // 92: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse + 34, // 93: account.AdminService.ListSources:output_type -> account.ListSourcesResponse + 36, // 94: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 95: account.AdminService.GetSource:output_type -> account.GetSourceResponse + 40, // 96: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 97: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse + 53, // 98: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 99: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse + 30, // 100: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse + 32, // 101: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse + 34, // 102: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse + 36, // 103: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 104: account.ManagementService.GetSource:output_type -> account.GetSourceResponse + 40, // 105: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 106: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse + 46, // 107: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 46, // 108: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 51, // 109: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse + 53, // 110: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 111: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse + 57, // 112: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse + 60, // 113: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse + 62, // 114: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse + 64, // 115: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse + 66, // 116: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse + 72, // 117: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse + 76, // 118: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse + 68, // 119: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse + 70, // 120: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse + 88, // [88:121] is the sub-list for method output_type + 55, // [55:88] is the sub-list for method input_type + 55, // [55:55] is the sub-list for extension type_name + 55, // [55:55] is the sub-list for extension extendee + 0, // [0:55] is the sub-list for field type_name +} + +func init() { file_account_proto_init() } +func file_account_proto_init() { + if File_account_proto != nil { + return + } + file_items_proto_init() + file_account_proto_msgTypes[40].OneofWrappers = []any{} + file_account_proto_msgTypes[42].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc)), + NumEnums: 5, + NumMessages: 72, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_account_proto_goTypes, + DependencyIndexes: file_account_proto_depIdxs, + EnumInfos: file_account_proto_enumTypes, + MessageInfos: file_account_proto_msgTypes, + }.Build() + File_account_proto = out.File + file_account_proto_goTypes = nil + file_account_proto_depIdxs = nil +} diff --git a/go/sdp-go/apikey.go b/go/sdp-go/apikey.go new file mode 100644 index 00000000..f445a9ea --- /dev/null +++ b/go/sdp-go/apikey.go @@ -0,0 +1,11 @@ +package sdp + +import "github.com/google/uuid" + +func (a *APIKeyMetadata) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUuid()) + if err != nil { + return nil + } + return &u +} diff --git a/go/sdp-go/apikeys.pb.go b/go/sdp-go/apikeys.pb.go new file mode 100644 index 00000000..8b03f283 --- /dev/null +++ b/go/sdp-go/apikeys.pb.go @@ -0,0 +1,1111 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: apikeys.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type KeyStatus int32 + +const ( + KeyStatus_KEY_STATUS_UNKNOWN KeyStatus = 0 + // This means the key has been created but we have not yet received the + // callback from Auth0 which allows us to fetch the access token + KeyStatus_KEY_STATUS_UNAUTHORIZED KeyStatus = 1 + // Key is ready for use + KeyStatus_KEY_STATUS_READY KeyStatus = 2 + // There was an error getting the access token from Auth0 + KeyStatus_KEY_STATUS_ERROR KeyStatus = 3 + // The API key has been revoked + KeyStatus_KEY_STATUS_REVOKED KeyStatus = 4 +) + +// Enum value maps for KeyStatus. +var ( + KeyStatus_name = map[int32]string{ + 0: "KEY_STATUS_UNKNOWN", + 1: "KEY_STATUS_UNAUTHORIZED", + 2: "KEY_STATUS_READY", + 3: "KEY_STATUS_ERROR", + 4: "KEY_STATUS_REVOKED", + } + KeyStatus_value = map[string]int32{ + "KEY_STATUS_UNKNOWN": 0, + "KEY_STATUS_UNAUTHORIZED": 1, + "KEY_STATUS_READY": 2, + "KEY_STATUS_ERROR": 3, + "KEY_STATUS_REVOKED": 4, + } +) + +func (x KeyStatus) Enum() *KeyStatus { + p := new(KeyStatus) + *p = x + return p +} + +func (x KeyStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (KeyStatus) Descriptor() protoreflect.EnumDescriptor { + return file_apikeys_proto_enumTypes[0].Descriptor() +} + +func (KeyStatus) Type() protoreflect.EnumType { + return &file_apikeys_proto_enumTypes[0] +} + +func (x KeyStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use KeyStatus.Descriptor instead. +func (KeyStatus) EnumDescriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{0} +} + +type APIKey struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *APIKeyMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *APIKeyProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIKey) Reset() { + *x = APIKey{} + mi := &file_apikeys_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIKey) ProtoMessage() {} + +func (x *APIKey) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[0] + 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 APIKey.ProtoReflect.Descriptor instead. +func (*APIKey) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{0} +} + +func (x *APIKey) GetMetadata() *APIKeyMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *APIKey) GetProperties() *APIKeyProperties { + if x != nil { + return x.Properties + } + return nil +} + +type APIKeyMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The ID of this API key + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + // When the API Key was created + Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` + // The last time the API key was exchanged for an access token + LastUsed *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=lastUsed,proto3" json:"lastUsed,omitempty"` + // The actual API key + Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` + // The list of scopes that this token has access to + Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` + // The status of the key + Status KeyStatus `protobuf:"varint,6,opt,name=status,proto3,enum=apikeys.KeyStatus" json:"status,omitempty"` + // The error encountered when authorizing the key. This will only be set if + // the status is ERROR + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIKeyMetadata) Reset() { + *x = APIKeyMetadata{} + mi := &file_apikeys_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIKeyMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIKeyMetadata) ProtoMessage() {} + +func (x *APIKeyMetadata) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[1] + 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 APIKeyMetadata.ProtoReflect.Descriptor instead. +func (*APIKeyMetadata) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{1} +} + +func (x *APIKeyMetadata) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +func (x *APIKeyMetadata) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + +func (x *APIKeyMetadata) GetLastUsed() *timestamppb.Timestamp { + if x != nil { + return x.LastUsed + } + return nil +} + +func (x *APIKeyMetadata) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *APIKeyMetadata) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *APIKeyMetadata) GetStatus() KeyStatus { + if x != nil { + return x.Status + } + return KeyStatus_KEY_STATUS_UNKNOWN +} + +func (x *APIKeyMetadata) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type APIKeyProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the API key + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIKeyProperties) Reset() { + *x = APIKeyProperties{} + mi := &file_apikeys_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIKeyProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIKeyProperties) ProtoMessage() {} + +func (x *APIKeyProperties) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[2] + 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 APIKeyProperties.ProtoReflect.Descriptor instead. +func (*APIKeyProperties) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{2} +} + +func (x *APIKeyProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CreateAPIKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the key to create + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The scopes that the key should have + Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` + // The URL that the user should be redirected to after the whole process is + // over. This should be a page in the frontend, probably the one they + // started from, but could also be a detail page for this particular API key + FinalFrontendRedirect string `protobuf:"bytes,3,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAPIKeyRequest) Reset() { + *x = CreateAPIKeyRequest{} + mi := &file_apikeys_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAPIKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAPIKeyRequest) ProtoMessage() {} + +func (x *CreateAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[3] + 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 CreateAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*CreateAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateAPIKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateAPIKeyRequest) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *CreateAPIKeyRequest) GetFinalFrontendRedirect() string { + if x != nil { + return x.FinalFrontendRedirect + } + return "" +} + +type CreateAPIKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Details of the newly created API Key + Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // The URL that the user should visit in order to authorize the newly + // created key. This will allow Auth0 to generate a code that will be passed + // to the API server via a callback. This code is then exchanged by the API + // server for an access token and refresh token. The user will be redirected + // back to the frontend once this process is complete. + // + // The authorizeURL will contain a `state` paremeter which is a UUID that + // can be used to look up the API key in the database once the callback is + // received + AuthorizeURL string `protobuf:"bytes,2,opt,name=authorizeURL,proto3" json:"authorizeURL,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAPIKeyResponse) Reset() { + *x = CreateAPIKeyResponse{} + mi := &file_apikeys_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAPIKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAPIKeyResponse) ProtoMessage() {} + +func (x *CreateAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[4] + 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 CreateAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*CreateAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateAPIKeyResponse) GetKey() *APIKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *CreateAPIKeyResponse) GetAuthorizeURL() string { + if x != nil { + return x.AuthorizeURL + } + return "" +} + +type RefreshAPIKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the API key to refresh + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + // The URL that the user should be redirected to after the whole process is + // over. This should be a page in the frontend, probably the one they + // started from, but could also be a detail page for this particular API key + FinalFrontendRedirect string `protobuf:"bytes,2,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshAPIKeyRequest) Reset() { + *x = RefreshAPIKeyRequest{} + mi := &file_apikeys_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshAPIKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshAPIKeyRequest) ProtoMessage() {} + +func (x *RefreshAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[5] + 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 RefreshAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*RefreshAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{5} +} + +func (x *RefreshAPIKeyRequest) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +func (x *RefreshAPIKeyRequest) GetFinalFrontendRedirect() string { + if x != nil { + return x.FinalFrontendRedirect + } + return "" +} + +type RefreshAPIKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Refreshing the API key will return the same response as CreateAPIKey, as + // it is basically the a new Key, just under the same UUID and reusing the + // old info. + Response *CreateAPIKeyResponse `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshAPIKeyResponse) Reset() { + *x = RefreshAPIKeyResponse{} + mi := &file_apikeys_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshAPIKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshAPIKeyResponse) ProtoMessage() {} + +func (x *RefreshAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[6] + 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 RefreshAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*RefreshAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{6} +} + +func (x *RefreshAPIKeyResponse) GetResponse() *CreateAPIKeyResponse { + if x != nil { + return x.Response + } + return nil +} + +type GetAPIKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the API Key to get + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAPIKeyRequest) Reset() { + *x = GetAPIKeyRequest{} + mi := &file_apikeys_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAPIKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAPIKeyRequest) ProtoMessage() {} + +func (x *GetAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[7] + 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 GetAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*GetAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{7} +} + +func (x *GetAPIKeyRequest) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +type GetAPIKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAPIKeyResponse) Reset() { + *x = GetAPIKeyResponse{} + mi := &file_apikeys_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAPIKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAPIKeyResponse) ProtoMessage() {} + +func (x *GetAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[8] + 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 GetAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*GetAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{8} +} + +func (x *GetAPIKeyResponse) GetKey() *APIKey { + if x != nil { + return x.Key + } + return nil +} + +type UpdateAPIKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the API key to update + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + // New properties to update + Properties *APIKeyProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAPIKeyRequest) Reset() { + *x = UpdateAPIKeyRequest{} + mi := &file_apikeys_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAPIKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAPIKeyRequest) ProtoMessage() {} + +func (x *UpdateAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[9] + 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 UpdateAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*UpdateAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateAPIKeyRequest) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +func (x *UpdateAPIKeyRequest) GetProperties() *APIKeyProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateAPIKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The updated API key + Key *APIKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAPIKeyResponse) Reset() { + *x = UpdateAPIKeyResponse{} + mi := &file_apikeys_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAPIKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAPIKeyResponse) ProtoMessage() {} + +func (x *UpdateAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[10] + 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 UpdateAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*UpdateAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateAPIKeyResponse) GetKey() *APIKey { + if x != nil { + return x.Key + } + return nil +} + +type ListAPIKeysRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAPIKeysRequest) Reset() { + *x = ListAPIKeysRequest{} + mi := &file_apikeys_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAPIKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAPIKeysRequest) ProtoMessage() {} + +func (x *ListAPIKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[11] + 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 ListAPIKeysRequest.ProtoReflect.Descriptor instead. +func (*ListAPIKeysRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{11} +} + +type ListAPIKeysResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Keys []*APIKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAPIKeysResponse) Reset() { + *x = ListAPIKeysResponse{} + mi := &file_apikeys_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAPIKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAPIKeysResponse) ProtoMessage() {} + +func (x *ListAPIKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[12] + 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 ListAPIKeysResponse.ProtoReflect.Descriptor instead. +func (*ListAPIKeysResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{12} +} + +func (x *ListAPIKeysResponse) GetKeys() []*APIKey { + if x != nil { + return x.Keys + } + return nil +} + +type DeleteAPIKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the API key to delete + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAPIKeyRequest) Reset() { + *x = DeleteAPIKeyRequest{} + mi := &file_apikeys_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAPIKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAPIKeyRequest) ProtoMessage() {} + +func (x *DeleteAPIKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAPIKeyRequest.ProtoReflect.Descriptor instead. +func (*DeleteAPIKeyRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{13} +} + +func (x *DeleteAPIKeyRequest) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +type DeleteAPIKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAPIKeyResponse) Reset() { + *x = DeleteAPIKeyResponse{} + mi := &file_apikeys_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAPIKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAPIKeyResponse) ProtoMessage() {} + +func (x *DeleteAPIKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAPIKeyResponse.ProtoReflect.Descriptor instead. +func (*DeleteAPIKeyResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{14} +} + +type ExchangeKeyForTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The API Key that you want to exchange for an Oauth access token + ApiKey string `protobuf:"bytes,1,opt,name=apiKey,proto3" json:"apiKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeKeyForTokenRequest) Reset() { + *x = ExchangeKeyForTokenRequest{} + mi := &file_apikeys_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeKeyForTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeKeyForTokenRequest) ProtoMessage() {} + +func (x *ExchangeKeyForTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[15] + 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 ExchangeKeyForTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeKeyForTokenRequest) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{15} +} + +func (x *ExchangeKeyForTokenRequest) GetApiKey() string { + if x != nil { + return x.ApiKey + } + return "" +} + +type ExchangeKeyForTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The access token that can now be use to authenticate to Overmind and its + // APIs + AccessToken string `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeKeyForTokenResponse) Reset() { + *x = ExchangeKeyForTokenResponse{} + mi := &file_apikeys_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeKeyForTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeKeyForTokenResponse) ProtoMessage() {} + +func (x *ExchangeKeyForTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_apikeys_proto_msgTypes[16] + 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 ExchangeKeyForTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeKeyForTokenResponse) Descriptor() ([]byte, []int) { + return file_apikeys_proto_rawDescGZIP(), []int{16} +} + +func (x *ExchangeKeyForTokenResponse) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +var File_apikeys_proto protoreflect.FileDescriptor + +const file_apikeys_proto_rawDesc = "" + + "\n" + + "\rapikeys.proto\x12\aapikeys\x1a\x1fgoogle/protobuf/timestamp.proto\"x\n" + + "\x06APIKey\x123\n" + + "\bmetadata\x18\x01 \x01(\v2\x17.apikeys.APIKeyMetadataR\bmetadata\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.apikeys.APIKeyPropertiesR\n" + + "properties\"\xfe\x01\n" + + "\x0eAPIKeyMetadata\x12\x12\n" + + "\x04uuid\x18\x01 \x01(\fR\x04uuid\x124\n" + + "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\x126\n" + + "\blastUsed\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\blastUsed\x12\x10\n" + + "\x03key\x18\x04 \x01(\tR\x03key\x12\x16\n" + + "\x06scopes\x18\x05 \x03(\tR\x06scopes\x12*\n" + + "\x06status\x18\x06 \x01(\x0e2\x12.apikeys.KeyStatusR\x06status\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\"&\n" + + "\x10APIKeyProperties\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"w\n" + + "\x13CreateAPIKeyRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06scopes\x18\x02 \x03(\tR\x06scopes\x124\n" + + "\x15finalFrontendRedirect\x18\x03 \x01(\tR\x15finalFrontendRedirect\"]\n" + + "\x14CreateAPIKeyResponse\x12!\n" + + "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\x12\"\n" + + "\fauthorizeURL\x18\x02 \x01(\tR\fauthorizeURL\"`\n" + + "\x14RefreshAPIKeyRequest\x12\x12\n" + + "\x04uuid\x18\x01 \x01(\fR\x04uuid\x124\n" + + "\x15finalFrontendRedirect\x18\x02 \x01(\tR\x15finalFrontendRedirect\"R\n" + + "\x15RefreshAPIKeyResponse\x129\n" + + "\bresponse\x18\x01 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\bresponse\"&\n" + + "\x10GetAPIKeyRequest\x12\x12\n" + + "\x04uuid\x18\x01 \x01(\fR\x04uuid\"6\n" + + "\x11GetAPIKeyResponse\x12!\n" + + "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\"d\n" + + "\x13UpdateAPIKeyRequest\x12\x12\n" + + "\x04uuid\x18\x01 \x01(\fR\x04uuid\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.apikeys.APIKeyPropertiesR\n" + + "properties\"9\n" + + "\x14UpdateAPIKeyResponse\x12!\n" + + "\x03key\x18\x01 \x01(\v2\x0f.apikeys.APIKeyR\x03key\"\x14\n" + + "\x12ListAPIKeysRequest\":\n" + + "\x13ListAPIKeysResponse\x12#\n" + + "\x04keys\x18\x01 \x03(\v2\x0f.apikeys.APIKeyR\x04keys\")\n" + + "\x13DeleteAPIKeyRequest\x12\x12\n" + + "\x04uuid\x18\x01 \x01(\fR\x04uuid\"\x16\n" + + "\x14DeleteAPIKeyResponse\"4\n" + + "\x1aExchangeKeyForTokenRequest\x12\x16\n" + + "\x06apiKey\x18\x01 \x01(\tR\x06apiKey\"?\n" + + "\x1bExchangeKeyForTokenResponse\x12 \n" + + "\vaccessToken\x18\x01 \x01(\tR\vaccessToken*\x84\x01\n" + + "\tKeyStatus\x12\x16\n" + + "\x12KEY_STATUS_UNKNOWN\x10\x00\x12\x1b\n" + + "\x17KEY_STATUS_UNAUTHORIZED\x10\x01\x12\x14\n" + + "\x10KEY_STATUS_READY\x10\x02\x12\x14\n" + + "\x10KEY_STATUS_ERROR\x10\x03\x12\x16\n" + + "\x12KEY_STATUS_REVOKED\x10\x042\xb6\x04\n" + + "\rApiKeyService\x12K\n" + + "\fCreateAPIKey\x12\x1c.apikeys.CreateAPIKeyRequest\x1a\x1d.apikeys.CreateAPIKeyResponse\x12N\n" + + "\rRefreshAPIKey\x12\x1d.apikeys.RefreshAPIKeyRequest\x1a\x1e.apikeys.RefreshAPIKeyResponse\x12B\n" + + "\tGetAPIKey\x12\x19.apikeys.GetAPIKeyRequest\x1a\x1a.apikeys.GetAPIKeyResponse\x12K\n" + + "\fUpdateAPIKey\x12\x1c.apikeys.UpdateAPIKeyRequest\x1a\x1d.apikeys.UpdateAPIKeyResponse\x12H\n" + + "\vListAPIKeys\x12\x1b.apikeys.ListAPIKeysRequest\x1a\x1c.apikeys.ListAPIKeysResponse\x12K\n" + + "\fDeleteAPIKey\x12\x1c.apikeys.DeleteAPIKeyRequest\x1a\x1d.apikeys.DeleteAPIKeyResponse\x12`\n" + + "\x13ExchangeKeyForToken\x12#.apikeys.ExchangeKeyForTokenRequest\x1a$.apikeys.ExchangeKeyForTokenResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_apikeys_proto_rawDescOnce sync.Once + file_apikeys_proto_rawDescData []byte +) + +func file_apikeys_proto_rawDescGZIP() []byte { + file_apikeys_proto_rawDescOnce.Do(func() { + file_apikeys_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_apikeys_proto_rawDesc), len(file_apikeys_proto_rawDesc))) + }) + return file_apikeys_proto_rawDescData +} + +var file_apikeys_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_apikeys_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_apikeys_proto_goTypes = []any{ + (KeyStatus)(0), // 0: apikeys.KeyStatus + (*APIKey)(nil), // 1: apikeys.APIKey + (*APIKeyMetadata)(nil), // 2: apikeys.APIKeyMetadata + (*APIKeyProperties)(nil), // 3: apikeys.APIKeyProperties + (*CreateAPIKeyRequest)(nil), // 4: apikeys.CreateAPIKeyRequest + (*CreateAPIKeyResponse)(nil), // 5: apikeys.CreateAPIKeyResponse + (*RefreshAPIKeyRequest)(nil), // 6: apikeys.RefreshAPIKeyRequest + (*RefreshAPIKeyResponse)(nil), // 7: apikeys.RefreshAPIKeyResponse + (*GetAPIKeyRequest)(nil), // 8: apikeys.GetAPIKeyRequest + (*GetAPIKeyResponse)(nil), // 9: apikeys.GetAPIKeyResponse + (*UpdateAPIKeyRequest)(nil), // 10: apikeys.UpdateAPIKeyRequest + (*UpdateAPIKeyResponse)(nil), // 11: apikeys.UpdateAPIKeyResponse + (*ListAPIKeysRequest)(nil), // 12: apikeys.ListAPIKeysRequest + (*ListAPIKeysResponse)(nil), // 13: apikeys.ListAPIKeysResponse + (*DeleteAPIKeyRequest)(nil), // 14: apikeys.DeleteAPIKeyRequest + (*DeleteAPIKeyResponse)(nil), // 15: apikeys.DeleteAPIKeyResponse + (*ExchangeKeyForTokenRequest)(nil), // 16: apikeys.ExchangeKeyForTokenRequest + (*ExchangeKeyForTokenResponse)(nil), // 17: apikeys.ExchangeKeyForTokenResponse + (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp +} +var file_apikeys_proto_depIdxs = []int32{ + 2, // 0: apikeys.APIKey.metadata:type_name -> apikeys.APIKeyMetadata + 3, // 1: apikeys.APIKey.properties:type_name -> apikeys.APIKeyProperties + 18, // 2: apikeys.APIKeyMetadata.created:type_name -> google.protobuf.Timestamp + 18, // 3: apikeys.APIKeyMetadata.lastUsed:type_name -> google.protobuf.Timestamp + 0, // 4: apikeys.APIKeyMetadata.status:type_name -> apikeys.KeyStatus + 1, // 5: apikeys.CreateAPIKeyResponse.key:type_name -> apikeys.APIKey + 5, // 6: apikeys.RefreshAPIKeyResponse.response:type_name -> apikeys.CreateAPIKeyResponse + 1, // 7: apikeys.GetAPIKeyResponse.key:type_name -> apikeys.APIKey + 3, // 8: apikeys.UpdateAPIKeyRequest.properties:type_name -> apikeys.APIKeyProperties + 1, // 9: apikeys.UpdateAPIKeyResponse.key:type_name -> apikeys.APIKey + 1, // 10: apikeys.ListAPIKeysResponse.keys:type_name -> apikeys.APIKey + 4, // 11: apikeys.ApiKeyService.CreateAPIKey:input_type -> apikeys.CreateAPIKeyRequest + 6, // 12: apikeys.ApiKeyService.RefreshAPIKey:input_type -> apikeys.RefreshAPIKeyRequest + 8, // 13: apikeys.ApiKeyService.GetAPIKey:input_type -> apikeys.GetAPIKeyRequest + 10, // 14: apikeys.ApiKeyService.UpdateAPIKey:input_type -> apikeys.UpdateAPIKeyRequest + 12, // 15: apikeys.ApiKeyService.ListAPIKeys:input_type -> apikeys.ListAPIKeysRequest + 14, // 16: apikeys.ApiKeyService.DeleteAPIKey:input_type -> apikeys.DeleteAPIKeyRequest + 16, // 17: apikeys.ApiKeyService.ExchangeKeyForToken:input_type -> apikeys.ExchangeKeyForTokenRequest + 5, // 18: apikeys.ApiKeyService.CreateAPIKey:output_type -> apikeys.CreateAPIKeyResponse + 7, // 19: apikeys.ApiKeyService.RefreshAPIKey:output_type -> apikeys.RefreshAPIKeyResponse + 9, // 20: apikeys.ApiKeyService.GetAPIKey:output_type -> apikeys.GetAPIKeyResponse + 11, // 21: apikeys.ApiKeyService.UpdateAPIKey:output_type -> apikeys.UpdateAPIKeyResponse + 13, // 22: apikeys.ApiKeyService.ListAPIKeys:output_type -> apikeys.ListAPIKeysResponse + 15, // 23: apikeys.ApiKeyService.DeleteAPIKey:output_type -> apikeys.DeleteAPIKeyResponse + 17, // 24: apikeys.ApiKeyService.ExchangeKeyForToken:output_type -> apikeys.ExchangeKeyForTokenResponse + 18, // [18:25] is the sub-list for method output_type + 11, // [11:18] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_apikeys_proto_init() } +func file_apikeys_proto_init() { + if File_apikeys_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_apikeys_proto_rawDesc), len(file_apikeys_proto_rawDesc)), + NumEnums: 1, + NumMessages: 17, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_apikeys_proto_goTypes, + DependencyIndexes: file_apikeys_proto_depIdxs, + EnumInfos: file_apikeys_proto_enumTypes, + MessageInfos: file_apikeys_proto_msgTypes, + }.Build() + File_apikeys_proto = out.File + file_apikeys_proto_goTypes = nil + file_apikeys_proto_depIdxs = nil +} diff --git a/go/sdp-go/area51.pb.go b/go/sdp-go/area51.pb.go new file mode 100644 index 00000000..df1d3c5c --- /dev/null +++ b/go/sdp-go/area51.pb.go @@ -0,0 +1,336 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: area51.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ChangeArchive struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change *Change `protobuf:"bytes,1,opt,name=Change,proto3" json:"Change,omitempty"` + ChangingItemsBookmark *Bookmark `protobuf:"bytes,2,opt,name=changingItemsBookmark,proto3,oneof" json:"changingItemsBookmark,omitempty"` + BlastRadiusSnapshot *Snapshot `protobuf:"bytes,3,opt,name=blastRadiusSnapshot,proto3,oneof" json:"blastRadiusSnapshot,omitempty"` + SystemBeforeSnapshot *Snapshot `protobuf:"bytes,4,opt,name=systemBeforeSnapshot,proto3,oneof" json:"systemBeforeSnapshot,omitempty"` + SystemAfterSnapshot *Snapshot `protobuf:"bytes,5,opt,name=systemAfterSnapshot,proto3,oneof" json:"systemAfterSnapshot,omitempty"` + ChangeRiskMetadata *ChangeRiskMetadata `protobuf:"bytes,6,opt,name=changeRiskMetadata,proto3" json:"changeRiskMetadata,omitempty"` + PlannedChanges []*MappedItemDiff `protobuf:"bytes,7,rep,name=plannedChanges,proto3" json:"plannedChanges,omitempty"` + TimelineV2 []*ChangeTimelineEntryV2 `protobuf:"bytes,8,rep,name=timelineV2,proto3" json:"timelineV2,omitempty"` + Signals []*Signal `protobuf:"bytes,9,rep,name=signals,proto3" json:"signals,omitempty"` + Hypotheses []*HypothesesDetails `protobuf:"bytes,10,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeArchive) Reset() { + *x = ChangeArchive{} + mi := &file_area51_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeArchive) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeArchive) ProtoMessage() {} + +func (x *ChangeArchive) ProtoReflect() protoreflect.Message { + mi := &file_area51_proto_msgTypes[0] + 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 ChangeArchive.ProtoReflect.Descriptor instead. +func (*ChangeArchive) Descriptor() ([]byte, []int) { + return file_area51_proto_rawDescGZIP(), []int{0} +} + +func (x *ChangeArchive) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +func (x *ChangeArchive) GetChangingItemsBookmark() *Bookmark { + if x != nil { + return x.ChangingItemsBookmark + } + return nil +} + +func (x *ChangeArchive) GetBlastRadiusSnapshot() *Snapshot { + if x != nil { + return x.BlastRadiusSnapshot + } + return nil +} + +func (x *ChangeArchive) GetSystemBeforeSnapshot() *Snapshot { + if x != nil { + return x.SystemBeforeSnapshot + } + return nil +} + +func (x *ChangeArchive) GetSystemAfterSnapshot() *Snapshot { + if x != nil { + return x.SystemAfterSnapshot + } + return nil +} + +func (x *ChangeArchive) GetChangeRiskMetadata() *ChangeRiskMetadata { + if x != nil { + return x.ChangeRiskMetadata + } + return nil +} + +func (x *ChangeArchive) GetPlannedChanges() []*MappedItemDiff { + if x != nil { + return x.PlannedChanges + } + return nil +} + +func (x *ChangeArchive) GetTimelineV2() []*ChangeTimelineEntryV2 { + if x != nil { + return x.TimelineV2 + } + return nil +} + +func (x *ChangeArchive) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +func (x *ChangeArchive) GetHypotheses() []*HypothesesDetails { + if x != nil { + return x.Hypotheses + } + return nil +} + +type GetChangeArchiveRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeArchiveRequest) Reset() { + *x = GetChangeArchiveRequest{} + mi := &file_area51_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeArchiveRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeArchiveRequest) ProtoMessage() {} + +func (x *GetChangeArchiveRequest) ProtoReflect() protoreflect.Message { + mi := &file_area51_proto_msgTypes[1] + 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 GetChangeArchiveRequest.ProtoReflect.Descriptor instead. +func (*GetChangeArchiveRequest) Descriptor() ([]byte, []int) { + return file_area51_proto_rawDescGZIP(), []int{1} +} + +func (x *GetChangeArchiveRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type GetChangeArchiveResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeArchive *ChangeArchive `protobuf:"bytes,1,opt,name=changeArchive,proto3" json:"changeArchive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeArchiveResponse) Reset() { + *x = GetChangeArchiveResponse{} + mi := &file_area51_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeArchiveResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeArchiveResponse) ProtoMessage() {} + +func (x *GetChangeArchiveResponse) ProtoReflect() protoreflect.Message { + mi := &file_area51_proto_msgTypes[2] + 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 GetChangeArchiveResponse.ProtoReflect.Descriptor instead. +func (*GetChangeArchiveResponse) Descriptor() ([]byte, []int) { + return file_area51_proto_rawDescGZIP(), []int{2} +} + +func (x *GetChangeArchiveResponse) GetChangeArchive() *ChangeArchive { + if x != nil { + return x.ChangeArchive + } + return nil +} + +var File_area51_proto protoreflect.FileDescriptor + +const file_area51_proto_rawDesc = "" + + "\n" + + "\farea51.proto\x12\x06area51\x1a\x0fbookmarks.proto\x1a\rchanges.proto\x1a\fsignal.proto\x1a\x0fsnapshots.proto\x1a\n" + + "util.proto\"\x85\x06\n" + + "\rChangeArchive\x12'\n" + + "\x06Change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06Change\x12N\n" + + "\x15changingItemsBookmark\x18\x02 \x01(\v2\x13.bookmarks.BookmarkH\x00R\x15changingItemsBookmark\x88\x01\x01\x12J\n" + + "\x13blastRadiusSnapshot\x18\x03 \x01(\v2\x13.snapshots.SnapshotH\x01R\x13blastRadiusSnapshot\x88\x01\x01\x12L\n" + + "\x14systemBeforeSnapshot\x18\x04 \x01(\v2\x13.snapshots.SnapshotH\x02R\x14systemBeforeSnapshot\x88\x01\x01\x12J\n" + + "\x13systemAfterSnapshot\x18\x05 \x01(\v2\x13.snapshots.SnapshotH\x03R\x13systemAfterSnapshot\x88\x01\x01\x12K\n" + + "\x12changeRiskMetadata\x18\x06 \x01(\v2\x1b.changes.ChangeRiskMetadataR\x12changeRiskMetadata\x12?\n" + + "\x0eplannedChanges\x18\a \x03(\v2\x17.changes.MappedItemDiffR\x0eplannedChanges\x12>\n" + + "\n" + + "timelineV2\x18\b \x03(\v2\x1e.changes.ChangeTimelineEntryV2R\n" + + "timelineV2\x12(\n" + + "\asignals\x18\t \x03(\v2\x0e.signal.SignalR\asignals\x12:\n" + + "\n" + + "hypotheses\x18\n" + + " \x03(\v2\x1a.changes.HypothesesDetailsR\n" + + "hypothesesB\x18\n" + + "\x16_changingItemsBookmarkB\x16\n" + + "\x14_blastRadiusSnapshotB\x17\n" + + "\x15_systemBeforeSnapshotB\x16\n" + + "\x14_systemAfterSnapshot\"-\n" + + "\x17GetChangeArchiveRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"W\n" + + "\x18GetChangeArchiveResponse\x12;\n" + + "\rchangeArchive\x18\x01 \x01(\v2\x15.area51.ChangeArchiveR\rchangeArchive2f\n" + + "\rArea51Service\x12U\n" + + "\x10GetChangeArchive\x12\x1f.area51.GetChangeArchiveRequest\x1a .area51.GetChangeArchiveResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_area51_proto_rawDescOnce sync.Once + file_area51_proto_rawDescData []byte +) + +func file_area51_proto_rawDescGZIP() []byte { + file_area51_proto_rawDescOnce.Do(func() { + file_area51_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_area51_proto_rawDesc), len(file_area51_proto_rawDesc))) + }) + return file_area51_proto_rawDescData +} + +var file_area51_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_area51_proto_goTypes = []any{ + (*ChangeArchive)(nil), // 0: area51.ChangeArchive + (*GetChangeArchiveRequest)(nil), // 1: area51.GetChangeArchiveRequest + (*GetChangeArchiveResponse)(nil), // 2: area51.GetChangeArchiveResponse + (*Change)(nil), // 3: changes.Change + (*Bookmark)(nil), // 4: bookmarks.Bookmark + (*Snapshot)(nil), // 5: snapshots.Snapshot + (*ChangeRiskMetadata)(nil), // 6: changes.ChangeRiskMetadata + (*MappedItemDiff)(nil), // 7: changes.MappedItemDiff + (*ChangeTimelineEntryV2)(nil), // 8: changes.ChangeTimelineEntryV2 + (*Signal)(nil), // 9: signal.Signal + (*HypothesesDetails)(nil), // 10: changes.HypothesesDetails +} +var file_area51_proto_depIdxs = []int32{ + 3, // 0: area51.ChangeArchive.Change:type_name -> changes.Change + 4, // 1: area51.ChangeArchive.changingItemsBookmark:type_name -> bookmarks.Bookmark + 5, // 2: area51.ChangeArchive.blastRadiusSnapshot:type_name -> snapshots.Snapshot + 5, // 3: area51.ChangeArchive.systemBeforeSnapshot:type_name -> snapshots.Snapshot + 5, // 4: area51.ChangeArchive.systemAfterSnapshot:type_name -> snapshots.Snapshot + 6, // 5: area51.ChangeArchive.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 7, // 6: area51.ChangeArchive.plannedChanges:type_name -> changes.MappedItemDiff + 8, // 7: area51.ChangeArchive.timelineV2:type_name -> changes.ChangeTimelineEntryV2 + 9, // 8: area51.ChangeArchive.signals:type_name -> signal.Signal + 10, // 9: area51.ChangeArchive.hypotheses:type_name -> changes.HypothesesDetails + 0, // 10: area51.GetChangeArchiveResponse.changeArchive:type_name -> area51.ChangeArchive + 1, // 11: area51.Area51Service.GetChangeArchive:input_type -> area51.GetChangeArchiveRequest + 2, // 12: area51.Area51Service.GetChangeArchive:output_type -> area51.GetChangeArchiveResponse + 12, // [12:13] is the sub-list for method output_type + 11, // [11:12] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_area51_proto_init() } +func file_area51_proto_init() { + if File_area51_proto != nil { + return + } + file_bookmarks_proto_init() + file_changes_proto_init() + file_signal_proto_init() + file_snapshots_proto_init() + file_util_proto_init() + file_area51_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_area51_proto_rawDesc), len(file_area51_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_area51_proto_goTypes, + DependencyIndexes: file_area51_proto_depIdxs, + MessageInfos: file_area51_proto_msgTypes, + }.Build() + File_area51_proto = out.File + file_area51_proto_goTypes = nil + file_area51_proto_depIdxs = nil +} diff --git a/go/sdp-go/auth0support.pb.go b/go/sdp-go/auth0support.pb.go new file mode 100644 index 00000000..8916ed84 --- /dev/null +++ b/go/sdp-go/auth0support.pb.go @@ -0,0 +1,258 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: auth0support.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/structpb" + _ "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Auth0CreateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The Auth0 User ID + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + // The user's email address + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + // The user's full name. This will be split and stored as first_name and + // last_name internally. It is provided for convenience since some social + // providers do not provide first_name and last_name fields. If `first_name` + // and `last_name` are provided, this field will be ignored. + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Whether the user's email address has been verified + EmailVerified bool `protobuf:"varint,4,opt,name=email_verified,json=emailVerified,proto3" json:"email_verified,omitempty"` + // The user's first name + FirstName string `protobuf:"bytes,5,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` + // The user's last name + LastName string `protobuf:"bytes,6,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + // The user's connection id + ConnectionId string `protobuf:"bytes,7,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + // The user's profile picture URL + PictureUrl string `protobuf:"bytes,8,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Auth0CreateUserRequest) Reset() { + *x = Auth0CreateUserRequest{} + mi := &file_auth0support_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Auth0CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Auth0CreateUserRequest) ProtoMessage() {} + +func (x *Auth0CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_auth0support_proto_msgTypes[0] + 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 Auth0CreateUserRequest.ProtoReflect.Descriptor instead. +func (*Auth0CreateUserRequest) Descriptor() ([]byte, []int) { + return file_auth0support_proto_rawDescGZIP(), []int{0} +} + +func (x *Auth0CreateUserRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *Auth0CreateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *Auth0CreateUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Auth0CreateUserRequest) GetEmailVerified() bool { + if x != nil { + return x.EmailVerified + } + return false +} + +func (x *Auth0CreateUserRequest) GetFirstName() string { + if x != nil { + return x.FirstName + } + return "" +} + +func (x *Auth0CreateUserRequest) GetLastName() string { + if x != nil { + return x.LastName + } + return "" +} + +func (x *Auth0CreateUserRequest) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +func (x *Auth0CreateUserRequest) GetPictureUrl() string { + if x != nil { + return x.PictureUrl + } + return "" +} + +type Auth0CreateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Auth0CreateUserResponse) Reset() { + *x = Auth0CreateUserResponse{} + mi := &file_auth0support_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Auth0CreateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Auth0CreateUserResponse) ProtoMessage() {} + +func (x *Auth0CreateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_auth0support_proto_msgTypes[1] + 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 Auth0CreateUserResponse.ProtoReflect.Descriptor instead. +func (*Auth0CreateUserResponse) Descriptor() ([]byte, []int) { + return file_auth0support_proto_rawDescGZIP(), []int{1} +} + +func (x *Auth0CreateUserResponse) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +var File_auth0support_proto protoreflect.FileDescriptor + +const file_auth0support_proto_rawDesc = "" + + "\n" + + "\x12auth0support.proto\x12\fauth0support\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\raccount.proto\"\x84\x02\n" + + "\x16Auth0CreateUserRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12%\n" + + "\x0eemail_verified\x18\x04 \x01(\bR\remailVerified\x12\x1d\n" + + "\n" + + "first_name\x18\x05 \x01(\tR\tfirstName\x12\x1b\n" + + "\tlast_name\x18\x06 \x01(\tR\blastName\x12#\n" + + "\rconnection_id\x18\a \x01(\tR\fconnectionId\x12\x1f\n" + + "\vpicture_url\x18\b \x01(\tR\n" + + "pictureUrl\"0\n" + + "\x17Auth0CreateUserResponse\x12\x15\n" + + "\x06org_id\x18\x01 \x01(\tR\x05orgId2\xc7\x01\n" + + "\fAuth0Support\x12Y\n" + + "\n" + + "CreateUser\x12$.auth0support.Auth0CreateUserRequest\x1a%.auth0support.Auth0CreateUserResponse\x12\\\n" + + "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_auth0support_proto_rawDescOnce sync.Once + file_auth0support_proto_rawDescData []byte +) + +func file_auth0support_proto_rawDescGZIP() []byte { + file_auth0support_proto_rawDescOnce.Do(func() { + file_auth0support_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth0support_proto_rawDesc), len(file_auth0support_proto_rawDesc))) + }) + return file_auth0support_proto_rawDescData +} + +var file_auth0support_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_auth0support_proto_goTypes = []any{ + (*Auth0CreateUserRequest)(nil), // 0: auth0support.Auth0CreateUserRequest + (*Auth0CreateUserResponse)(nil), // 1: auth0support.Auth0CreateUserResponse + (*AdminKeepaliveSourcesRequest)(nil), // 2: account.AdminKeepaliveSourcesRequest + (*KeepaliveSourcesResponse)(nil), // 3: account.KeepaliveSourcesResponse +} +var file_auth0support_proto_depIdxs = []int32{ + 0, // 0: auth0support.Auth0Support.CreateUser:input_type -> auth0support.Auth0CreateUserRequest + 2, // 1: auth0support.Auth0Support.KeepaliveSources:input_type -> account.AdminKeepaliveSourcesRequest + 1, // 2: auth0support.Auth0Support.CreateUser:output_type -> auth0support.Auth0CreateUserResponse + 3, // 3: auth0support.Auth0Support.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_auth0support_proto_init() } +func file_auth0support_proto_init() { + if File_auth0support_proto != nil { + return + } + file_account_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth0support_proto_rawDesc), len(file_auth0support_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_auth0support_proto_goTypes, + DependencyIndexes: file_auth0support_proto_depIdxs, + MessageInfos: file_auth0support_proto_msgTypes, + }.Build() + File_auth0support_proto = out.File + file_auth0support_proto_goTypes = nil + file_auth0support_proto_depIdxs = nil +} diff --git a/go/sdp-go/bookmarks.go b/go/sdp-go/bookmarks.go new file mode 100644 index 00000000..578fa845 --- /dev/null +++ b/go/sdp-go/bookmarks.go @@ -0,0 +1,23 @@ +package sdp + +func (b *Bookmark) ToMap() map[string]any { + return map[string]any{ + "metadata": b.GetMetadata().ToMap(), + "properties": b.GetProperties().ToMap(), + } +} + +func (bm *BookmarkMetadata) ToMap() map[string]any { + return map[string]any{ + "UUID": stringFromUuidBytes(bm.GetUUID()), + "created": bm.GetCreated().AsTime(), + } +} + +func (bp *BookmarkProperties) ToMap() map[string]any { + return map[string]any{ + "name": bp.GetName(), + "description": bp.GetDescription(), + "queries": bp.GetQueries(), + } +} diff --git a/go/sdp-go/bookmarks.pb.go b/go/sdp-go/bookmarks.pb.go new file mode 100644 index 00000000..4e7ca90e --- /dev/null +++ b/go/sdp-go/bookmarks.pb.go @@ -0,0 +1,886 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: bookmarks.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// a complete Bookmark with user-supplied and machine-supplied values +type Bookmark struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *BookmarkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *BookmarkProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Bookmark) Reset() { + *x = Bookmark{} + mi := &file_bookmarks_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Bookmark) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Bookmark) ProtoMessage() {} + +func (x *Bookmark) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[0] + 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 Bookmark.ProtoReflect.Descriptor instead. +func (*Bookmark) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{0} +} + +func (x *Bookmark) GetMetadata() *BookmarkMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Bookmark) GetProperties() *BookmarkProperties { + if x != nil { + return x.Properties + } + return nil +} + +// The user-editable parts of a Bookmark +type BookmarkProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // user supplied name of this bookmark + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // user supplied description of this bookmark + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // queries that make up the bookmark + Queries []*Query `protobuf:"bytes,3,rep,name=queries,proto3" json:"queries,omitempty"` + // Whether this bookmark is a system bookmark. System bookmarks are hidden + // from list results and can therefore only be accessed by their UUID. + // Bookmarks created by users are not system bookmarks. + IsSystem bool `protobuf:"varint,5,opt,name=isSystem,proto3" json:"isSystem,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BookmarkProperties) Reset() { + *x = BookmarkProperties{} + mi := &file_bookmarks_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BookmarkProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BookmarkProperties) ProtoMessage() {} + +func (x *BookmarkProperties) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[1] + 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 BookmarkProperties.ProtoReflect.Descriptor instead. +func (*BookmarkProperties) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{1} +} + +func (x *BookmarkProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *BookmarkProperties) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *BookmarkProperties) GetQueries() []*Query { + if x != nil { + return x.Queries + } + return nil +} + +func (x *BookmarkProperties) GetIsSystem() bool { + if x != nil { + return x.IsSystem + } + return false +} + +// Descriptor for a bookmark +type BookmarkMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id to identify this bookmark + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // timestamp when this bookmark was created + Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BookmarkMetadata) Reset() { + *x = BookmarkMetadata{} + mi := &file_bookmarks_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BookmarkMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BookmarkMetadata) ProtoMessage() {} + +func (x *BookmarkMetadata) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[2] + 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 BookmarkMetadata.ProtoReflect.Descriptor instead. +func (*BookmarkMetadata) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{2} +} + +func (x *BookmarkMetadata) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *BookmarkMetadata) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + +// list all bookmarks +type ListBookmarksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListBookmarksRequest) Reset() { + *x = ListBookmarksRequest{} + mi := &file_bookmarks_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListBookmarksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListBookmarksRequest) ProtoMessage() {} + +func (x *ListBookmarksRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[3] + 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 ListBookmarksRequest.ProtoReflect.Descriptor instead. +func (*ListBookmarksRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{3} +} + +type ListBookmarkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bookmarks []*Bookmark `protobuf:"bytes,3,rep,name=bookmarks,proto3" json:"bookmarks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListBookmarkResponse) Reset() { + *x = ListBookmarkResponse{} + mi := &file_bookmarks_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListBookmarkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListBookmarkResponse) ProtoMessage() {} + +func (x *ListBookmarkResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[4] + 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 ListBookmarkResponse.ProtoReflect.Descriptor instead. +func (*ListBookmarkResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{4} +} + +func (x *ListBookmarkResponse) GetBookmarks() []*Bookmark { + if x != nil { + return x.Bookmarks + } + return nil +} + +// creates a new bookmark +type CreateBookmarkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *BookmarkProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateBookmarkRequest) Reset() { + *x = CreateBookmarkRequest{} + mi := &file_bookmarks_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateBookmarkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateBookmarkRequest) ProtoMessage() {} + +func (x *CreateBookmarkRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[5] + 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 CreateBookmarkRequest.ProtoReflect.Descriptor instead. +func (*CreateBookmarkRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateBookmarkRequest) GetProperties() *BookmarkProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateBookmarkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bookmark *Bookmark `protobuf:"bytes,1,opt,name=bookmark,proto3" json:"bookmark,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateBookmarkResponse) Reset() { + *x = CreateBookmarkResponse{} + mi := &file_bookmarks_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateBookmarkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateBookmarkResponse) ProtoMessage() {} + +func (x *CreateBookmarkResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[6] + 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 CreateBookmarkResponse.ProtoReflect.Descriptor instead. +func (*CreateBookmarkResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateBookmarkResponse) GetBookmark() *Bookmark { + if x != nil { + return x.Bookmark + } + return nil +} + +// gets a specific bookmark +type GetBookmarkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBookmarkRequest) Reset() { + *x = GetBookmarkRequest{} + mi := &file_bookmarks_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBookmarkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBookmarkRequest) ProtoMessage() {} + +func (x *GetBookmarkRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[7] + 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 GetBookmarkRequest.ProtoReflect.Descriptor instead. +func (*GetBookmarkRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{7} +} + +func (x *GetBookmarkRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type GetBookmarkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bookmark *Bookmark `protobuf:"bytes,1,opt,name=bookmark,proto3" json:"bookmark,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBookmarkResponse) Reset() { + *x = GetBookmarkResponse{} + mi := &file_bookmarks_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBookmarkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBookmarkResponse) ProtoMessage() {} + +func (x *GetBookmarkResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[8] + 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 GetBookmarkResponse.ProtoReflect.Descriptor instead. +func (*GetBookmarkResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{8} +} + +func (x *GetBookmarkResponse) GetBookmark() *Bookmark { + if x != nil { + return x.Bookmark + } + return nil +} + +// updates an existing bookmark +type UpdateBookmarkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id to identify this bookmark + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // new attributes for this bookmark + Properties *BookmarkProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateBookmarkRequest) Reset() { + *x = UpdateBookmarkRequest{} + mi := &file_bookmarks_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateBookmarkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateBookmarkRequest) ProtoMessage() {} + +func (x *UpdateBookmarkRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[9] + 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 UpdateBookmarkRequest.ProtoReflect.Descriptor instead. +func (*UpdateBookmarkRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateBookmarkRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *UpdateBookmarkRequest) GetProperties() *BookmarkProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateBookmarkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bookmark *Bookmark `protobuf:"bytes,3,opt,name=bookmark,proto3" json:"bookmark,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateBookmarkResponse) Reset() { + *x = UpdateBookmarkResponse{} + mi := &file_bookmarks_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateBookmarkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateBookmarkResponse) ProtoMessage() {} + +func (x *UpdateBookmarkResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[10] + 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 UpdateBookmarkResponse.ProtoReflect.Descriptor instead. +func (*UpdateBookmarkResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateBookmarkResponse) GetBookmark() *Bookmark { + if x != nil { + return x.Bookmark + } + return nil +} + +// Delete the bookmark with the specified ID. +type DeleteBookmarkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id of the bookmark to delete + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteBookmarkRequest) Reset() { + *x = DeleteBookmarkRequest{} + mi := &file_bookmarks_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteBookmarkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteBookmarkRequest) ProtoMessage() {} + +func (x *DeleteBookmarkRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[11] + 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 DeleteBookmarkRequest.ProtoReflect.Descriptor instead. +func (*DeleteBookmarkRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteBookmarkRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type DeleteBookmarkResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteBookmarkResponse) Reset() { + *x = DeleteBookmarkResponse{} + mi := &file_bookmarks_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteBookmarkResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteBookmarkResponse) ProtoMessage() {} + +func (x *DeleteBookmarkResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[12] + 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 DeleteBookmarkResponse.ProtoReflect.Descriptor instead. +func (*DeleteBookmarkResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{12} +} + +type GetAffectedBookmarksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the snapshot to consider + SnapshotUUID []byte `protobuf:"bytes,1,opt,name=snapshotUUID,proto3" json:"snapshotUUID,omitempty"` + // the bookmarks to filter + BookmarkUUIDs [][]byte `protobuf:"bytes,2,rep,name=bookmarkUUIDs,proto3" json:"bookmarkUUIDs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAffectedBookmarksRequest) Reset() { + *x = GetAffectedBookmarksRequest{} + mi := &file_bookmarks_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAffectedBookmarksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAffectedBookmarksRequest) ProtoMessage() {} + +func (x *GetAffectedBookmarksRequest) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAffectedBookmarksRequest.ProtoReflect.Descriptor instead. +func (*GetAffectedBookmarksRequest) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{13} +} + +func (x *GetAffectedBookmarksRequest) GetSnapshotUUID() []byte { + if x != nil { + return x.SnapshotUUID + } + return nil +} + +func (x *GetAffectedBookmarksRequest) GetBookmarkUUIDs() [][]byte { + if x != nil { + return x.BookmarkUUIDs + } + return nil +} + +type GetAffectedBookmarksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the bookmarks that intersected with the snapshot + BookmarkUUIDs [][]byte `protobuf:"bytes,1,rep,name=bookmarkUUIDs,proto3" json:"bookmarkUUIDs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAffectedBookmarksResponse) Reset() { + *x = GetAffectedBookmarksResponse{} + mi := &file_bookmarks_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAffectedBookmarksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAffectedBookmarksResponse) ProtoMessage() {} + +func (x *GetAffectedBookmarksResponse) ProtoReflect() protoreflect.Message { + mi := &file_bookmarks_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAffectedBookmarksResponse.ProtoReflect.Descriptor instead. +func (*GetAffectedBookmarksResponse) Descriptor() ([]byte, []int) { + return file_bookmarks_proto_rawDescGZIP(), []int{14} +} + +func (x *GetAffectedBookmarksResponse) GetBookmarkUUIDs() [][]byte { + if x != nil { + return x.BookmarkUUIDs + } + return nil +} + +var File_bookmarks_proto protoreflect.FileDescriptor + +const file_bookmarks_proto_rawDesc = "" + + "\n" + + "\x0fbookmarks.proto\x12\tbookmarks\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\vitems.proto\"\x82\x01\n" + + "\bBookmark\x127\n" + + "\bmetadata\x18\x01 \x01(\v2\x1b.bookmarks.BookmarkMetadataR\bmetadata\x12=\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + + "properties\"\x8e\x01\n" + + "\x12BookmarkProperties\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12 \n" + + "\aqueries\x18\x03 \x03(\v2\x06.QueryR\aqueries\x12\x1a\n" + + "\bisSystem\x18\x05 \x01(\bR\bisSystemJ\x04\b\x04\x10\x05\"\\\n" + + "\x10BookmarkMetadata\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + + "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\"\x16\n" + + "\x14ListBookmarksRequest\"I\n" + + "\x14ListBookmarkResponse\x121\n" + + "\tbookmarks\x18\x03 \x03(\v2\x13.bookmarks.BookmarkR\tbookmarks\"V\n" + + "\x15CreateBookmarkRequest\x12=\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + + "properties\"I\n" + + "\x16CreateBookmarkResponse\x12/\n" + + "\bbookmark\x18\x01 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"(\n" + + "\x12GetBookmarkRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"F\n" + + "\x13GetBookmarkResponse\x12/\n" + + "\bbookmark\x18\x01 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"j\n" + + "\x15UpdateBookmarkRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12=\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1d.bookmarks.BookmarkPropertiesR\n" + + "properties\"I\n" + + "\x16UpdateBookmarkResponse\x12/\n" + + "\bbookmark\x18\x03 \x01(\v2\x13.bookmarks.BookmarkR\bbookmark\"+\n" + + "\x15DeleteBookmarkRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x18\n" + + "\x16DeleteBookmarkResponse\"g\n" + + "\x1bGetAffectedBookmarksRequest\x12\"\n" + + "\fsnapshotUUID\x18\x01 \x01(\fR\fsnapshotUUID\x12$\n" + + "\rbookmarkUUIDs\x18\x02 \x03(\fR\rbookmarkUUIDs\"D\n" + + "\x1cGetAffectedBookmarksResponse\x12$\n" + + "\rbookmarkUUIDs\x18\x01 \x03(\fR\rbookmarkUUIDs2\xa1\x04\n" + + "\x10BookmarksService\x12Q\n" + + "\rListBookmarks\x12\x1f.bookmarks.ListBookmarksRequest\x1a\x1f.bookmarks.ListBookmarkResponse\x12U\n" + + "\x0eCreateBookmark\x12 .bookmarks.CreateBookmarkRequest\x1a!.bookmarks.CreateBookmarkResponse\x12L\n" + + "\vGetBookmark\x12\x1d.bookmarks.GetBookmarkRequest\x1a\x1e.bookmarks.GetBookmarkResponse\x12U\n" + + "\x0eUpdateBookmark\x12 .bookmarks.UpdateBookmarkRequest\x1a!.bookmarks.UpdateBookmarkResponse\x12U\n" + + "\x0eDeleteBookmark\x12 .bookmarks.DeleteBookmarkRequest\x1a!.bookmarks.DeleteBookmarkResponse\x12g\n" + + "\x14GetAffectedBookmarks\x12&.bookmarks.GetAffectedBookmarksRequest\x1a'.bookmarks.GetAffectedBookmarksResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_bookmarks_proto_rawDescOnce sync.Once + file_bookmarks_proto_rawDescData []byte +) + +func file_bookmarks_proto_rawDescGZIP() []byte { + file_bookmarks_proto_rawDescOnce.Do(func() { + file_bookmarks_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bookmarks_proto_rawDesc), len(file_bookmarks_proto_rawDesc))) + }) + return file_bookmarks_proto_rawDescData +} + +var file_bookmarks_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_bookmarks_proto_goTypes = []any{ + (*Bookmark)(nil), // 0: bookmarks.Bookmark + (*BookmarkProperties)(nil), // 1: bookmarks.BookmarkProperties + (*BookmarkMetadata)(nil), // 2: bookmarks.BookmarkMetadata + (*ListBookmarksRequest)(nil), // 3: bookmarks.ListBookmarksRequest + (*ListBookmarkResponse)(nil), // 4: bookmarks.ListBookmarkResponse + (*CreateBookmarkRequest)(nil), // 5: bookmarks.CreateBookmarkRequest + (*CreateBookmarkResponse)(nil), // 6: bookmarks.CreateBookmarkResponse + (*GetBookmarkRequest)(nil), // 7: bookmarks.GetBookmarkRequest + (*GetBookmarkResponse)(nil), // 8: bookmarks.GetBookmarkResponse + (*UpdateBookmarkRequest)(nil), // 9: bookmarks.UpdateBookmarkRequest + (*UpdateBookmarkResponse)(nil), // 10: bookmarks.UpdateBookmarkResponse + (*DeleteBookmarkRequest)(nil), // 11: bookmarks.DeleteBookmarkRequest + (*DeleteBookmarkResponse)(nil), // 12: bookmarks.DeleteBookmarkResponse + (*GetAffectedBookmarksRequest)(nil), // 13: bookmarks.GetAffectedBookmarksRequest + (*GetAffectedBookmarksResponse)(nil), // 14: bookmarks.GetAffectedBookmarksResponse + (*Query)(nil), // 15: Query + (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp +} +var file_bookmarks_proto_depIdxs = []int32{ + 2, // 0: bookmarks.Bookmark.metadata:type_name -> bookmarks.BookmarkMetadata + 1, // 1: bookmarks.Bookmark.properties:type_name -> bookmarks.BookmarkProperties + 15, // 2: bookmarks.BookmarkProperties.queries:type_name -> Query + 16, // 3: bookmarks.BookmarkMetadata.created:type_name -> google.protobuf.Timestamp + 0, // 4: bookmarks.ListBookmarkResponse.bookmarks:type_name -> bookmarks.Bookmark + 1, // 5: bookmarks.CreateBookmarkRequest.properties:type_name -> bookmarks.BookmarkProperties + 0, // 6: bookmarks.CreateBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark + 0, // 7: bookmarks.GetBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark + 1, // 8: bookmarks.UpdateBookmarkRequest.properties:type_name -> bookmarks.BookmarkProperties + 0, // 9: bookmarks.UpdateBookmarkResponse.bookmark:type_name -> bookmarks.Bookmark + 3, // 10: bookmarks.BookmarksService.ListBookmarks:input_type -> bookmarks.ListBookmarksRequest + 5, // 11: bookmarks.BookmarksService.CreateBookmark:input_type -> bookmarks.CreateBookmarkRequest + 7, // 12: bookmarks.BookmarksService.GetBookmark:input_type -> bookmarks.GetBookmarkRequest + 9, // 13: bookmarks.BookmarksService.UpdateBookmark:input_type -> bookmarks.UpdateBookmarkRequest + 11, // 14: bookmarks.BookmarksService.DeleteBookmark:input_type -> bookmarks.DeleteBookmarkRequest + 13, // 15: bookmarks.BookmarksService.GetAffectedBookmarks:input_type -> bookmarks.GetAffectedBookmarksRequest + 4, // 16: bookmarks.BookmarksService.ListBookmarks:output_type -> bookmarks.ListBookmarkResponse + 6, // 17: bookmarks.BookmarksService.CreateBookmark:output_type -> bookmarks.CreateBookmarkResponse + 8, // 18: bookmarks.BookmarksService.GetBookmark:output_type -> bookmarks.GetBookmarkResponse + 10, // 19: bookmarks.BookmarksService.UpdateBookmark:output_type -> bookmarks.UpdateBookmarkResponse + 12, // 20: bookmarks.BookmarksService.DeleteBookmark:output_type -> bookmarks.DeleteBookmarkResponse + 14, // 21: bookmarks.BookmarksService.GetAffectedBookmarks:output_type -> bookmarks.GetAffectedBookmarksResponse + 16, // [16:22] is the sub-list for method output_type + 10, // [10:16] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_bookmarks_proto_init() } +func file_bookmarks_proto_init() { + if File_bookmarks_proto != nil { + return + } + file_items_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_bookmarks_proto_rawDesc), len(file_bookmarks_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_bookmarks_proto_goTypes, + DependencyIndexes: file_bookmarks_proto_depIdxs, + MessageInfos: file_bookmarks_proto_msgTypes, + }.Build() + File_bookmarks_proto = out.File + file_bookmarks_proto_goTypes = nil + file_bookmarks_proto_depIdxs = nil +} diff --git a/go/sdp-go/cached_entry.pb.go b/go/sdp-go/cached_entry.pb.go new file mode 100644 index 00000000..c82a8e54 --- /dev/null +++ b/go/sdp-go/cached_entry.pb.go @@ -0,0 +1,188 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: cached_entry.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// CachedEntry represents a cached result in the BoltDB cache +type CachedEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The cached item (nil/empty for errors) + Item *Item `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // The cached error (nil/empty for items) + Error *QueryError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + // Expiry timestamp in Unix nanoseconds + ExpiryUnixNano int64 `protobuf:"varint,3,opt,name=expiry_unix_nano,json=expiryUnixNano,proto3" json:"expiry_unix_nano,omitempty"` + // Index values for efficient lookup + UniqueAttributeValue string `protobuf:"bytes,4,opt,name=unique_attribute_value,json=uniqueAttributeValue,proto3" json:"unique_attribute_value,omitempty"` + Method QueryMethod `protobuf:"varint,5,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` + Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"` + SstHash string `protobuf:"bytes,7,opt,name=sst_hash,json=sstHash,proto3" json:"sst_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CachedEntry) Reset() { + *x = CachedEntry{} + mi := &file_cached_entry_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CachedEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CachedEntry) ProtoMessage() {} + +func (x *CachedEntry) ProtoReflect() protoreflect.Message { + mi := &file_cached_entry_proto_msgTypes[0] + 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 CachedEntry.ProtoReflect.Descriptor instead. +func (*CachedEntry) Descriptor() ([]byte, []int) { + return file_cached_entry_proto_rawDescGZIP(), []int{0} +} + +func (x *CachedEntry) GetItem() *Item { + if x != nil { + return x.Item + } + return nil +} + +func (x *CachedEntry) GetError() *QueryError { + if x != nil { + return x.Error + } + return nil +} + +func (x *CachedEntry) GetExpiryUnixNano() int64 { + if x != nil { + return x.ExpiryUnixNano + } + return 0 +} + +func (x *CachedEntry) GetUniqueAttributeValue() string { + if x != nil { + return x.UniqueAttributeValue + } + return "" +} + +func (x *CachedEntry) GetMethod() QueryMethod { + if x != nil { + return x.Method + } + return QueryMethod_GET +} + +func (x *CachedEntry) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *CachedEntry) GetSstHash() string { + if x != nil { + return x.SstHash + } + return "" +} + +var File_cached_entry_proto protoreflect.FileDescriptor + +const file_cached_entry_proto_rawDesc = "" + + "\n" + + "\x12cached_entry.proto\x1a\vitems.proto\"\x82\x02\n" + + "\vCachedEntry\x12\x19\n" + + "\x04item\x18\x01 \x01(\v2\x05.ItemR\x04item\x12!\n" + + "\x05error\x18\x02 \x01(\v2\v.QueryErrorR\x05error\x12(\n" + + "\x10expiry_unix_nano\x18\x03 \x01(\x03R\x0eexpiryUnixNano\x124\n" + + "\x16unique_attribute_value\x18\x04 \x01(\tR\x14uniqueAttributeValue\x12$\n" + + "\x06method\x18\x05 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + + "\x05query\x18\x06 \x01(\tR\x05query\x12\x19\n" + + "\bsst_hash\x18\a \x01(\tR\asstHashB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_cached_entry_proto_rawDescOnce sync.Once + file_cached_entry_proto_rawDescData []byte +) + +func file_cached_entry_proto_rawDescGZIP() []byte { + file_cached_entry_proto_rawDescOnce.Do(func() { + file_cached_entry_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cached_entry_proto_rawDesc), len(file_cached_entry_proto_rawDesc))) + }) + return file_cached_entry_proto_rawDescData +} + +var file_cached_entry_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_cached_entry_proto_goTypes = []any{ + (*CachedEntry)(nil), // 0: CachedEntry + (*Item)(nil), // 1: Item + (*QueryError)(nil), // 2: QueryError + (QueryMethod)(0), // 3: QueryMethod +} +var file_cached_entry_proto_depIdxs = []int32{ + 1, // 0: CachedEntry.item:type_name -> Item + 2, // 1: CachedEntry.error:type_name -> QueryError + 3, // 2: CachedEntry.method:type_name -> QueryMethod + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_cached_entry_proto_init() } +func file_cached_entry_proto_init() { + if File_cached_entry_proto != nil { + return + } + file_items_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_cached_entry_proto_rawDesc), len(file_cached_entry_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_cached_entry_proto_goTypes, + DependencyIndexes: file_cached_entry_proto_depIdxs, + MessageInfos: file_cached_entry_proto_msgTypes, + }.Build() + File_cached_entry_proto = out.File + file_cached_entry_proto_goTypes = nil + file_cached_entry_proto_depIdxs = nil +} diff --git a/go/sdp-go/changes.go b/go/sdp-go/changes.go new file mode 100644 index 00000000..b338f2b1 --- /dev/null +++ b/go/sdp-go/changes.go @@ -0,0 +1,484 @@ +package sdp + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + "gopkg.in/yaml.v3" +) + +// GetUUIDParsed returns the parsed UUID from the ChangeMetadata, or nil if invalid. +func (a *ChangeMetadata) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// GetNullUUID returns the UUID as a nullable type for database operations. +func (a *ChangeMetadata) GetNullUUID() uuid.NullUUID { + u := a.GetUUIDParsed() + if u == nil { + return uuid.NullUUID{Valid: false} + } + return uuid.NullUUID{UUID: *u, Valid: true} +} + +// GetChangingItemsBookmarkUUIDParsed returns the parsed UUID for the bookmark +// containing the directly affected items, or nil if invalid. +func (a *ChangeProperties) GetChangingItemsBookmarkUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetChangingItemsBookmarkUUID()) + if err != nil { + return nil + } + return &u +} + +// GetSystemBeforeSnapshotUUIDParsed returns the parsed UUID for the whole-system +// snapshot taken before the change, or nil if invalid. +func (a *ChangeProperties) GetSystemBeforeSnapshotUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetSystemBeforeSnapshotUUID()) + if err != nil { + return nil + } + return &u +} + +// GetSystemAfterSnapshotUUIDParsed returns the parsed UUID for the whole-system +// snapshot taken after the change, or nil if invalid. +func (a *ChangeProperties) GetSystemAfterSnapshotUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetSystemAfterSnapshotUUID()) + if err != nil { + return nil + } + return &u +} + +// GetUUIDParsed returns the parsed UUID from GetChangeRequest, or nil if invalid. +func (a *GetChangeRequest) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// GetUUIDParsed returns the parsed UUID from UpdateChangeRequest, or nil if invalid. +func (a *UpdateChangeRequest) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// GetUUIDParsed returns the parsed UUID from DeleteChangeRequest, or nil if invalid. +func (a *DeleteChangeRequest) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(a.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// GetChangeUUIDParsed returns the parsed change UUID from GetDiffRequest, or nil if invalid. +func (x *GetDiffRequest) GetChangeUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetChangeUUID()) + if err != nil { + return nil + } + return &u +} + +// GetChangeUUIDParsed returns the parsed change UUID from ListChangingItemsSummaryRequest, or nil if invalid. +func (x *ListChangingItemsSummaryRequest) GetChangeUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetChangeUUID()) + if err != nil { + return nil + } + return &u +} + +// GetChangeUUIDParsed returns the parsed change UUID from StartChangeRequest, or nil if invalid. +func (x *StartChangeRequest) GetChangeUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetChangeUUID()) + if err != nil { + return nil + } + return &u +} + +// GetChangeUUIDParsed returns the parsed change UUID from EndChangeRequest, or nil if invalid. +func (x *EndChangeRequest) GetChangeUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetChangeUUID()) + if err != nil { + return nil + } + return &u +} + +// ToMap converts a Change to a map for serialization (e.g., for LLM templates). +func (c *Change) ToMap() map[string]any { + return map[string]any{ + "metadata": c.GetMetadata().ToMap(), + "properties": c.GetProperties().ToMap(), + } +} + +// stringFromUuidBytes converts UUID bytes to a string, returning empty string on error. +func stringFromUuidBytes(b []byte) string { + u, err := uuid.FromBytes(b) + if err != nil { + return "" + } + return u.String() +} + +// ToMap converts a Reference to a map for serialization. +func (r *Reference) ToMap() map[string]any { + return map[string]any{ + "type": r.GetType(), + "uniqueAttributeValue": r.GetUniqueAttributeValue(), + "scope": r.GetScope(), + } +} + +// ToMap converts a Risk to a map for serialization, including related items. +func (r *Risk) ToMap() map[string]any { + relatedItems := make([]map[string]any, len(r.GetRelatedItems())) + for i, ri := range r.GetRelatedItems() { + relatedItems[i] = ri.ToMap() + } + + return map[string]any{ + "uuid": stringFromUuidBytes(r.GetUUID()), + "title": r.GetTitle(), + "severity": r.GetSeverity().String(), + "description": r.GetDescription(), + "relatedItems": relatedItems, + } +} + +// ToMap converts a GetChangeRisksResponse to a map for serialization. +func (r *GetChangeRisksResponse) ToMap() map[string]any { + rmd := r.GetChangeRiskMetadata() + risks := make([]map[string]any, len(rmd.GetRisks())) + for i, ri := range rmd.GetRisks() { + risks[i] = ri.ToMap() + } + + return map[string]any{ + "risks": risks, + "numHighRisk": rmd.GetNumHighRisk(), + "numMediumRisk": rmd.GetNumMediumRisk(), + "numLowRisk": rmd.GetNumLowRisk(), + "changeAnalysisStatus": rmd.GetChangeAnalysisStatus().ToMap(), + } +} + +// ToMap converts ChangeMetadata to a map for serialization. +func (cm *ChangeMetadata) ToMap() map[string]any { + return map[string]any{ + "UUID": stringFromUuidBytes(cm.GetUUID()), + "createdAt": cm.GetCreatedAt().AsTime(), + "updatedAt": cm.GetUpdatedAt().AsTime(), + "status": cm.GetStatus().String(), + "creatorName": cm.GetCreatorName(), + "numAffectedItems": cm.GetNumAffectedItems(), + "numAffectedEdges": cm.GetNumAffectedEdges(), + "numUnchangedItems": cm.GetNumUnchangedItems(), + "numCreatedItems": cm.GetNumCreatedItems(), + "numUpdatedItems": cm.GetNumUpdatedItems(), + "numDeletedItems": cm.GetNumDeletedItems(), + "UnknownHealthChange": cm.GetUnknownHealthChange(), + "OkHealthChange": cm.GetOkHealthChange(), + "WarningHealthChange": cm.GetWarningHealthChange(), + "ErrorHealthChange": cm.GetErrorHealthChange(), + "PendingHealthChange": cm.GetPendingHealthChange(), + } +} + +// ToMap converts an Item to a map for serialization. +func (i *Item) ToMap() map[string]any { + return map[string]any{ + "type": i.GetType(), + "uniqueAttributeValue": i.UniqueAttributeValue(), + "scope": i.GetScope(), + "attributes": i.GetAttributes().GetAttrStruct().GetFields(), + } +} + +// ToMap converts an ItemDiff to a map for serialization. +func (id *ItemDiff) ToMap() map[string]any { + result := map[string]any{ + "status": id.GetStatus().String(), + } + if id.GetItem() != nil { + result["item"] = id.GetItem().ToMap() + } + if id.GetBefore() != nil { + result["before"] = id.GetBefore().ToMap() + } + if id.GetAfter() != nil { + result["after"] = id.GetAfter().ToMap() + } + return result +} + +// GloballyUniqueName returns the GUN from the item, before, or after state, +// whichever is available (in that order of preference). +func (id *ItemDiff) GloballyUniqueName() string { + if id.GetItem() != nil { + return id.GetItem().GloballyUniqueName() + } else if id.GetBefore() != nil { + return id.GetBefore().GloballyUniqueName() + } else if id.GetAfter() != nil { + return id.GetAfter().GloballyUniqueName() + } else { + return "empty item diff" + } +} + +// ToMap converts ChangeProperties to a map for serialization. +func (cp *ChangeProperties) ToMap() map[string]any { + plannedChanges := make([]map[string]any, len(cp.GetPlannedChanges())) + for i, id := range cp.GetPlannedChanges() { + plannedChanges[i] = id.ToMap() + } + + return map[string]any{ + "title": cp.GetTitle(), + "description": cp.GetDescription(), + "ticketLink": cp.GetTicketLink(), + "owner": cp.GetOwner(), + "ccEmails": cp.GetCcEmails(), + "changingItemsBookmarkUUID": stringFromUuidBytes(cp.GetChangingItemsBookmarkUUID()), + "blastRadiusSnapshotUUID": stringFromUuidBytes(cp.GetBlastRadiusSnapshotUUID()), + "systemBeforeSnapshotUUID": stringFromUuidBytes(cp.GetSystemBeforeSnapshotUUID()), + "systemAfterSnapshotUUID": stringFromUuidBytes(cp.GetSystemAfterSnapshotUUID()), + "plannedChanges": cp.GetPlannedChanges(), + "rawPlan": cp.GetRawPlan(), + "codeChanges": cp.GetCodeChanges(), + "repo": cp.GetRepo(), + "tags": cp.GetEnrichedTags(), + } +} + +// ToMap converts ChangeAnalysisStatus to a map for serialization. +func (rcs *ChangeAnalysisStatus) ToMap() map[string]any { + if rcs == nil { + return map[string]any{} + } + + return map[string]any{ + "status": rcs.GetStatus().String(), + } +} + +// ToMessage converts a StartChangeResponse_State enum to a human-readable message. +func (s StartChangeResponse_State) ToMessage() string { + switch s { + case StartChangeResponse_STATE_UNSPECIFIED: + return "unknown" + case StartChangeResponse_STATE_TAKING_SNAPSHOT: + return "Snapshot is being taken" + case StartChangeResponse_STATE_SAVING_SNAPSHOT: + return "Snapshot is being saved" + case StartChangeResponse_STATE_DONE: + return "Everything is complete" + default: + return "unknown" + } +} + +// ToMessage converts an EndChangeResponse_State enum to a human-readable message. +func (s EndChangeResponse_State) ToMessage() string { + switch s { + case EndChangeResponse_STATE_UNSPECIFIED: + return "unknown" + case EndChangeResponse_STATE_TAKING_SNAPSHOT: + return "Snapshot is being taken" + case EndChangeResponse_STATE_SAVING_SNAPSHOT: + return "Snapshot is being saved" + case EndChangeResponse_STATE_DONE: + return "Everything is complete" + default: + return "unknown" + } +} + +// RoutineChangesYAML represents the YAML structure for routine changes configuration. +// It defines parameters for detecting routine changes in infrastructure: +// - Sensitivity: Threshold for determining what constitutes a routine change (0 or higher) +// - DurationInDays: Time window in days to analyze for routine patterns (must be >= 1) +// - EventsPerDay: Expected number of change events per day for routine detection (must be >= 1) +type RoutineChangesYAML struct { + Sensitivity float32 `yaml:"sensitivity"` + DurationInDays float32 `yaml:"duration_in_days"` + EventsPerDay float32 `yaml:"events_per_day"` +} + +// GithubOrganisationYAML represents the YAML structure for GitHub organization profile configuration. +// It contains organization-specific settings such as the primary branch name used for +// change detection and analysis. +type GithubOrganisationYAML struct { + PrimaryBranchName string `yaml:"primary_branch_name"` +} + +// SignalConfigYAML represents the root YAML structure for signal configuration files. +// It can contain either or both of: +// - RoutineChangesConfig: Configuration for routine change detection +// - GithubOrganisationProfile: GitHub organization-specific settings +// At least one section must be provided in the YAML file. +type SignalConfigYAML struct { + RoutineChangesConfig *RoutineChangesYAML `yaml:"routine_changes_config,omitempty"` + GithubOrganisationProfile *GithubOrganisationYAML `yaml:"github_organisation_profile,omitempty"` +} + +// SignalConfigFile represents the internal, parsed signal configuration structure. +// This is the converted form of SignalConfigYAML, where YAML-specific types are +// transformed into their corresponding protocol buffer types for use in the application. +type SignalConfigFile struct { + RoutineChangesConfig *RoutineChangesConfig + GithubOrganisationProfile *GithubOrganisationProfile +} + +// YamlStringToSignalConfig parses a YAML string containing signal configuration and converts it +// into a SignalConfigFile. It validates that at least one configuration section is provided +// and performs validation on the routine changes configuration if present. +// +// The function handles conversion from YAML-friendly types (e.g., float32 for durations) +// to the internal protocol buffer types (e.g., RoutineChangesConfig with unit specifications). +// +// Returns an error if: +// - The YAML is invalid or cannot be unmarshaled +// - No configuration sections are provided +// - Routine changes configuration validation fails +func YamlStringToSignalConfig(yamlString string) (*SignalConfigFile, error) { + var signalConfigYAML SignalConfigYAML + err := yaml.Unmarshal([]byte(yamlString), &signalConfigYAML) + if err != nil { + return nil, fmt.Errorf("error unmarshalling yaml to signal config: %w", err) + } + + // check that at least one section is provided + if signalConfigYAML.RoutineChangesConfig == nil && signalConfigYAML.GithubOrganisationProfile == nil { + return nil, fmt.Errorf("signal config file must contain at least one of: routine_changes_config or github_organisation_profile") + } + + // validate the routine changes config + if signalConfigYAML.RoutineChangesConfig != nil { + if err := validateRoutineChangesConfig(signalConfigYAML.RoutineChangesConfig); err != nil { + return nil, err + } + } + + var routineCfg *RoutineChangesConfig + if signalConfigYAML.RoutineChangesConfig != nil { + routineCfg = &RoutineChangesConfig{ + Sensitivity: signalConfigYAML.RoutineChangesConfig.Sensitivity, + EventsPer: signalConfigYAML.RoutineChangesConfig.EventsPerDay, + EventsPerUnit: RoutineChangesConfig_DAYS, + Duration: signalConfigYAML.RoutineChangesConfig.DurationInDays, + DurationUnit: RoutineChangesConfig_DAYS, + } + } + + var githubProfile *GithubOrganisationProfile + if signalConfigYAML.GithubOrganisationProfile != nil { + githubProfile = &GithubOrganisationProfile{ + PrimaryBranchName: signalConfigYAML.GithubOrganisationProfile.PrimaryBranchName, + } + } + + signalConfigFile := &SignalConfigFile{ + RoutineChangesConfig: routineCfg, + GithubOrganisationProfile: githubProfile, + } + return signalConfigFile, nil +} + +// validateRoutineChangesConfig validates the routine changes configuration values. +// It ensures that: +// - EventsPerDay is at least 1 +// - DurationInDays is at least 1 +// - Sensitivity is 0 or higher +// +// Returns an error with a descriptive message if any validation fails. +func validateRoutineChangesConfig(routineChangesConfigYAML *RoutineChangesYAML) error { + if routineChangesConfigYAML.EventsPerDay < 1 { + return fmt.Errorf("events_per_day must be greater than 1, got %v", routineChangesConfigYAML.EventsPerDay) + } + if routineChangesConfigYAML.DurationInDays < 1 { + return fmt.Errorf("duration_in_days must be greater than 1, got %v", routineChangesConfigYAML.DurationInDays) + } + if routineChangesConfigYAML.Sensitivity < 0 { + return fmt.Errorf("sensitivity must be 0 or higher, got %v", routineChangesConfigYAML.Sensitivity) + } + return nil +} + +// TimelineEntryContentDescription returns a human-readable description of the +// entry's content based on its type. +func TimelineEntryContentDescription(entry *ChangeTimelineEntryV2) string { + switch c := entry.GetContent().(type) { + case *ChangeTimelineEntryV2_MappedItems: + return fmt.Sprintf("%d mapped items", len(c.MappedItems.GetMappedItems())) + case *ChangeTimelineEntryV2_CalculatedBlastRadius: + return fmt.Sprintf("%d items, %d edges", c.CalculatedBlastRadius.GetNumItems(), c.CalculatedBlastRadius.GetNumEdges()) + case *ChangeTimelineEntryV2_CalculatedRisks: + return fmt.Sprintf("%d risks", len(c.CalculatedRisks.GetRisks())) + case *ChangeTimelineEntryV2_CalculatedLabels: + return fmt.Sprintf("%d labels", len(c.CalculatedLabels.GetLabels())) + case *ChangeTimelineEntryV2_ChangeValidation: + return fmt.Sprintf("%d validation categories", len(c.ChangeValidation.GetValidationChecklist())) + case *ChangeTimelineEntryV2_FormHypotheses: + return fmt.Sprintf("%d hypotheses", c.FormHypotheses.GetNumHypotheses()) + case *ChangeTimelineEntryV2_InvestigateHypotheses: + return fmt.Sprintf("%d proven, %d disproven, %d investigating", + c.InvestigateHypotheses.GetNumProven(), + c.InvestigateHypotheses.GetNumDisproven(), + c.InvestigateHypotheses.GetNumInvestigating()) + case *ChangeTimelineEntryV2_RecordObservations: + return fmt.Sprintf("%d observations", c.RecordObservations.GetNumObservations()) + case *ChangeTimelineEntryV2_Error: + return c.Error + case *ChangeTimelineEntryV2_StatusMessage: + return c.StatusMessage + case *ChangeTimelineEntryV2_Empty, nil: + return "" + default: + return "" + } +} + +// TimelineFindInProgressEntry returns the current running entry in the list of entries +// The function handles the following cases: +// - If the input slice is nil or empty, it returns an error. +// - The first entry that has a status of IN_PROGRESS, PENDING, or ERROR, it returns the entry's name, content description, status, and a nil error. +// - If an entry has an unknown status, it returns an error. +// - If the timeline is complete it returns an empty string, empty content description, DONE status, and a nil error. +func TimelineFindInProgressEntry(entries []*ChangeTimelineEntryV2) (string, string, ChangeTimelineEntryStatus, error) { + if entries == nil { + return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, errors.New("entries is nil") + } + if len(entries) == 0 { + return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, errors.New("entries is empty") + } + + for _, entry := range entries { + switch entry.GetStatus() { + case ChangeTimelineEntryStatus_IN_PROGRESS, ChangeTimelineEntryStatus_PENDING, ChangeTimelineEntryStatus_ERROR: + // if the entry is in progress or about to start, or has an error(to be retried) + return entry.GetName(), TimelineEntryContentDescription(entry), entry.GetStatus(), nil + case ChangeTimelineEntryStatus_UNSPECIFIED, ChangeTimelineEntryStatus_DONE: + // do nothing + default: + return "", "", ChangeTimelineEntryStatus_UNSPECIFIED, fmt.Errorf("unknown status: %s", entry.GetStatus().String()) + } + } + + return "", "", ChangeTimelineEntryStatus_DONE, nil +} diff --git a/go/sdp-go/changes.pb.go b/go/sdp-go/changes.pb.go new file mode 100644 index 00000000..5e3f6c43 --- /dev/null +++ b/go/sdp-go/changes.pb.go @@ -0,0 +1,7264 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: changes.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = 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 + MappedItemTimelineStatus_MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION MappedItemTimelineStatus = 4 +) + +// 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", + 4: "MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION", + } + 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, + "MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION": 4, + } +) + +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} +} + +// Explicit mapping status from CLI - allows CLI to communicate state instead of API inferring +type MappedItemMappingStatus int32 + +const ( + MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED MappedItemMappingStatus = 0 + MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_SUCCESS MappedItemMappingStatus = 1 + MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED MappedItemMappingStatus = 2 + MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION MappedItemMappingStatus = 3 + MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR MappedItemMappingStatus = 4 +) + +// Enum value maps for MappedItemMappingStatus. +var ( + MappedItemMappingStatus_name = map[int32]string{ + 0: "MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED", + 1: "MAPPED_ITEM_MAPPING_STATUS_SUCCESS", + 2: "MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED", + 3: "MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION", + 4: "MAPPED_ITEM_MAPPING_STATUS_ERROR", + } + MappedItemMappingStatus_value = map[string]int32{ + "MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED": 0, + "MAPPED_ITEM_MAPPING_STATUS_SUCCESS": 1, + "MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED": 2, + "MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION": 3, + "MAPPED_ITEM_MAPPING_STATUS_ERROR": 4, + } +) + +func (x MappedItemMappingStatus) Enum() *MappedItemMappingStatus { + p := new(MappedItemMappingStatus) + *p = x + return p +} + +func (x MappedItemMappingStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MappedItemMappingStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[1].Descriptor() +} + +func (MappedItemMappingStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[1] +} + +func (x MappedItemMappingStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MappedItemMappingStatus.Descriptor instead. +func (MappedItemMappingStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{1} +} + +type HypothesisStatus int32 + +const ( + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED HypothesisStatus = 0 + // The hypothesis is being formed, the detail may change as more observations + // are recorded + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_FORMING HypothesisStatus = 1 + // The hypotheses is being investigated, the detail will be available once the + // investigation is complete + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING HypothesisStatus = 2 + // They hypothesis has been proven to be a risk + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_PROVEN HypothesisStatus = 3 + // The hypothesis has been disproven, no risk has been found + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN HypothesisStatus = 4 +) + +// Enum value maps for HypothesisStatus. +var ( + HypothesisStatus_name = map[int32]string{ + 0: "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED", + 1: "INVESTIGATED_HYPOTHESIS_STATUS_FORMING", + 2: "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING", + 3: "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN", + 4: "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN", + } + HypothesisStatus_value = map[string]int32{ + "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED": 0, + "INVESTIGATED_HYPOTHESIS_STATUS_FORMING": 1, + "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING": 2, + "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN": 3, + "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN": 4, + } +) + +func (x HypothesisStatus) Enum() *HypothesisStatus { + p := new(HypothesisStatus) + *p = x + return p +} + +func (x HypothesisStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HypothesisStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[2].Descriptor() +} + +func (HypothesisStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[2] +} + +func (x HypothesisStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HypothesisStatus.Descriptor instead. +func (HypothesisStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{2} +} + +type ChangeTimelineEntryStatus int32 + +const ( + // This should never be used, it is the default value + ChangeTimelineEntryStatus_UNSPECIFIED ChangeTimelineEntryStatus = 0 + // This step has not yet started + ChangeTimelineEntryStatus_PENDING ChangeTimelineEntryStatus = 1 + // This step is currently happening + ChangeTimelineEntryStatus_IN_PROGRESS ChangeTimelineEntryStatus = 2 + // The step is completed + ChangeTimelineEntryStatus_DONE ChangeTimelineEntryStatus = 3 + // The step has an error and cannot be completed + ChangeTimelineEntryStatus_ERROR ChangeTimelineEntryStatus = 4 +) + +// Enum value maps for ChangeTimelineEntryStatus. +var ( + ChangeTimelineEntryStatus_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "PENDING", + 2: "IN_PROGRESS", + 3: "DONE", + 4: "ERROR", + } + ChangeTimelineEntryStatus_value = map[string]int32{ + "UNSPECIFIED": 0, + "PENDING": 1, + "IN_PROGRESS": 2, + "DONE": 3, + "ERROR": 4, + } +) + +func (x ChangeTimelineEntryStatus) Enum() *ChangeTimelineEntryStatus { + p := new(ChangeTimelineEntryStatus) + *p = x + return p +} + +func (x ChangeTimelineEntryStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChangeTimelineEntryStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[3].Descriptor() +} + +func (ChangeTimelineEntryStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[3] +} + +func (x ChangeTimelineEntryStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChangeTimelineEntryStatus.Descriptor instead. +func (ChangeTimelineEntryStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{3} +} + +type ItemDiffStatus int32 + +const ( + ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED ItemDiffStatus = 0 + ItemDiffStatus_ITEM_DIFF_STATUS_UNCHANGED ItemDiffStatus = 1 + ItemDiffStatus_ITEM_DIFF_STATUS_CREATED ItemDiffStatus = 2 + ItemDiffStatus_ITEM_DIFF_STATUS_UPDATED ItemDiffStatus = 3 + ItemDiffStatus_ITEM_DIFF_STATUS_DELETED ItemDiffStatus = 4 + ItemDiffStatus_ITEM_DIFF_STATUS_REPLACED ItemDiffStatus = 5 +) + +// Enum value maps for ItemDiffStatus. +var ( + ItemDiffStatus_name = map[int32]string{ + 0: "ITEM_DIFF_STATUS_UNSPECIFIED", + 1: "ITEM_DIFF_STATUS_UNCHANGED", + 2: "ITEM_DIFF_STATUS_CREATED", + 3: "ITEM_DIFF_STATUS_UPDATED", + 4: "ITEM_DIFF_STATUS_DELETED", + 5: "ITEM_DIFF_STATUS_REPLACED", + } + ItemDiffStatus_value = map[string]int32{ + "ITEM_DIFF_STATUS_UNSPECIFIED": 0, + "ITEM_DIFF_STATUS_UNCHANGED": 1, + "ITEM_DIFF_STATUS_CREATED": 2, + "ITEM_DIFF_STATUS_UPDATED": 3, + "ITEM_DIFF_STATUS_DELETED": 4, + "ITEM_DIFF_STATUS_REPLACED": 5, + } +) + +func (x ItemDiffStatus) Enum() *ItemDiffStatus { + p := new(ItemDiffStatus) + *p = x + return p +} + +func (x ItemDiffStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ItemDiffStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[4].Descriptor() +} + +func (ItemDiffStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[4] +} + +func (x ItemDiffStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ItemDiffStatus.Descriptor instead. +func (ItemDiffStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{4} +} + +type ChangeOutputFormat int32 + +const ( + ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED ChangeOutputFormat = 0 + ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_JSON ChangeOutputFormat = 1 + ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_MARKDOWN ChangeOutputFormat = 2 +) + +// Enum value maps for ChangeOutputFormat. +var ( + ChangeOutputFormat_name = map[int32]string{ + 0: "CHANGE_OUTPUT_FORMAT_UNSPECIFIED", + 1: "CHANGE_OUTPUT_FORMAT_JSON", + 2: "CHANGE_OUTPUT_FORMAT_MARKDOWN", + } + ChangeOutputFormat_value = map[string]int32{ + "CHANGE_OUTPUT_FORMAT_UNSPECIFIED": 0, + "CHANGE_OUTPUT_FORMAT_JSON": 1, + "CHANGE_OUTPUT_FORMAT_MARKDOWN": 2, + } +) + +func (x ChangeOutputFormat) Enum() *ChangeOutputFormat { + p := new(ChangeOutputFormat) + *p = x + return p +} + +func (x ChangeOutputFormat) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChangeOutputFormat) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[5].Descriptor() +} + +func (ChangeOutputFormat) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[5] +} + +func (x ChangeOutputFormat) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChangeOutputFormat.Descriptor instead. +func (ChangeOutputFormat) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{5} +} + +type LabelType int32 + +const ( + LabelType_LABEL_TYPE_UNSPECIFIED LabelType = 0 + LabelType_LABEL_TYPE_AUTO LabelType = 1 + LabelType_LABEL_TYPE_USER LabelType = 2 +) + +// Enum value maps for LabelType. +var ( + LabelType_name = map[int32]string{ + 0: "LABEL_TYPE_UNSPECIFIED", + 1: "LABEL_TYPE_AUTO", + 2: "LABEL_TYPE_USER", + } + LabelType_value = map[string]int32{ + "LABEL_TYPE_UNSPECIFIED": 0, + "LABEL_TYPE_AUTO": 1, + "LABEL_TYPE_USER": 2, + } +) + +func (x LabelType) Enum() *LabelType { + p := new(LabelType) + *p = x + return p +} + +func (x LabelType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LabelType) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[6].Descriptor() +} + +func (LabelType) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[6] +} + +func (x LabelType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LabelType.Descriptor instead. +func (LabelType) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{6} +} + +type ChangeStatus int32 + +const ( + // Reserved for truly unspecified states. Should not be used for newly created changes. + ChangeStatus_CHANGE_STATUS_UNSPECIFIED ChangeStatus = 0 + // The change has been created and is ready for change analysis to be started. + // Or change analysis is in progress. + // Or change analysis is complete and the change is ready to be started. + ChangeStatus_CHANGE_STATUS_DEFINING ChangeStatus = 1 + // The change is in progress or deployment is in progress. The change can be ended using the `EndChange` + // RPC. + ChangeStatus_CHANGE_STATUS_HAPPENING ChangeStatus = 2 + // DEPRECATED: This status is no longer used and should not be used in new code. + // It will be removed in a future version. Use other appropriate status values instead. + // Deprecated as part of https://linear.app/overmind/issue/ENG-1520/change-status-processing-is-no-longer-used-in-a-meaningful-way + // + // Deprecated: Marked as deprecated in changes.proto. + ChangeStatus_CHANGE_STATUS_PROCESSING ChangeStatus = 3 + // The change has been ended and the results have been processed. + ChangeStatus_CHANGE_STATUS_DONE ChangeStatus = 4 +) + +// Enum value maps for ChangeStatus. +var ( + ChangeStatus_name = map[int32]string{ + 0: "CHANGE_STATUS_UNSPECIFIED", + 1: "CHANGE_STATUS_DEFINING", + 2: "CHANGE_STATUS_HAPPENING", + 3: "CHANGE_STATUS_PROCESSING", + 4: "CHANGE_STATUS_DONE", + } + ChangeStatus_value = map[string]int32{ + "CHANGE_STATUS_UNSPECIFIED": 0, + "CHANGE_STATUS_DEFINING": 1, + "CHANGE_STATUS_HAPPENING": 2, + "CHANGE_STATUS_PROCESSING": 3, + "CHANGE_STATUS_DONE": 4, + } +) + +func (x ChangeStatus) Enum() *ChangeStatus { + p := new(ChangeStatus) + *p = x + return p +} + +func (x ChangeStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChangeStatus) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[7].Descriptor() +} + +func (ChangeStatus) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[7] +} + +func (x ChangeStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChangeStatus.Descriptor instead. +func (ChangeStatus) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{7} +} + +type StartChangeResponse_State int32 + +const ( + // No state has been specified + StartChangeResponse_STATE_UNSPECIFIED StartChangeResponse_State = 0 + // Snapshot is being taken + StartChangeResponse_STATE_TAKING_SNAPSHOT StartChangeResponse_State = 1 + // Snapshot is being saved + StartChangeResponse_STATE_SAVING_SNAPSHOT StartChangeResponse_State = 2 + // Everything is complete + StartChangeResponse_STATE_DONE StartChangeResponse_State = 3 +) + +// Enum value maps for StartChangeResponse_State. +var ( + StartChangeResponse_State_name = map[int32]string{ + 0: "STATE_UNSPECIFIED", + 1: "STATE_TAKING_SNAPSHOT", + 2: "STATE_SAVING_SNAPSHOT", + 3: "STATE_DONE", + } + StartChangeResponse_State_value = map[string]int32{ + "STATE_UNSPECIFIED": 0, + "STATE_TAKING_SNAPSHOT": 1, + "STATE_SAVING_SNAPSHOT": 2, + "STATE_DONE": 3, + } +) + +func (x StartChangeResponse_State) Enum() *StartChangeResponse_State { + p := new(StartChangeResponse_State) + *p = x + return p +} + +func (x StartChangeResponse_State) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StartChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[8].Descriptor() +} + +func (StartChangeResponse_State) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[8] +} + +func (x StartChangeResponse_State) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StartChangeResponse_State.Descriptor instead. +func (StartChangeResponse_State) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{84, 0} +} + +type EndChangeResponse_State int32 + +const ( + // No state has been specified + EndChangeResponse_STATE_UNSPECIFIED EndChangeResponse_State = 0 + // Snapshot is being taken + EndChangeResponse_STATE_TAKING_SNAPSHOT EndChangeResponse_State = 1 + // Snapshot is being saved + EndChangeResponse_STATE_SAVING_SNAPSHOT EndChangeResponse_State = 2 + // Everything is complete + EndChangeResponse_STATE_DONE EndChangeResponse_State = 3 +) + +// Enum value maps for EndChangeResponse_State. +var ( + EndChangeResponse_State_name = map[int32]string{ + 0: "STATE_UNSPECIFIED", + 1: "STATE_TAKING_SNAPSHOT", + 2: "STATE_SAVING_SNAPSHOT", + 3: "STATE_DONE", + } + EndChangeResponse_State_value = map[string]int32{ + "STATE_UNSPECIFIED": 0, + "STATE_TAKING_SNAPSHOT": 1, + "STATE_SAVING_SNAPSHOT": 2, + "STATE_DONE": 3, + } +) + +func (x EndChangeResponse_State) Enum() *EndChangeResponse_State { + p := new(EndChangeResponse_State) + *p = x + return p +} + +func (x EndChangeResponse_State) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EndChangeResponse_State) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[9].Descriptor() +} + +func (EndChangeResponse_State) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[9] +} + +func (x EndChangeResponse_State) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EndChangeResponse_State.Descriptor instead. +func (EndChangeResponse_State) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{86, 0} +} + +type Risk_Severity int32 + +const ( + Risk_SEVERITY_UNSPECIFIED Risk_Severity = 0 + Risk_SEVERITY_LOW Risk_Severity = 1 + Risk_SEVERITY_MEDIUM Risk_Severity = 2 + Risk_SEVERITY_HIGH Risk_Severity = 3 +) + +// Enum value maps for Risk_Severity. +var ( + Risk_Severity_name = map[int32]string{ + 0: "SEVERITY_UNSPECIFIED", + 1: "SEVERITY_LOW", + 2: "SEVERITY_MEDIUM", + 3: "SEVERITY_HIGH", + } + Risk_Severity_value = map[string]int32{ + "SEVERITY_UNSPECIFIED": 0, + "SEVERITY_LOW": 1, + "SEVERITY_MEDIUM": 2, + "SEVERITY_HIGH": 3, + } +) + +func (x Risk_Severity) Enum() *Risk_Severity { + p := new(Risk_Severity) + *p = x + return p +} + +func (x Risk_Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Risk_Severity) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[10].Descriptor() +} + +func (Risk_Severity) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[10] +} + +func (x Risk_Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Risk_Severity.Descriptor instead. +func (Risk_Severity) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{89, 0} +} + +type ChangeAnalysisStatus_Status int32 + +const ( + ChangeAnalysisStatus_STATUS_UNSPECIFIED ChangeAnalysisStatus_Status = 0 + ChangeAnalysisStatus_STATUS_INPROGRESS ChangeAnalysisStatus_Status = 1 + ChangeAnalysisStatus_STATUS_SKIPPED ChangeAnalysisStatus_Status = 2 + ChangeAnalysisStatus_STATUS_DONE ChangeAnalysisStatus_Status = 3 + ChangeAnalysisStatus_STATUS_ERROR ChangeAnalysisStatus_Status = 4 +) + +// Enum value maps for ChangeAnalysisStatus_Status. +var ( + ChangeAnalysisStatus_Status_name = map[int32]string{ + 0: "STATUS_UNSPECIFIED", + 1: "STATUS_INPROGRESS", + 2: "STATUS_SKIPPED", + 3: "STATUS_DONE", + 4: "STATUS_ERROR", + } + ChangeAnalysisStatus_Status_value = map[string]int32{ + "STATUS_UNSPECIFIED": 0, + "STATUS_INPROGRESS": 1, + "STATUS_SKIPPED": 2, + "STATUS_DONE": 3, + "STATUS_ERROR": 4, + } +) + +func (x ChangeAnalysisStatus_Status) Enum() *ChangeAnalysisStatus_Status { + p := new(ChangeAnalysisStatus_Status) + *p = x + return p +} + +func (x ChangeAnalysisStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChangeAnalysisStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_changes_proto_enumTypes[11].Descriptor() +} + +func (ChangeAnalysisStatus_Status) Type() protoreflect.EnumType { + return &file_changes_proto_enumTypes[11] +} + +func (x ChangeAnalysisStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChangeAnalysisStatus_Status.Descriptor instead. +func (ChangeAnalysisStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{90, 0} +} + +type LabelRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *LabelRuleMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *LabelRuleProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LabelRule) Reset() { + *x = LabelRule{} + mi := &file_changes_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LabelRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LabelRule) ProtoMessage() {} + +func (x *LabelRule) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[0] + 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 LabelRule.ProtoReflect.Descriptor instead. +func (*LabelRule) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{0} +} + +func (x *LabelRule) GetMetadata() *LabelRuleMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *LabelRule) GetProperties() *LabelRuleProperties { + if x != nil { + return x.Properties + } + return nil +} + +type LabelRuleMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The unique identifier for this rule, it is required + LabelRuleUUID []byte `protobuf:"bytes,1,opt,name=LabelRuleUUID,proto3" json:"LabelRuleUUID,omitempty"` + // The time that this rule was created, set to the current time when the rule is created + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + // The time the rule was last updated, set to the current time when the rule is created + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LabelRuleMetadata) Reset() { + *x = LabelRuleMetadata{} + mi := &file_changes_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LabelRuleMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LabelRuleMetadata) ProtoMessage() {} + +func (x *LabelRuleMetadata) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[1] + 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 LabelRuleMetadata.ProtoReflect.Descriptor instead. +func (*LabelRuleMetadata) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{1} +} + +func (x *LabelRuleMetadata) GetLabelRuleUUID() []byte { + if x != nil { + return x.LabelRuleUUID + } + return nil +} + +func (x *LabelRuleMetadata) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *LabelRuleMetadata) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type LabelRuleProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the rule, friendly for users, it is required + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The colour of the label, it is required + Colour string `protobuf:"bytes,2,opt,name=colour,proto3" json:"colour,omitempty"` + // The instructions for the rule, this is the logic that will be used to determine if the label should be applied to a change, it is required + Instructions string `protobuf:"bytes,3,opt,name=instructions,proto3" json:"instructions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LabelRuleProperties) Reset() { + *x = LabelRuleProperties{} + mi := &file_changes_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LabelRuleProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LabelRuleProperties) ProtoMessage() {} + +func (x *LabelRuleProperties) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[2] + 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 LabelRuleProperties.ProtoReflect.Descriptor instead. +func (*LabelRuleProperties) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{2} +} + +func (x *LabelRuleProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LabelRuleProperties) GetColour() string { + if x != nil { + return x.Colour + } + return "" +} + +func (x *LabelRuleProperties) GetInstructions() string { + if x != nil { + return x.Instructions + } + return "" +} + +type ListLabelRulesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListLabelRulesRequest) Reset() { + *x = ListLabelRulesRequest{} + mi := &file_changes_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLabelRulesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLabelRulesRequest) ProtoMessage() {} + +func (x *ListLabelRulesRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[3] + 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 ListLabelRulesRequest.ProtoReflect.Descriptor instead. +func (*ListLabelRulesRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{3} +} + +type ListLabelRulesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*LabelRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListLabelRulesResponse) Reset() { + *x = ListLabelRulesResponse{} + mi := &file_changes_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLabelRulesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLabelRulesResponse) ProtoMessage() {} + +func (x *ListLabelRulesResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[4] + 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 ListLabelRulesResponse.ProtoReflect.Descriptor instead. +func (*ListLabelRulesResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{4} +} + +func (x *ListLabelRulesResponse) GetRules() []*LabelRule { + if x != nil { + return x.Rules + } + return nil +} + +type CreateLabelRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *LabelRuleProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateLabelRuleRequest) Reset() { + *x = CreateLabelRuleRequest{} + mi := &file_changes_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateLabelRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateLabelRuleRequest) ProtoMessage() {} + +func (x *CreateLabelRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[5] + 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 CreateLabelRuleRequest.ProtoReflect.Descriptor instead. +func (*CreateLabelRuleRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateLabelRuleRequest) GetProperties() *LabelRuleProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateLabelRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateLabelRuleResponse) Reset() { + *x = CreateLabelRuleResponse{} + mi := &file_changes_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateLabelRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateLabelRuleResponse) ProtoMessage() {} + +func (x *CreateLabelRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[6] + 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 CreateLabelRuleResponse.ProtoReflect.Descriptor instead. +func (*CreateLabelRuleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateLabelRuleResponse) GetRule() *LabelRule { + if x != nil { + return x.Rule + } + return nil +} + +type GetLabelRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLabelRuleRequest) Reset() { + *x = GetLabelRuleRequest{} + mi := &file_changes_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLabelRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLabelRuleRequest) ProtoMessage() {} + +func (x *GetLabelRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[7] + 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 GetLabelRuleRequest.ProtoReflect.Descriptor instead. +func (*GetLabelRuleRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{7} +} + +func (x *GetLabelRuleRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type GetLabelRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLabelRuleResponse) Reset() { + *x = GetLabelRuleResponse{} + mi := &file_changes_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLabelRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLabelRuleResponse) ProtoMessage() {} + +func (x *GetLabelRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[8] + 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 GetLabelRuleResponse.ProtoReflect.Descriptor instead. +func (*GetLabelRuleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{8} +} + +func (x *GetLabelRuleResponse) GetRule() *LabelRule { + if x != nil { + return x.Rule + } + return nil +} + +type UpdateLabelRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + Properties *LabelRuleProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateLabelRuleRequest) Reset() { + *x = UpdateLabelRuleRequest{} + mi := &file_changes_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateLabelRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateLabelRuleRequest) ProtoMessage() {} + +func (x *UpdateLabelRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[9] + 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 UpdateLabelRuleRequest.ProtoReflect.Descriptor instead. +func (*UpdateLabelRuleRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateLabelRuleRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *UpdateLabelRuleRequest) GetProperties() *LabelRuleProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateLabelRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *LabelRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateLabelRuleResponse) Reset() { + *x = UpdateLabelRuleResponse{} + mi := &file_changes_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateLabelRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateLabelRuleResponse) ProtoMessage() {} + +func (x *UpdateLabelRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[10] + 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 UpdateLabelRuleResponse.ProtoReflect.Descriptor instead. +func (*UpdateLabelRuleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateLabelRuleResponse) GetRule() *LabelRule { + if x != nil { + return x.Rule + } + return nil +} + +type DeleteLabelRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteLabelRuleRequest) Reset() { + *x = DeleteLabelRuleRequest{} + mi := &file_changes_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteLabelRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteLabelRuleRequest) ProtoMessage() {} + +func (x *DeleteLabelRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[11] + 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 DeleteLabelRuleRequest.ProtoReflect.Descriptor instead. +func (*DeleteLabelRuleRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteLabelRuleRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type DeleteLabelRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteLabelRuleResponse) Reset() { + *x = DeleteLabelRuleResponse{} + mi := &file_changes_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteLabelRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteLabelRuleResponse) ProtoMessage() {} + +func (x *DeleteLabelRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[12] + 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 DeleteLabelRuleResponse.ProtoReflect.Descriptor instead. +func (*DeleteLabelRuleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{12} +} + +type TestLabelRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *LabelRuleProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + ChangeUUID [][]byte `protobuf:"bytes,2,rep,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestLabelRuleRequest) Reset() { + *x = TestLabelRuleRequest{} + mi := &file_changes_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestLabelRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestLabelRuleRequest) ProtoMessage() {} + +func (x *TestLabelRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestLabelRuleRequest.ProtoReflect.Descriptor instead. +func (*TestLabelRuleRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{13} +} + +func (x *TestLabelRuleRequest) GetProperties() *LabelRuleProperties { + if x != nil { + return x.Properties + } + return nil +} + +func (x *TestLabelRuleRequest) GetChangeUUID() [][]byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type TestLabelRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + Applied bool `protobuf:"varint,2,opt,name=applied,proto3" json:"applied,omitempty"` + Label *Label `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestLabelRuleResponse) Reset() { + *x = TestLabelRuleResponse{} + mi := &file_changes_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestLabelRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestLabelRuleResponse) ProtoMessage() {} + +func (x *TestLabelRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestLabelRuleResponse.ProtoReflect.Descriptor instead. +func (*TestLabelRuleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{14} +} + +func (x *TestLabelRuleResponse) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +func (x *TestLabelRuleResponse) GetApplied() bool { + if x != nil { + return x.Applied + } + return false +} + +func (x *TestLabelRuleResponse) GetLabel() *Label { + if x != nil { + return x.Label + } + return nil +} + +type ReapplyLabelRuleInTimeRangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + StartAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=startAt,proto3" json:"startAt,omitempty"` + EndAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=endAt,proto3" json:"endAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReapplyLabelRuleInTimeRangeRequest) Reset() { + *x = ReapplyLabelRuleInTimeRangeRequest{} + mi := &file_changes_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReapplyLabelRuleInTimeRangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReapplyLabelRuleInTimeRangeRequest) ProtoMessage() {} + +func (x *ReapplyLabelRuleInTimeRangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[15] + 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 ReapplyLabelRuleInTimeRangeRequest.ProtoReflect.Descriptor instead. +func (*ReapplyLabelRuleInTimeRangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{15} +} + +func (x *ReapplyLabelRuleInTimeRangeRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *ReapplyLabelRuleInTimeRangeRequest) GetStartAt() *timestamppb.Timestamp { + if x != nil { + return x.StartAt + } + return nil +} + +func (x *ReapplyLabelRuleInTimeRangeRequest) GetEndAt() *timestamppb.Timestamp { + if x != nil { + return x.EndAt + } + return nil +} + +type ReapplyLabelRuleInTimeRangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID [][]byte `protobuf:"bytes,1,rep,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReapplyLabelRuleInTimeRangeResponse) Reset() { + *x = ReapplyLabelRuleInTimeRangeResponse{} + mi := &file_changes_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReapplyLabelRuleInTimeRangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReapplyLabelRuleInTimeRangeResponse) ProtoMessage() {} + +func (x *ReapplyLabelRuleInTimeRangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[16] + 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 ReapplyLabelRuleInTimeRangeResponse.ProtoReflect.Descriptor instead. +func (*ReapplyLabelRuleInTimeRangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{16} +} + +func (x *ReapplyLabelRuleInTimeRangeResponse) GetChangeUUID() [][]byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetHypothesesDetailsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHypothesesDetailsRequest) Reset() { + *x = GetHypothesesDetailsRequest{} + mi := &file_changes_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHypothesesDetailsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHypothesesDetailsRequest) ProtoMessage() {} + +func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHypothesesDetailsRequest.ProtoReflect.Descriptor instead. +func (*GetHypothesesDetailsRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{17} +} + +func (x *GetHypothesesDetailsRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetHypothesesDetailsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hypotheses []*HypothesesDetails `protobuf:"bytes,1,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHypothesesDetailsResponse) Reset() { + *x = GetHypothesesDetailsResponse{} + mi := &file_changes_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHypothesesDetailsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHypothesesDetailsResponse) ProtoMessage() {} + +func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHypothesesDetailsResponse.ProtoReflect.Descriptor instead. +func (*GetHypothesesDetailsResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{18} +} + +func (x *GetHypothesesDetailsResponse) GetHypotheses() []*HypothesesDetails { + if x != nil { + return x.Hypotheses + } + return nil +} + +type HypothesesDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The title of the hypothesis + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + // The number of observations that were combined to form the hypothesis + NumObservations uint32 `protobuf:"varint,2,opt,name=numObservations,proto3" json:"numObservations,omitempty"` + // The detail of the hypothesis + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` + // The status of the hypothesis + Status HypothesisStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` + // The results of the investigation of the hypothesis + InvestigationResults string `protobuf:"bytes,5,opt,name=investigationResults,proto3" json:"investigationResults,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HypothesesDetails) Reset() { + *x = HypothesesDetails{} + mi := &file_changes_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HypothesesDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HypothesesDetails) ProtoMessage() {} + +func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { + mi := &file_changes_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 HypothesesDetails.ProtoReflect.Descriptor instead. +func (*HypothesesDetails) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{19} +} + +func (x *HypothesesDetails) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *HypothesesDetails) GetNumObservations() uint32 { + if x != nil { + return x.NumObservations + } + return 0 +} + +func (x *HypothesesDetails) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +func (x *HypothesesDetails) GetStatus() HypothesisStatus { + if x != nil { + return x.Status + } + return HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED +} + +func (x *HypothesesDetails) GetInvestigationResults() string { + if x != nil { + return x.InvestigationResults + } + return "" +} + +type GetChangeTimelineV2Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeTimelineV2Request) Reset() { + *x = GetChangeTimelineV2Request{} + mi := &file_changes_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeTimelineV2Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeTimelineV2Request) ProtoMessage() {} + +func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[20] + 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 GetChangeTimelineV2Request.ProtoReflect.Descriptor instead. +func (*GetChangeTimelineV2Request) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{20} +} + +func (x *GetChangeTimelineV2Request) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetChangeTimelineV2Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The entries of this timeline, in chronological order (oldest first) + Entries []*ChangeTimelineEntryV2 `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeTimelineV2Response) Reset() { + *x = GetChangeTimelineV2Response{} + mi := &file_changes_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeTimelineV2Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeTimelineV2Response) ProtoMessage() {} + +func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[21] + 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 GetChangeTimelineV2Response.ProtoReflect.Descriptor instead. +func (*GetChangeTimelineV2Response) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{21} +} + +func (x *GetChangeTimelineV2Response) GetEntries() []*ChangeTimelineEntryV2 { + if x != nil { + return x.Entries + } + return nil +} + +// Contains all the information about a step in the Change Analysis workflow +// to show the user the historical, current and future of this Change. +// to show the user the historical, current and future. +type ChangeTimelineEntryV2 struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of this step, this will be shown to the user + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The little icon that will be shown for the timeline entry to indicate + // it's status + Status ChangeTimelineEntryStatus `protobuf:"varint,2,opt,name=status,proto3,enum=changes.ChangeTimelineEntryStatus" json:"status,omitempty"` + // The time that this step started, this will be used to calculate the + // duration this step. If `startedAt` is set, but `endedAt` is not, then the + // step is currently in progress. If `startedAt` is not set, the step is still + // pending. + StartedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=startedAt,proto3,oneof" json:"startedAt,omitempty"` + // When this step ended, this allows us to calculate how long it took, and + // allows users to see when certain things happened when looking back. If + // `endedAt` is set but `startedAt` is not, or `endedAt` is before `startedAt` + // then the step will be considered done, but we will be unable to calculate + // the duration. If `startedAt` and `endedAt` are the same timestamp, or only + // `endedAt` is populated, this entry does not have a duration, but should be + // interpreted as a point-in-time event. + EndedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=endedAt,proto3,oneof" json:"endedAt,omitempty"` + // Who performed this step, this will be shown to the user + Actor *string `protobuf:"bytes,5,opt,name=actor,proto3,oneof" json:"actor,omitempty"` + // The actual content of this step. This will be displayed to the user + // within the timeline and rendered differently depending on the type + // + // Types that are valid to be assigned to Content: + // + // *ChangeTimelineEntryV2_MappedItems + // *ChangeTimelineEntryV2_CalculatedBlastRadius + // *ChangeTimelineEntryV2_CalculatedRisks + // *ChangeTimelineEntryV2_Error + // *ChangeTimelineEntryV2_StatusMessage + // *ChangeTimelineEntryV2_Empty + // *ChangeTimelineEntryV2_ChangeValidation + // *ChangeTimelineEntryV2_CalculatedLabels + // *ChangeTimelineEntryV2_FormHypotheses + // *ChangeTimelineEntryV2_InvestigateHypotheses + // *ChangeTimelineEntryV2_RecordObservations + Content isChangeTimelineEntryV2_Content `protobuf_oneof:"content"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeTimelineEntryV2) Reset() { + *x = ChangeTimelineEntryV2{} + mi := &file_changes_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeTimelineEntryV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeTimelineEntryV2) ProtoMessage() {} + +func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[22] + 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 ChangeTimelineEntryV2.ProtoReflect.Descriptor instead. +func (*ChangeTimelineEntryV2) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{22} +} + +func (x *ChangeTimelineEntryV2) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ChangeTimelineEntryV2) GetStatus() ChangeTimelineEntryStatus { + if x != nil { + return x.Status + } + return ChangeTimelineEntryStatus_UNSPECIFIED +} + +func (x *ChangeTimelineEntryV2) GetStartedAt() *timestamppb.Timestamp { + if x != nil { + return x.StartedAt + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetEndedAt() *timestamppb.Timestamp { + if x != nil { + return x.EndedAt + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetActor() string { + if x != nil && x.Actor != nil { + return *x.Actor + } + return "" +} + +func (x *ChangeTimelineEntryV2) GetContent() isChangeTimelineEntryV2_Content { + if x != nil { + return x.Content + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetMappedItems() *MappedItemsTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_MappedItems); ok { + return x.MappedItems + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetCalculatedBlastRadius() *CalculatedBlastRadiusTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedBlastRadius); ok { + return x.CalculatedBlastRadius + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetCalculatedRisks() *CalculatedRisksTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedRisks); ok { + return x.CalculatedRisks + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetError() string { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_Error); ok { + return x.Error + } + } + return "" +} + +func (x *ChangeTimelineEntryV2) GetStatusMessage() string { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_StatusMessage); ok { + return x.StatusMessage + } + } + return "" +} + +func (x *ChangeTimelineEntryV2) GetEmpty() *EmptyContent { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_Empty); ok { + return x.Empty + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetChangeValidation() *ChangeValidationTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_ChangeValidation); ok { + return x.ChangeValidation + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetCalculatedLabels() *CalculatedLabelsTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_CalculatedLabels); ok { + return x.CalculatedLabels + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetFormHypotheses() *FormHypothesesTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_FormHypotheses); ok { + return x.FormHypotheses + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetInvestigateHypotheses() *InvestigateHypothesesTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_InvestigateHypotheses); ok { + return x.InvestigateHypotheses + } + } + return nil +} + +func (x *ChangeTimelineEntryV2) GetRecordObservations() *RecordObservationsTimelineEntry { + if x != nil { + if x, ok := x.Content.(*ChangeTimelineEntryV2_RecordObservations); ok { + return x.RecordObservations + } + } + return nil +} + +type isChangeTimelineEntryV2_Content interface { + isChangeTimelineEntryV2_Content() +} + +type ChangeTimelineEntryV2_MappedItems struct { + // Shows the mapping results so that user can see why items didn't map + // successfully + MappedItems *MappedItemsTimelineEntry `protobuf:"bytes,7,opt,name=mappedItems,proto3,oneof"` +} + +type ChangeTimelineEntryV2_CalculatedBlastRadius struct { + // The number of items in the blast radius + CalculatedBlastRadius *CalculatedBlastRadiusTimelineEntry `protobuf:"bytes,8,opt,name=calculatedBlastRadius,proto3,oneof"` +} + +type ChangeTimelineEntryV2_CalculatedRisks struct { + // The list of risks + CalculatedRisks *CalculatedRisksTimelineEntry `protobuf:"bytes,9,opt,name=calculatedRisks,proto3,oneof"` +} + +type ChangeTimelineEntryV2_Error struct { + // An error that will be shown to the user + Error string `protobuf:"bytes,11,opt,name=error,proto3,oneof"` +} + +type ChangeTimelineEntryV2_StatusMessage struct { + // A generic message that will be rendered as a paragraph + StatusMessage string `protobuf:"bytes,12,opt,name=statusMessage,proto3,oneof"` +} + +type ChangeTimelineEntryV2_Empty struct { + // A message that will be shown to the user, but will not have any content + // associated with it. Examples of this include "Change Created", "Change + // Started" etc. + Empty *EmptyContent `protobuf:"bytes,13,opt,name=empty,proto3,oneof"` +} + +type ChangeTimelineEntryV2_ChangeValidation struct { + // A list of validation steps that should be performed on the change + ChangeValidation *ChangeValidationTimelineEntry `protobuf:"bytes,14,opt,name=changeValidation,proto3,oneof"` +} + +type ChangeTimelineEntryV2_CalculatedLabels struct { + // The list of labels that have been calculated for this change, or that were assigned by a user + CalculatedLabels *CalculatedLabelsTimelineEntry `protobuf:"bytes,15,opt,name=calculatedLabels,proto3,oneof"` +} + +type ChangeTimelineEntryV2_FormHypotheses struct { + // The list of hypotheses that have been formed + FormHypotheses *FormHypothesesTimelineEntry `protobuf:"bytes,16,opt,name=formHypotheses,proto3,oneof"` +} + +type ChangeTimelineEntryV2_InvestigateHypotheses struct { + // The list of hypotheses that have been investigated + InvestigateHypotheses *InvestigateHypothesesTimelineEntry `protobuf:"bytes,17,opt,name=investigateHypotheses,proto3,oneof"` +} + +type ChangeTimelineEntryV2_RecordObservations struct { + // The number of observations that were found as part of calculating the blast + // radius + RecordObservations *RecordObservationsTimelineEntry `protobuf:"bytes,18,opt,name=recordObservations,proto3,oneof"` +} + +func (*ChangeTimelineEntryV2_MappedItems) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_CalculatedBlastRadius) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_CalculatedRisks) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_Error) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_StatusMessage) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_Empty) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_ChangeValidation) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_CalculatedLabels) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_FormHypotheses) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_InvestigateHypotheses) isChangeTimelineEntryV2_Content() {} + +func (*ChangeTimelineEntryV2_RecordObservations) isChangeTimelineEntryV2_Content() {} + +// This is a message that can be used to signal that a step in the timeline +// should be empty. This is useful for when we want to show a step in the +// timeline, but there is no content to show +type EmptyContent struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmptyContent) Reset() { + *x = EmptyContent{} + mi := &file_changes_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmptyContent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyContent) ProtoMessage() {} + +func (x *EmptyContent) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[23] + 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 EmptyContent.ProtoReflect.Descriptor instead. +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"` + // 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. + // + // 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[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MappedItemsTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MappedItemsTimelineEntry) ProtoMessage() {} + +func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[25] + 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 MappedItemsTimelineEntry.ProtoReflect.Descriptor instead. +func (*MappedItemsTimelineEntry) Descriptor() ([]byte, []int) { + 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 + } + 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"` + NumEdges uint32 `protobuf:"varint,2,opt,name=numEdges,proto3" json:"numEdges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CalculatedBlastRadiusTimelineEntry) Reset() { + *x = CalculatedBlastRadiusTimelineEntry{} + mi := &file_changes_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CalculatedBlastRadiusTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculatedBlastRadiusTimelineEntry) ProtoMessage() {} + +func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[26] + 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 CalculatedBlastRadiusTimelineEntry.ProtoReflect.Descriptor instead. +func (*CalculatedBlastRadiusTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{26} +} + +func (x *CalculatedBlastRadiusTimelineEntry) GetNumItems() uint32 { + if x != nil { + return x.NumItems + } + return 0 +} + +func (x *CalculatedBlastRadiusTimelineEntry) GetNumEdges() uint32 { + if x != nil { + return x.NumEdges + } + return 0 +} + +type RecordObservationsTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The number of observations that were found as part of calculating the blast + // radius + NumObservations uint32 `protobuf:"varint,1,opt,name=numObservations,proto3" json:"numObservations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RecordObservationsTimelineEntry) Reset() { + *x = RecordObservationsTimelineEntry{} + mi := &file_changes_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecordObservationsTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecordObservationsTimelineEntry) ProtoMessage() {} + +func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[27] + 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 RecordObservationsTimelineEntry.ProtoReflect.Descriptor instead. +func (*RecordObservationsTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{27} +} + +func (x *RecordObservationsTimelineEntry) GetNumObservations() uint32 { + if x != nil { + return x.NumObservations + } + return 0 +} + +// Timeline entry: Forming hypotheses by grouping observations +type FormHypothesesTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Total number of hypotheses formed + NumHypotheses uint32 `protobuf:"varint,1,opt,name=numHypotheses,proto3" json:"numHypotheses,omitempty"` + // The current state of the hypotheses under investigation + Hypotheses []*HypothesisSummary `protobuf:"bytes,2,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FormHypothesesTimelineEntry) Reset() { + *x = FormHypothesesTimelineEntry{} + mi := &file_changes_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FormHypothesesTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FormHypothesesTimelineEntry) ProtoMessage() {} + +func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[28] + 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 FormHypothesesTimelineEntry.ProtoReflect.Descriptor instead. +func (*FormHypothesesTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{28} +} + +func (x *FormHypothesesTimelineEntry) GetNumHypotheses() uint32 { + if x != nil { + return x.NumHypotheses + } + return 0 +} + +func (x *FormHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummary { + if x != nil { + return x.Hypotheses + } + return nil +} + +type InvestigateHypothesesTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Number of hypotheses that became real risks + NumProven uint32 `protobuf:"varint,1,opt,name=numProven,proto3" json:"numProven,omitempty"` + // Number of hypotheses that were disproven (verified safe) + NumDisproven uint32 `protobuf:"varint,2,opt,name=numDisproven,proto3" json:"numDisproven,omitempty"` + // Number of hypotheses that are still being investigated + NumInvestigating uint32 `protobuf:"varint,3,opt,name=numInvestigating,proto3" json:"numInvestigating,omitempty"` + // The current state of the hypotheses under investigation + Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvestigateHypothesesTimelineEntry) Reset() { + *x = InvestigateHypothesesTimelineEntry{} + mi := &file_changes_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvestigateHypothesesTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvestigateHypothesesTimelineEntry) ProtoMessage() {} + +func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[29] + 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 InvestigateHypothesesTimelineEntry.ProtoReflect.Descriptor instead. +func (*InvestigateHypothesesTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{29} +} + +func (x *InvestigateHypothesesTimelineEntry) GetNumProven() uint32 { + if x != nil { + return x.NumProven + } + return 0 +} + +func (x *InvestigateHypothesesTimelineEntry) GetNumDisproven() uint32 { + if x != nil { + return x.NumDisproven + } + return 0 +} + +func (x *InvestigateHypothesesTimelineEntry) GetNumInvestigating() uint32 { + if x != nil { + return x.NumInvestigating + } + return 0 +} + +func (x *InvestigateHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummary { + if x != nil { + return x.Hypotheses + } + return nil +} + +type HypothesisSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The status of the investigation + Status HypothesisStatus `protobuf:"varint,1,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` + // The title of the hypothesis + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // The current detail to show the user, While the hypothesis is being + // investigated, this could be the description of the hypothesis itself. And + // then once the investigation is finished, it could be the conclusion of the + // investigation. So it could update. This will be limited to two or three + // lines and truncated after that. + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HypothesisSummary) Reset() { + *x = HypothesisSummary{} + mi := &file_changes_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HypothesisSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HypothesisSummary) ProtoMessage() {} + +func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[30] + 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 HypothesisSummary.ProtoReflect.Descriptor instead. +func (*HypothesisSummary) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{30} +} + +func (x *HypothesisSummary) GetStatus() HypothesisStatus { + if x != nil { + return x.Status + } + return HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED +} + +func (x *HypothesisSummary) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *HypothesisSummary) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +type CalculatedRisksTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Risks []*Risk `protobuf:"bytes,1,rep,name=risks,proto3" json:"risks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CalculatedRisksTimelineEntry) Reset() { + *x = CalculatedRisksTimelineEntry{} + mi := &file_changes_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CalculatedRisksTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculatedRisksTimelineEntry) ProtoMessage() {} + +func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[31] + 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 CalculatedRisksTimelineEntry.ProtoReflect.Descriptor instead. +func (*CalculatedRisksTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{31} +} + +func (x *CalculatedRisksTimelineEntry) GetRisks() []*Risk { + if x != nil { + return x.Risks + } + return nil +} + +// The list of labels that have been calculated for this change, or that were assigned by a user +type CalculatedLabelsTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Labels []*Label `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CalculatedLabelsTimelineEntry) Reset() { + *x = CalculatedLabelsTimelineEntry{} + mi := &file_changes_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CalculatedLabelsTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CalculatedLabelsTimelineEntry) ProtoMessage() {} + +func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[32] + 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 CalculatedLabelsTimelineEntry.ProtoReflect.Descriptor instead. +func (*CalculatedLabelsTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{32} +} + +func (x *CalculatedLabelsTimelineEntry) GetLabels() []*Label { + if x != nil { + return x.Labels + } + return nil +} + +// The list of validation steps that are to be performed on the change +type ChangeValidationTimelineEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + // "A concise overview of the proposed changes and their potential impact (2-3 sentences) + BriefAnalysis string `protobuf:"bytes,1,opt,name=briefAnalysis,proto3" json:"briefAnalysis,omitempty"` + // For the first stage of the change validation implementation we are only returning Validation Checklist Category + ValidationChecklist []*ChangeValidationCategory `protobuf:"bytes,2,rep,name=validationChecklist,proto3" json:"validationChecklist,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeValidationTimelineEntry) Reset() { + *x = ChangeValidationTimelineEntry{} + mi := &file_changes_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeValidationTimelineEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeValidationTimelineEntry) ProtoMessage() {} + +func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[33] + 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 ChangeValidationTimelineEntry.ProtoReflect.Descriptor instead. +func (*ChangeValidationTimelineEntry) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{33} +} + +func (x *ChangeValidationTimelineEntry) GetBriefAnalysis() string { + if x != nil { + return x.BriefAnalysis + } + return "" +} + +func (x *ChangeValidationTimelineEntry) GetValidationChecklist() []*ChangeValidationCategory { + if x != nil { + return x.ValidationChecklist + } + return nil +} + +// Description with specific commands/API calls to execute (1-2 sentences).Add commentMore actions +// - Include exact AWS CLI commands with parameters and resource IDs +// - Focus on must-have verification steps only +type ChangeValidationCategory struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The category title (e.g., 'Security Validation', 'Configuration Verification') + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + // Description with specific AWS CLI commands/API calls to execute (1-2 sentences) + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeValidationCategory) Reset() { + *x = ChangeValidationCategory{} + mi := &file_changes_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeValidationCategory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeValidationCategory) ProtoMessage() {} + +func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[34] + 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 ChangeValidationCategory.ProtoReflect.Descriptor instead. +func (*ChangeValidationCategory) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{34} +} + +func (x *ChangeValidationCategory) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ChangeValidationCategory) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type GetDiffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDiffRequest) Reset() { + *x = GetDiffRequest{} + mi := &file_changes_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDiffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDiffRequest) ProtoMessage() {} + +func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[35] + 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 GetDiffRequest.ProtoReflect.Descriptor instead. +func (*GetDiffRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{35} +} + +func (x *GetDiffRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetDiffResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Items that were planned to be changed, and were changed + ExpectedItems []*ItemDiff `protobuf:"bytes,1,rep,name=expectedItems,proto3" json:"expectedItems,omitempty"` + // Items that were changed, but were not planned to be changed + UnexpectedItems []*ItemDiff `protobuf:"bytes,3,rep,name=unexpectedItems,proto3" json:"unexpectedItems,omitempty"` + Edges []*Edge `protobuf:"bytes,2,rep,name=edges,proto3" json:"edges,omitempty"` + // Items that were planned to be changed, but were not changed + MissingItems []*ItemDiff `protobuf:"bytes,4,rep,name=missingItems,proto3" json:"missingItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDiffResponse) Reset() { + *x = GetDiffResponse{} + mi := &file_changes_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDiffResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDiffResponse) ProtoMessage() {} + +func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[36] + 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 GetDiffResponse.ProtoReflect.Descriptor instead. +func (*GetDiffResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{36} +} + +func (x *GetDiffResponse) GetExpectedItems() []*ItemDiff { + if x != nil { + return x.ExpectedItems + } + return nil +} + +func (x *GetDiffResponse) GetUnexpectedItems() []*ItemDiff { + if x != nil { + return x.UnexpectedItems + } + return nil +} + +func (x *GetDiffResponse) GetEdges() []*Edge { + if x != nil { + return x.Edges + } + return nil +} + +func (x *GetDiffResponse) GetMissingItems() []*ItemDiff { + if x != nil { + return x.MissingItems + } + return nil +} + +type ListChangingItemsSummaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangingItemsSummaryRequest) Reset() { + *x = ListChangingItemsSummaryRequest{} + mi := &file_changes_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangingItemsSummaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangingItemsSummaryRequest) ProtoMessage() {} + +func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[37] + 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 ListChangingItemsSummaryRequest.ProtoReflect.Descriptor instead. +func (*ListChangingItemsSummaryRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{37} +} + +func (x *ListChangingItemsSummaryRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type ListChangingItemsSummaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*ItemDiffSummary `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangingItemsSummaryResponse) Reset() { + *x = ListChangingItemsSummaryResponse{} + mi := &file_changes_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangingItemsSummaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangingItemsSummaryResponse) ProtoMessage() {} + +func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[38] + 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 ListChangingItemsSummaryResponse.ProtoReflect.Descriptor instead. +func (*ListChangingItemsSummaryResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{38} +} + +func (x *ListChangingItemsSummaryResponse) GetItems() []*ItemDiffSummary { + if x != nil { + return x.Items + } + return nil +} + +type MappedItemDiff struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The item that is changing and any known changes to it + Item *ItemDiff `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // a mapping query that can be used to find the item. this can be empty if the + // submitter does not know how to map this item. + MappingQuery *Query `protobuf:"bytes,2,opt,name=mappingQuery,proto3,oneof" json:"mappingQuery,omitempty"` + // The error that was returned as part of the mapping process. This will be + // empty if the mapping was successful. + MappingError *QueryError `protobuf:"bytes,3,opt,name=mappingError,proto3,oneof" json:"mappingError,omitempty"` + // Explicit status from CLI - when set, API uses this instead of inferring. + // This allows CLI to distinguish between "unsupported resource type", + // "pending creation (doesn't exist yet)", and "actual mapping error". + MappingStatus *MappedItemMappingStatus `protobuf:"varint,4,opt,name=mapping_status,json=mappingStatus,proto3,enum=changes.MappedItemMappingStatus,oneof" json:"mapping_status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MappedItemDiff) Reset() { + *x = MappedItemDiff{} + mi := &file_changes_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MappedItemDiff) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MappedItemDiff) ProtoMessage() {} + +func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[39] + 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 MappedItemDiff.ProtoReflect.Descriptor instead. +func (*MappedItemDiff) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{39} +} + +func (x *MappedItemDiff) GetItem() *ItemDiff { + if x != nil { + return x.Item + } + return nil +} + +func (x *MappedItemDiff) GetMappingQuery() *Query { + if x != nil { + return x.MappingQuery + } + return nil +} + +func (x *MappedItemDiff) GetMappingError() *QueryError { + if x != nil { + return x.MappingError + } + return nil +} + +func (x *MappedItemDiff) GetMappingStatus() MappedItemMappingStatus { + if x != nil && x.MappingStatus != nil { + return *x.MappingStatus + } + return MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED +} + +// StartChangeAnalysisRequest is used to start the change analysis process. This +// will calculate various things blast radius, risks, auto-tagging etc. This +// it contains overrides for the auto-tagging rules and the blast radius config +type StartChangeAnalysisRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The change to update + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + // The changing items + ChangingItems []*MappedItemDiff `protobuf:"bytes,2,rep,name=changingItems,proto3" json:"changingItems,omitempty"` + // Overrides the stored blast radius config for this change + BlastRadiusConfigOverride *BlastRadiusConfig `protobuf:"bytes,3,opt,name=blastRadiusConfigOverride,proto3,oneof" json:"blastRadiusConfigOverride,omitempty"` + // The routine config that should be used for this change. If this is empty + // the routine config that has been configured in the UI will be used + RoutineChangesConfigOverride *RoutineChangesConfig `protobuf:"bytes,5,opt,name=routineChangesConfigOverride,proto3,oneof" json:"routineChangesConfigOverride,omitempty"` + // github organisation profile to use for this change + GithubOrganisationProfileOverride *GithubOrganisationProfile `protobuf:"bytes,6,opt,name=githubOrganisationProfileOverride,proto3,oneof" json:"githubOrganisationProfileOverride,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartChangeAnalysisRequest) Reset() { + *x = StartChangeAnalysisRequest{} + mi := &file_changes_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartChangeAnalysisRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartChangeAnalysisRequest) ProtoMessage() {} + +func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[40] + 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 StartChangeAnalysisRequest.ProtoReflect.Descriptor instead. +func (*StartChangeAnalysisRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{40} +} + +func (x *StartChangeAnalysisRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +func (x *StartChangeAnalysisRequest) GetChangingItems() []*MappedItemDiff { + if x != nil { + return x.ChangingItems + } + return nil +} + +func (x *StartChangeAnalysisRequest) GetBlastRadiusConfigOverride() *BlastRadiusConfig { + if x != nil { + return x.BlastRadiusConfigOverride + } + return nil +} + +func (x *StartChangeAnalysisRequest) GetRoutineChangesConfigOverride() *RoutineChangesConfig { + if x != nil { + return x.RoutineChangesConfigOverride + } + return nil +} + +func (x *StartChangeAnalysisRequest) GetGithubOrganisationProfileOverride() *GithubOrganisationProfile { + if x != nil { + return x.GithubOrganisationProfileOverride + } + return nil +} + +// StartChangeAnalysisResponse is used to signal that the change analysis has been successfully started +// we use HTTP response codes to signal errors +type StartChangeAnalysisResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartChangeAnalysisResponse) Reset() { + *x = StartChangeAnalysisResponse{} + mi := &file_changes_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartChangeAnalysisResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartChangeAnalysisResponse) ProtoMessage() {} + +func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[41] + 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 StartChangeAnalysisResponse.ProtoReflect.Descriptor instead. +func (*StartChangeAnalysisResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{41} +} + +type ListHomeChangesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pagination *PaginationRequest `protobuf:"bytes,1,opt,name=pagination,proto3" json:"pagination,omitempty"` + Filters *ChangeFiltersRequest `protobuf:"bytes,2,opt,name=filters,proto3,oneof" json:"filters,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHomeChangesRequest) Reset() { + *x = ListHomeChangesRequest{} + mi := &file_changes_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHomeChangesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHomeChangesRequest) ProtoMessage() {} + +func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[42] + 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 ListHomeChangesRequest.ProtoReflect.Descriptor instead. +func (*ListHomeChangesRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{42} +} + +func (x *ListHomeChangesRequest) GetPagination() *PaginationRequest { + if x != nil { + return x.Pagination + } + return nil +} + +func (x *ListHomeChangesRequest) GetFilters() *ChangeFiltersRequest { + if x != nil { + return x.Filters + } + return nil +} + +// ChangeFiltersRequest is used for filtering on the changes page. +// Repeated entries of the same type are used to represent OR conditions. eg if repo is ["a", "b"] then the filter is (repo == "a" OR repo == "b") +// The filters are ANDed together. eg if repo is ["a", "b"] and author is ["c"] then the filter is (repo == "a" OR repo == "b") AND author == "c" +type ChangeFiltersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Repos []string `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` + Risks []Risk_Severity `protobuf:"varint,3,rep,packed,name=risks,proto3,enum=changes.Risk_Severity" json:"risks,omitempty"` + Authors []string `protobuf:"bytes,4,rep,name=authors,proto3" json:"authors,omitempty"` + Statuses []ChangeStatus `protobuf:"varint,5,rep,packed,name=statuses,proto3,enum=changes.ChangeStatus" json:"statuses,omitempty"` + SortOrder *SortOrder `protobuf:"varint,6,opt,name=sortOrder,proto3,enum=SortOrder,oneof" json:"sortOrder,omitempty"` // the default is SortOrder.DATE_DESCENDING (newest first) + Labels []string `protobuf:"bytes,7,rep,name=labels,proto3" json:"labels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeFiltersRequest) Reset() { + *x = ChangeFiltersRequest{} + mi := &file_changes_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeFiltersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeFiltersRequest) ProtoMessage() {} + +func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[43] + 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 ChangeFiltersRequest.ProtoReflect.Descriptor instead. +func (*ChangeFiltersRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{43} +} + +func (x *ChangeFiltersRequest) GetRepos() []string { + if x != nil { + return x.Repos + } + return nil +} + +func (x *ChangeFiltersRequest) GetRisks() []Risk_Severity { + if x != nil { + return x.Risks + } + return nil +} + +func (x *ChangeFiltersRequest) GetAuthors() []string { + if x != nil { + return x.Authors + } + return nil +} + +func (x *ChangeFiltersRequest) GetStatuses() []ChangeStatus { + if x != nil { + return x.Statuses + } + return nil +} + +func (x *ChangeFiltersRequest) GetSortOrder() SortOrder { + if x != nil && x.SortOrder != nil { + return *x.SortOrder + } + return SortOrder_ALPHABETICAL_ASCENDING +} + +func (x *ChangeFiltersRequest) GetLabels() []string { + if x != nil { + return x.Labels + } + return nil +} + +type ListHomeChangesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Changes []*ChangeSummary `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` + Pagination *PaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHomeChangesResponse) Reset() { + *x = ListHomeChangesResponse{} + mi := &file_changes_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHomeChangesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHomeChangesResponse) ProtoMessage() {} + +func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[44] + 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 ListHomeChangesResponse.ProtoReflect.Descriptor instead. +func (*ListHomeChangesResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{44} +} + +func (x *ListHomeChangesResponse) GetChanges() []*ChangeSummary { + if x != nil { + return x.Changes + } + return nil +} + +func (x *ListHomeChangesResponse) GetPagination() *PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +type PopulateChangeFiltersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PopulateChangeFiltersRequest) Reset() { + *x = PopulateChangeFiltersRequest{} + mi := &file_changes_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PopulateChangeFiltersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PopulateChangeFiltersRequest) ProtoMessage() {} + +func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[45] + 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 PopulateChangeFiltersRequest.ProtoReflect.Descriptor instead. +func (*PopulateChangeFiltersRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{45} +} + +type PopulateChangeFiltersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Repos []string `protobuf:"bytes,1,rep,name=repos,proto3" json:"repos,omitempty"` + Authors []string `protobuf:"bytes,2,rep,name=authors,proto3" json:"authors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PopulateChangeFiltersResponse) Reset() { + *x = PopulateChangeFiltersResponse{} + mi := &file_changes_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PopulateChangeFiltersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PopulateChangeFiltersResponse) ProtoMessage() {} + +func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[46] + 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 PopulateChangeFiltersResponse.ProtoReflect.Descriptor instead. +func (*PopulateChangeFiltersResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{46} +} + +func (x *PopulateChangeFiltersResponse) GetRepos() []string { + if x != nil { + return x.Repos + } + return nil +} + +func (x *PopulateChangeFiltersResponse) GetAuthors() []string { + if x != nil { + return x.Authors + } + return nil +} + +type ItemDiffSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A reference to the item that this diff is related to + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // The status of the item + Status ItemDiffStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` + // The health of the item currently (as opposed to before the change) + HealthAfter Health `protobuf:"varint,5,opt,name=healthAfter,proto3,enum=Health" json:"healthAfter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItemDiffSummary) Reset() { + *x = ItemDiffSummary{} + mi := &file_changes_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItemDiffSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemDiffSummary) ProtoMessage() {} + +func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[47] + 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 ItemDiffSummary.ProtoReflect.Descriptor instead. +func (*ItemDiffSummary) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{47} +} + +func (x *ItemDiffSummary) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *ItemDiffSummary) GetStatus() ItemDiffStatus { + if x != nil { + return x.Status + } + return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED +} + +func (x *ItemDiffSummary) GetHealthAfter() Health { + if x != nil { + return x.HealthAfter + } + return Health_HEALTH_UNKNOWN +} + +type ItemDiff struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A reference to the item that this diff is related to, if this exists in the + // real infrastructure. If this is blank it represents a change that we were + // unable to find a matching item for + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3,oneof" json:"item,omitempty"` + // The status of the item + Status ItemDiffStatus `protobuf:"varint,2,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` + Before *Item `protobuf:"bytes,3,opt,name=before,proto3" json:"before,omitempty"` + After *Item `protobuf:"bytes,4,opt,name=after,proto3" json:"after,omitempty"` + // A summary of how often the GUN's have had similar changes for individual attributes along with planned and unplanned changes + ModificationSummary string `protobuf:"bytes,5,opt,name=modificationSummary,proto3" json:"modificationSummary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItemDiff) Reset() { + *x = ItemDiff{} + mi := &file_changes_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItemDiff) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemDiff) ProtoMessage() {} + +func (x *ItemDiff) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[48] + 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 ItemDiff.ProtoReflect.Descriptor instead. +func (*ItemDiff) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{48} +} + +func (x *ItemDiff) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *ItemDiff) GetStatus() ItemDiffStatus { + if x != nil { + return x.Status + } + return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED +} + +func (x *ItemDiff) GetBefore() *Item { + if x != nil { + return x.Before + } + return nil +} + +func (x *ItemDiff) GetAfter() *Item { + if x != nil { + return x.After + } + return nil +} + +func (x *ItemDiff) GetModificationSummary() string { + if x != nil { + return x.ModificationSummary + } + return "" +} + +type EnrichedTags struct { + state protoimpl.MessageState `protogen:"open.v1"` + TagValue map[string]*TagValue `protobuf:"bytes,18,rep,name=tagValue,proto3" json:"tagValue,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnrichedTags) Reset() { + *x = EnrichedTags{} + mi := &file_changes_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnrichedTags) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnrichedTags) ProtoMessage() {} + +func (x *EnrichedTags) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[49] + 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 EnrichedTags.ProtoReflect.Descriptor instead. +func (*EnrichedTags) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{49} +} + +func (x *EnrichedTags) GetTagValue() map[string]*TagValue { + if x != nil { + return x.TagValue + } + return nil +} + +type TagValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The value of the tag, this can be user-defined or auto-generated + // + // Types that are valid to be assigned to Value: + // + // *TagValue_UserTagValue + // *TagValue_AutoTagValue + Value isTagValue_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagValue) Reset() { + *x = TagValue{} + mi := &file_changes_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagValue) ProtoMessage() {} + +func (x *TagValue) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[50] + 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 TagValue.ProtoReflect.Descriptor instead. +func (*TagValue) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{50} +} + +func (x *TagValue) GetValue() isTagValue_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *TagValue) GetUserTagValue() *UserTagValue { + if x != nil { + if x, ok := x.Value.(*TagValue_UserTagValue); ok { + return x.UserTagValue + } + } + return nil +} + +func (x *TagValue) GetAutoTagValue() *AutoTagValue { + if x != nil { + if x, ok := x.Value.(*TagValue_AutoTagValue); ok { + return x.AutoTagValue + } + } + return nil +} + +type isTagValue_Value interface { + isTagValue_Value() +} + +type TagValue_UserTagValue struct { + UserTagValue *UserTagValue `protobuf:"bytes,1,opt,name=userTagValue,proto3,oneof"` +} + +type TagValue_AutoTagValue struct { + AutoTagValue *AutoTagValue `protobuf:"bytes,2,opt,name=autoTagValue,proto3,oneof"` +} + +func (*TagValue_UserTagValue) isTagValue_Value() {} + +func (*TagValue_AutoTagValue) isTagValue_Value() {} + +type UserTagValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The value of the tag that was set by the user. + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserTagValue) Reset() { + *x = UserTagValue{} + mi := &file_changes_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserTagValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserTagValue) ProtoMessage() {} + +func (x *UserTagValue) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[51] + 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 UserTagValue.ProtoReflect.Descriptor instead. +func (*UserTagValue) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{51} +} + +func (x *UserTagValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type AutoTagValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The value of the tag + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + // Reasoning for this decision + Reasoning string `protobuf:"bytes,2,opt,name=reasoning,proto3" json:"reasoning,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AutoTagValue) Reset() { + *x = AutoTagValue{} + mi := &file_changes_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AutoTagValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AutoTagValue) ProtoMessage() {} + +func (x *AutoTagValue) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[52] + 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 AutoTagValue.ProtoReflect.Descriptor instead. +func (*AutoTagValue) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{52} +} + +func (x *AutoTagValue) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *AutoTagValue) GetReasoning() string { + if x != nil { + return x.Reasoning + } + return "" +} + +// a label that can be applied to a change +// note that it keeps the colour / name based on the rule that was used at the time of creation +// if the rule is updated, the colour / name will not be updated, unless +// 1. the change is re-run, in which case the label will be updated to the new colour / name. +// 2. the user will have to manually re-run the rule to get the new colour / name. this may also remove the label from the change if the rule is no longer applied. +type Label struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of the label, it is required + Type LabelType `protobuf:"varint,1,opt,name=type,proto3,enum=changes.LabelType" json:"type,omitempty"` + // name of the label, it is required + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // colour of the label + // discussed with the UI team and we will use a hex code for the colour, it is required + Colour string `protobuf:"bytes,3,opt,name=colour,proto3" json:"colour,omitempty"` + // the label rule that was used to generate this label, this is only populated for auto-generated labels + LabelRuleUUID []byte `protobuf:"bytes,4,opt,name=labelRuleUUID,proto3" json:"labelRuleUUID,omitempty"` + // reasoning for this label, this is only populated for auto-generated labels + AutoLabelReasoning string `protobuf:"bytes,5,opt,name=autoLabelReasoning,proto3" json:"autoLabelReasoning,omitempty"` + // skipped if the label rule was not applied to the change, this is only populated for auto-generated labels + Skipped bool `protobuf:"varint,6,opt,name=skipped,proto3" json:"skipped,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Label) Reset() { + *x = Label{} + mi := &file_changes_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Label) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Label) ProtoMessage() {} + +func (x *Label) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[53] + 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 Label.ProtoReflect.Descriptor instead. +func (*Label) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{53} +} + +func (x *Label) GetType() LabelType { + if x != nil { + return x.Type + } + return LabelType_LABEL_TYPE_UNSPECIFIED +} + +func (x *Label) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Label) GetColour() string { + if x != nil { + return x.Colour + } + return "" +} + +func (x *Label) GetLabelRuleUUID() []byte { + if x != nil { + return x.LabelRuleUUID + } + return nil +} + +func (x *Label) GetAutoLabelReasoning() string { + if x != nil { + return x.AutoLabelReasoning + } + return "" +} + +func (x *Label) GetSkipped() bool { + if x != nil { + return x.Skipped + } + return false +} + +// A smaller summary of a change +type ChangeSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id to identify this change + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Short title for this change. + // Example: "database upgrade" + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // The current status of this change. This is changed by the lifecycle + // functions such as `StartChange` and `EndChange`. + Status ChangeStatus `protobuf:"varint,3,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` + // Link to the ticket for this change. + // Example: "http://jira.contoso-engineering.com/browse/CM-1337" + TicketLink string `protobuf:"bytes,4,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` + // timestamp when this change was created + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + // The name of the user that created the change + CreatorName string `protobuf:"bytes,6,opt,name=creatorName,proto3" json:"creatorName,omitempty"` + // The email of the user that created the change + CreatorEmail string `protobuf:"bytes,15,opt,name=creatorEmail,proto3" json:"creatorEmail,omitempty"` + // The number of items in the blast radius of this change + NumAffectedItems int32 `protobuf:"varint,9,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` + // The number of edges in the blast radius of this change + NumAffectedEdges int32 `protobuf:"varint,10,opt,name=numAffectedEdges,proto3" json:"numAffectedEdges,omitempty"` + // The number of low risks in this change + NumLowRisk int32 `protobuf:"varint,11,opt,name=numLowRisk,proto3" json:"numLowRisk,omitempty"` + // The number of medium risks in this change + NumMediumRisk int32 `protobuf:"varint,12,opt,name=numMediumRisk,proto3" json:"numMediumRisk,omitempty"` + // The number of high risks in this change + NumHighRisk int32 `protobuf:"varint,13,opt,name=numHighRisk,proto3" json:"numHighRisk,omitempty"` + // Quick description of the change. + // Example: "upgrade of the database to get access to the new contoso management processor" + Description string `protobuf:"bytes,14,opt,name=description,proto3" json:"description,omitempty"` + // Repo information; can be an empty string. CLI attempts auto-population, but + // users can override. Not necessarily a URL. The UI will be responsible for + // any formatting/shortening/sprucing up should it be required. + Repo string `protobuf:"bytes,16,opt,name=repo,proto3" json:"repo,omitempty"` + // Deprecated: Use enrichedTags instead + // + // Deprecated: Marked as deprecated in changes.proto. + Tags map[string]string `protobuf:"bytes,17,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Tags associated with this change + EnrichedTags *EnrichedTags `protobuf:"bytes,18,opt,name=enrichedTags,proto3" json:"enrichedTags,omitempty"` + // labels + Labels []*Label `protobuf:"bytes,19,rep,name=labels,proto3" json:"labels,omitempty"` + // Github change information + // contains information about the author + GithubChangeInfo *GithubChangeInfo `protobuf:"bytes,20,opt,name=githubChangeInfo,proto3" json:"githubChangeInfo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeSummary) Reset() { + *x = ChangeSummary{} + mi := &file_changes_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeSummary) ProtoMessage() {} + +func (x *ChangeSummary) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[54] + 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 ChangeSummary.ProtoReflect.Descriptor instead. +func (*ChangeSummary) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{54} +} + +func (x *ChangeSummary) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *ChangeSummary) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ChangeSummary) GetStatus() ChangeStatus { + if x != nil { + return x.Status + } + return ChangeStatus_CHANGE_STATUS_UNSPECIFIED +} + +func (x *ChangeSummary) GetTicketLink() string { + if x != nil { + return x.TicketLink + } + return "" +} + +func (x *ChangeSummary) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ChangeSummary) GetCreatorName() string { + if x != nil { + return x.CreatorName + } + return "" +} + +func (x *ChangeSummary) GetCreatorEmail() string { + if x != nil { + return x.CreatorEmail + } + return "" +} + +func (x *ChangeSummary) GetNumAffectedItems() int32 { + if x != nil { + return x.NumAffectedItems + } + return 0 +} + +func (x *ChangeSummary) GetNumAffectedEdges() int32 { + if x != nil { + return x.NumAffectedEdges + } + return 0 +} + +func (x *ChangeSummary) GetNumLowRisk() int32 { + if x != nil { + return x.NumLowRisk + } + return 0 +} + +func (x *ChangeSummary) GetNumMediumRisk() int32 { + if x != nil { + return x.NumMediumRisk + } + return 0 +} + +func (x *ChangeSummary) GetNumHighRisk() int32 { + if x != nil { + return x.NumHighRisk + } + return 0 +} + +func (x *ChangeSummary) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ChangeSummary) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +// Deprecated: Marked as deprecated in changes.proto. +func (x *ChangeSummary) GetTags() map[string]string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *ChangeSummary) GetEnrichedTags() *EnrichedTags { + if x != nil { + return x.EnrichedTags + } + return nil +} + +func (x *ChangeSummary) GetLabels() []*Label { + if x != nil { + return x.Labels + } + return nil +} + +func (x *ChangeSummary) GetGithubChangeInfo() *GithubChangeInfo { + if x != nil { + return x.GithubChangeInfo + } + return nil +} + +// a complete Change with machine-supplied and user-supplied values +type Change struct { + state protoimpl.MessageState `protogen:"open.v1"` + // machine-generated metadata of this change + Metadata *ChangeMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // user-supplied properties of this change + Properties *ChangeProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Change) Reset() { + *x = Change{} + mi := &file_changes_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Change) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Change) ProtoMessage() {} + +func (x *Change) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[55] + 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 Change.ProtoReflect.Descriptor instead. +func (*Change) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{55} +} + +func (x *Change) GetMetadata() *ChangeMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Change) GetProperties() *ChangeProperties { + if x != nil { + return x.Properties + } + return nil +} + +// machine-generated metadata of this change +type ChangeMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id to identify this change + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // timestamp when this change was created + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + // timestamp when this change was last updated + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"` + // The current status of this change. This is changed by the lifecycle + // functions such as `StartChange` and `EndChange`. + Status ChangeStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` + // The name of the user that created the change + CreatorName string `protobuf:"bytes,5,opt,name=creatorName,proto3" json:"creatorName,omitempty"` + // The email of the user that created the change + CreatorEmail string `protobuf:"bytes,19,opt,name=creatorEmail,proto3" json:"creatorEmail,omitempty"` + // The number of items in the blast radius if this change + NumAffectedItems int32 `protobuf:"varint,7,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` + // The number of edges in the blast radius if this change + NumAffectedEdges int32 `protobuf:"varint,17,opt,name=numAffectedEdges,proto3" json:"numAffectedEdges,omitempty"` + // The number of items within the blast radius that were not affected by this + // change + NumUnchangedItems int32 `protobuf:"varint,8,opt,name=numUnchangedItems,proto3" json:"numUnchangedItems,omitempty"` + // The number of items that were created as part of this change + NumCreatedItems int32 `protobuf:"varint,9,opt,name=numCreatedItems,proto3" json:"numCreatedItems,omitempty"` + // The number of items that were updated as part of this change + NumUpdatedItems int32 `protobuf:"varint,10,opt,name=numUpdatedItems,proto3" json:"numUpdatedItems,omitempty"` + // The number of items that were replaced as part of this change + NumReplacedItems int32 `protobuf:"varint,18,opt,name=numReplacedItems,proto3" json:"numReplacedItems,omitempty"` + // The number of items that were deleted as part of this change + NumDeletedItems int32 `protobuf:"varint,11,opt,name=numDeletedItems,proto3" json:"numDeletedItems,omitempty"` + UnknownHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,12,opt,name=UnknownHealthChange,proto3" json:"UnknownHealthChange,omitempty"` + OkHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,13,opt,name=OkHealthChange,proto3" json:"OkHealthChange,omitempty"` + WarningHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,14,opt,name=WarningHealthChange,proto3" json:"WarningHealthChange,omitempty"` + ErrorHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,15,opt,name=ErrorHealthChange,proto3" json:"ErrorHealthChange,omitempty"` + PendingHealthChange *ChangeMetadata_HealthChange `protobuf:"bytes,16,opt,name=PendingHealthChange,proto3" json:"PendingHealthChange,omitempty"` + // Github change information + // contains information about the author from github + GithubChangeInfo *GithubChangeInfo `protobuf:"bytes,20,opt,name=githubChangeInfo,proto3" json:"githubChangeInfo,omitempty"` + // The total number of observations recorded for this change during blast radius analysis. + // This is null/undefined for legacy changes where observations were not tracked. + // This count increments immediately as observations are added, providing fast feedback. + TotalObservations *uint32 `protobuf:"varint,21,opt,name=total_observations,json=totalObservations,proto3,oneof" json:"total_observations,omitempty"` + // Persisted change analysis completion status (single source of truth for GetChange/CLI). + ChangeAnalysisStatus *ChangeAnalysisStatus `protobuf:"bytes,22,opt,name=changeAnalysisStatus,proto3" json:"changeAnalysisStatus,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeMetadata) Reset() { + *x = ChangeMetadata{} + mi := &file_changes_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeMetadata) ProtoMessage() {} + +func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[56] + 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 ChangeMetadata.ProtoReflect.Descriptor instead. +func (*ChangeMetadata) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{56} +} + +func (x *ChangeMetadata) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *ChangeMetadata) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ChangeMetadata) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *ChangeMetadata) GetStatus() ChangeStatus { + if x != nil { + return x.Status + } + return ChangeStatus_CHANGE_STATUS_UNSPECIFIED +} + +func (x *ChangeMetadata) GetCreatorName() string { + if x != nil { + return x.CreatorName + } + return "" +} + +func (x *ChangeMetadata) GetCreatorEmail() string { + if x != nil { + return x.CreatorEmail + } + return "" +} + +func (x *ChangeMetadata) GetNumAffectedItems() int32 { + if x != nil { + return x.NumAffectedItems + } + return 0 +} + +func (x *ChangeMetadata) GetNumAffectedEdges() int32 { + if x != nil { + return x.NumAffectedEdges + } + return 0 +} + +func (x *ChangeMetadata) GetNumUnchangedItems() int32 { + if x != nil { + return x.NumUnchangedItems + } + return 0 +} + +func (x *ChangeMetadata) GetNumCreatedItems() int32 { + if x != nil { + return x.NumCreatedItems + } + return 0 +} + +func (x *ChangeMetadata) GetNumUpdatedItems() int32 { + if x != nil { + return x.NumUpdatedItems + } + return 0 +} + +func (x *ChangeMetadata) GetNumReplacedItems() int32 { + if x != nil { + return x.NumReplacedItems + } + return 0 +} + +func (x *ChangeMetadata) GetNumDeletedItems() int32 { + if x != nil { + return x.NumDeletedItems + } + return 0 +} + +func (x *ChangeMetadata) GetUnknownHealthChange() *ChangeMetadata_HealthChange { + if x != nil { + return x.UnknownHealthChange + } + return nil +} + +func (x *ChangeMetadata) GetOkHealthChange() *ChangeMetadata_HealthChange { + if x != nil { + return x.OkHealthChange + } + return nil +} + +func (x *ChangeMetadata) GetWarningHealthChange() *ChangeMetadata_HealthChange { + if x != nil { + return x.WarningHealthChange + } + return nil +} + +func (x *ChangeMetadata) GetErrorHealthChange() *ChangeMetadata_HealthChange { + if x != nil { + return x.ErrorHealthChange + } + return nil +} + +func (x *ChangeMetadata) GetPendingHealthChange() *ChangeMetadata_HealthChange { + if x != nil { + return x.PendingHealthChange + } + return nil +} + +func (x *ChangeMetadata) GetGithubChangeInfo() *GithubChangeInfo { + if x != nil { + return x.GithubChangeInfo + } + return nil +} + +func (x *ChangeMetadata) GetTotalObservations() uint32 { + if x != nil && x.TotalObservations != nil { + return *x.TotalObservations + } + return 0 +} + +func (x *ChangeMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { + if x != nil { + return x.ChangeAnalysisStatus + } + return nil +} + +// user-supplied properties of this change +type ChangeProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Short title for this change. + // Example: "database upgrade" + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // Quick description of the change. + // Example: "upgrade of the database to get access to the new contoso management processor" + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Link to the ticket for this change. + // Example: "http://jira.contoso-engineering.com/browse/CM-1337" + TicketLink string `protobuf:"bytes,4,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` + // The owner of this change. + // Example: Susan + Owner string `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` + // A comma-separated list of emails to keep updated with the status of this change. + // Example: susan@contoso.com, jimmy@contoso.com + CcEmails string `protobuf:"bytes,6,opt,name=ccEmails,proto3" json:"ccEmails,omitempty"` + // UUID of a bookmark for the item queries of the items *directly* affected by + // this change. This might be parsed from a terraform plan, added from the API, + // parsed from a freeform ticket description etc. + ChangingItemsBookmarkUUID []byte `protobuf:"bytes,7,opt,name=changingItemsBookmarkUUID,proto3" json:"changingItemsBookmarkUUID,omitempty"` + // UUID of a snapshot for the item queries of the items *indirectly* affected + // by this change i.e. the blast radius. The initial selection will be determined + // automatically based off changingItemsBookmark, but can refined by the user. + BlastRadiusSnapshotUUID []byte `protobuf:"bytes,11,opt,name=blastRadiusSnapshotUUID,proto3" json:"blastRadiusSnapshotUUID,omitempty"` + // UUID of the whole-system snapshot created before the change has started. + SystemBeforeSnapshotUUID []byte `protobuf:"bytes,8,opt,name=systemBeforeSnapshotUUID,proto3" json:"systemBeforeSnapshotUUID,omitempty"` + // UUID of the whole-system snapshot created after the change has finished. + SystemAfterSnapshotUUID []byte `protobuf:"bytes,9,opt,name=systemAfterSnapshotUUID,proto3" json:"systemAfterSnapshotUUID,omitempty"` + // a list of item diffs that were planned to be changed as part of this change. For all items that we could map, the ItemDiff.Reference will be set to the actual item found. + PlannedChanges []*ItemDiff `protobuf:"bytes,12,rep,name=plannedChanges,proto3" json:"plannedChanges,omitempty"` + // The raw plan output for calculating the change's risks. + RawPlan string `protobuf:"bytes,13,opt,name=rawPlan,proto3" json:"rawPlan,omitempty"` + // The code changes of this change for calculating the change's risks. + CodeChanges string `protobuf:"bytes,14,opt,name=codeChanges,proto3" json:"codeChanges,omitempty"` + // Repo information; can be an empty string. CLI attempts auto-population, but users can override. Not necessarily a URL. The UI will be responsible for any formatting/shortening/sprucing up should it be required. + Repo string `protobuf:"bytes,15,opt,name=repo,proto3" json:"repo,omitempty"` + // Tags that were set bu the user when the tag was created + // + // Deprecated: Use enrichedTags instead + // + // Deprecated: Marked as deprecated in changes.proto. + Tags map[string]string `protobuf:"bytes,16,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Tags associated with this change + EnrichedTags *EnrichedTags `protobuf:"bytes,18,opt,name=enrichedTags,proto3" json:"enrichedTags,omitempty"` + // labels + // note we keep track of the label type in the label struct itself + Labels []*Label `protobuf:"bytes,21,rep,name=labels,proto3" json:"labels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeProperties) Reset() { + *x = ChangeProperties{} + mi := &file_changes_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeProperties) ProtoMessage() {} + +func (x *ChangeProperties) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[57] + 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 ChangeProperties.ProtoReflect.Descriptor instead. +func (*ChangeProperties) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{57} +} + +func (x *ChangeProperties) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ChangeProperties) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ChangeProperties) GetTicketLink() string { + if x != nil { + return x.TicketLink + } + return "" +} + +func (x *ChangeProperties) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *ChangeProperties) GetCcEmails() string { + if x != nil { + return x.CcEmails + } + return "" +} + +func (x *ChangeProperties) GetChangingItemsBookmarkUUID() []byte { + if x != nil { + return x.ChangingItemsBookmarkUUID + } + return nil +} + +func (x *ChangeProperties) GetBlastRadiusSnapshotUUID() []byte { + if x != nil { + return x.BlastRadiusSnapshotUUID + } + return nil +} + +func (x *ChangeProperties) GetSystemBeforeSnapshotUUID() []byte { + if x != nil { + return x.SystemBeforeSnapshotUUID + } + return nil +} + +func (x *ChangeProperties) GetSystemAfterSnapshotUUID() []byte { + if x != nil { + return x.SystemAfterSnapshotUUID + } + return nil +} + +func (x *ChangeProperties) GetPlannedChanges() []*ItemDiff { + if x != nil { + return x.PlannedChanges + } + return nil +} + +func (x *ChangeProperties) GetRawPlan() string { + if x != nil { + return x.RawPlan + } + return "" +} + +func (x *ChangeProperties) GetCodeChanges() string { + if x != nil { + return x.CodeChanges + } + return "" +} + +func (x *ChangeProperties) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +// Deprecated: Marked as deprecated in changes.proto. +func (x *ChangeProperties) GetTags() map[string]string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *ChangeProperties) GetEnrichedTags() *EnrichedTags { + if x != nil { + return x.EnrichedTags + } + return nil +} + +func (x *ChangeProperties) GetLabels() []*Label { + if x != nil { + return x.Labels + } + return nil +} + +// GithubChangeInfo contains information about a change that originated from GitHub +// contains mostly author information. +type GithubChangeInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The GitHub username of the author of the change + AuthorUsername string `protobuf:"bytes,1,opt,name=authorUsername,proto3" json:"authorUsername,omitempty"` + // The author full name + AuthorFullName string `protobuf:"bytes,2,opt,name=authorFullName,proto3" json:"authorFullName,omitempty"` + // The link to the author's avatar + AuthorAvatarLink string `protobuf:"bytes,3,opt,name=authorAvatarLink,proto3" json:"authorAvatarLink,omitempty"` + // The email of the author + AuthorEmail string `protobuf:"bytes,4,opt,name=authorEmail,proto3" json:"authorEmail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GithubChangeInfo) Reset() { + *x = GithubChangeInfo{} + mi := &file_changes_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GithubChangeInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GithubChangeInfo) ProtoMessage() {} + +func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[58] + 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 GithubChangeInfo.ProtoReflect.Descriptor instead. +func (*GithubChangeInfo) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{58} +} + +func (x *GithubChangeInfo) GetAuthorUsername() string { + if x != nil { + return x.AuthorUsername + } + return "" +} + +func (x *GithubChangeInfo) GetAuthorFullName() string { + if x != nil { + return x.AuthorFullName + } + return "" +} + +func (x *GithubChangeInfo) GetAuthorAvatarLink() string { + if x != nil { + return x.AuthorAvatarLink + } + return "" +} + +func (x *GithubChangeInfo) GetAuthorEmail() string { + if x != nil { + return x.AuthorEmail + } + return "" +} + +// list all changes +type ListChangesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesRequest) Reset() { + *x = ListChangesRequest{} + mi := &file_changes_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesRequest) ProtoMessage() {} + +func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[59] + 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 ListChangesRequest.ProtoReflect.Descriptor instead. +func (*ListChangesRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{59} +} + +type ListChangesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesResponse) Reset() { + *x = ListChangesResponse{} + mi := &file_changes_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesResponse) ProtoMessage() {} + +func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[60] + 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 ListChangesResponse.ProtoReflect.Descriptor instead. +func (*ListChangesResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{60} +} + +func (x *ListChangesResponse) GetChanges() []*Change { + if x != nil { + return x.Changes + } + return nil +} + +// list all changes in a specific status +type ListChangesByStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status ChangeStatus `protobuf:"varint,1,opt,name=status,proto3,enum=changes.ChangeStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesByStatusRequest) Reset() { + *x = ListChangesByStatusRequest{} + mi := &file_changes_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesByStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesByStatusRequest) ProtoMessage() {} + +func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[61] + 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 ListChangesByStatusRequest.ProtoReflect.Descriptor instead. +func (*ListChangesByStatusRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{61} +} + +func (x *ListChangesByStatusRequest) GetStatus() ChangeStatus { + if x != nil { + return x.Status + } + return ChangeStatus_CHANGE_STATUS_UNSPECIFIED +} + +type ListChangesByStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesByStatusResponse) Reset() { + *x = ListChangesByStatusResponse{} + mi := &file_changes_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesByStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesByStatusResponse) ProtoMessage() {} + +func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[62] + 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 ListChangesByStatusResponse.ProtoReflect.Descriptor instead. +func (*ListChangesByStatusResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{62} +} + +func (x *ListChangesByStatusResponse) GetChanges() []*Change { + if x != nil { + return x.Changes + } + return nil +} + +// create a new change +type CreateChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Properties *ChangeProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateChangeRequest) Reset() { + *x = CreateChangeRequest{} + mi := &file_changes_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateChangeRequest) ProtoMessage() {} + +func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[63] + 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 CreateChangeRequest.ProtoReflect.Descriptor instead. +func (*CreateChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{63} +} + +func (x *CreateChangeRequest) GetProperties() *ChangeProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateChangeResponse) Reset() { + *x = CreateChangeResponse{} + mi := &file_changes_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateChangeResponse) ProtoMessage() {} + +func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[64] + 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 CreateChangeResponse.ProtoReflect.Descriptor instead. +func (*CreateChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{64} +} + +func (x *CreateChangeResponse) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +// get the details of a specific change +type GetChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Return a slimmed down version of the change. This will exclude the + // following data: + // * `rawPlan`: The entire Terraform plan output + // * `codeChanges`: The code changes that created this change + Slim bool `protobuf:"varint,2,opt,name=slim,proto3" json:"slim,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeRequest) Reset() { + *x = GetChangeRequest{} + mi := &file_changes_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeRequest) ProtoMessage() {} + +func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[65] + 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 GetChangeRequest.ProtoReflect.Descriptor instead. +func (*GetChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{65} +} + +func (x *GetChangeRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *GetChangeRequest) GetSlim() bool { + if x != nil { + return x.Slim + } + return false +} + +type GetChangeByTicketLinkRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TicketLink string `protobuf:"bytes,1,opt,name=ticketLink,proto3" json:"ticketLink,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeByTicketLinkRequest) Reset() { + *x = GetChangeByTicketLinkRequest{} + mi := &file_changes_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeByTicketLinkRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeByTicketLinkRequest) ProtoMessage() {} + +func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetChangeByTicketLinkRequest.ProtoReflect.Descriptor instead. +func (*GetChangeByTicketLinkRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{66} +} + +func (x *GetChangeByTicketLinkRequest) GetTicketLink() string { + if x != nil { + return x.TicketLink + } + return "" +} + +type GetChangeSummaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Return a slimmed down version of the change. This will exclude the + // following data: + // * `rawPlan`: The entire Terraform plan output + // * `codeChanges`: The code changes that created this change + Slim bool `protobuf:"varint,2,opt,name=slim,proto3" json:"slim,omitempty"` + // currently json or markdown + ChangeOutputFormat ChangeOutputFormat `protobuf:"varint,3,opt,name=changeOutputFormat,proto3,enum=changes.ChangeOutputFormat" json:"changeOutputFormat,omitempty"` + // For filtering risks from display in the output + RiskSeverityFilter []Risk_Severity `protobuf:"varint,4,rep,packed,name=riskSeverityFilter,proto3,enum=changes.Risk_Severity" json:"riskSeverityFilter,omitempty"` + // this is the app url the user has been today + AppURL string `protobuf:"bytes,5,opt,name=appURL,proto3" json:"appURL,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeSummaryRequest) Reset() { + *x = GetChangeSummaryRequest{} + mi := &file_changes_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeSummaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeSummaryRequest) ProtoMessage() {} + +func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetChangeSummaryRequest.ProtoReflect.Descriptor instead. +func (*GetChangeSummaryRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{67} +} + +func (x *GetChangeSummaryRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *GetChangeSummaryRequest) GetSlim() bool { + if x != nil { + return x.Slim + } + return false +} + +func (x *GetChangeSummaryRequest) GetChangeOutputFormat() ChangeOutputFormat { + if x != nil { + return x.ChangeOutputFormat + } + return ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED +} + +func (x *GetChangeSummaryRequest) GetRiskSeverityFilter() []Risk_Severity { + if x != nil { + return x.RiskSeverityFilter + } + return nil +} + +func (x *GetChangeSummaryRequest) GetAppURL() string { + if x != nil { + return x.AppURL + } + return "" +} + +type GetChangeSummaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change string `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeSummaryResponse) Reset() { + *x = GetChangeSummaryResponse{} + mi := &file_changes_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeSummaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeSummaryResponse) ProtoMessage() {} + +func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[68] + 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 GetChangeSummaryResponse.ProtoReflect.Descriptor instead. +func (*GetChangeSummaryResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{68} +} + +func (x *GetChangeSummaryResponse) GetChange() string { + if x != nil { + return x.Change + } + return "" +} + +type GetChangeSignalsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // Output format for the signals data (json by default) + ChangeOutputFormat ChangeOutputFormat `protobuf:"varint,2,opt,name=changeOutputFormat,proto3,enum=changes.ChangeOutputFormat" json:"changeOutputFormat,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeSignalsRequest) Reset() { + *x = GetChangeSignalsRequest{} + mi := &file_changes_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeSignalsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeSignalsRequest) ProtoMessage() {} + +func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[69] + 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 GetChangeSignalsRequest.ProtoReflect.Descriptor instead. +func (*GetChangeSignalsRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{69} +} + +func (x *GetChangeSignalsRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *GetChangeSignalsRequest) GetChangeOutputFormat() ChangeOutputFormat { + if x != nil { + return x.ChangeOutputFormat + } + return ChangeOutputFormat_CHANGE_OUTPUT_FORMAT_UNSPECIFIED +} + +type GetChangeSignalsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signals string `protobuf:"bytes,1,opt,name=signals,proto3" json:"signals,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeSignalsResponse) Reset() { + *x = GetChangeSignalsResponse{} + mi := &file_changes_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeSignalsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeSignalsResponse) ProtoMessage() {} + +func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[70] + 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 GetChangeSignalsResponse.ProtoReflect.Descriptor instead. +func (*GetChangeSignalsResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{70} +} + +func (x *GetChangeSignalsResponse) GetSignals() string { + if x != nil { + return x.Signals + } + return "" +} + +type GetChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeResponse) Reset() { + *x = GetChangeResponse{} + mi := &file_changes_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeResponse) ProtoMessage() {} + +func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[71] + 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 GetChangeResponse.ProtoReflect.Descriptor instead. +func (*GetChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{71} +} + +func (x *GetChangeResponse) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +// get the details of a specific change +type GetChangeRisksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeRisksRequest) Reset() { + *x = GetChangeRisksRequest{} + mi := &file_changes_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeRisksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeRisksRequest) ProtoMessage() {} + +func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[72] + 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 GetChangeRisksRequest.ProtoReflect.Descriptor instead. +func (*GetChangeRisksRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{72} +} + +func (x *GetChangeRisksRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type ChangeRiskMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The status of the risk calculation + ChangeAnalysisStatus *ChangeAnalysisStatus `protobuf:"bytes,1,opt,name=changeAnalysisStatus,proto3" json:"changeAnalysisStatus,omitempty"` + // The risks that are related to this change + Risks []*Risk `protobuf:"bytes,5,rep,name=risks,proto3" json:"risks,omitempty"` + // The number of low risks in this change + NumLowRisk int32 `protobuf:"varint,6,opt,name=numLowRisk,proto3" json:"numLowRisk,omitempty"` + // The number of medium risks in this change + NumMediumRisk int32 `protobuf:"varint,7,opt,name=numMediumRisk,proto3" json:"numMediumRisk,omitempty"` + // The number of high risks in this change + NumHighRisk int32 `protobuf:"varint,8,opt,name=numHighRisk,proto3" json:"numHighRisk,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeRiskMetadata) Reset() { + *x = ChangeRiskMetadata{} + mi := &file_changes_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeRiskMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeRiskMetadata) ProtoMessage() {} + +func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[73] + 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 ChangeRiskMetadata.ProtoReflect.Descriptor instead. +func (*ChangeRiskMetadata) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{73} +} + +func (x *ChangeRiskMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { + if x != nil { + return x.ChangeAnalysisStatus + } + return nil +} + +func (x *ChangeRiskMetadata) GetRisks() []*Risk { + if x != nil { + return x.Risks + } + return nil +} + +func (x *ChangeRiskMetadata) GetNumLowRisk() int32 { + if x != nil { + return x.NumLowRisk + } + return 0 +} + +func (x *ChangeRiskMetadata) GetNumMediumRisk() int32 { + if x != nil { + return x.NumMediumRisk + } + return 0 +} + +func (x *ChangeRiskMetadata) GetNumHighRisk() int32 { + if x != nil { + return x.NumHighRisk + } + return 0 +} + +type GetChangeRisksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeRiskMetadata *ChangeRiskMetadata `protobuf:"bytes,1,opt,name=changeRiskMetadata,proto3" json:"changeRiskMetadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeRisksResponse) Reset() { + *x = GetChangeRisksResponse{} + mi := &file_changes_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeRisksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeRisksResponse) ProtoMessage() {} + +func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[74] + 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 GetChangeRisksResponse.ProtoReflect.Descriptor instead. +func (*GetChangeRisksResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{74} +} + +func (x *GetChangeRisksResponse) GetChangeRiskMetadata() *ChangeRiskMetadata { + if x != nil { + return x.ChangeRiskMetadata + } + return nil +} + +// update an existing change +type UpdateChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + Properties *ChangeProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateChangeRequest) Reset() { + *x = UpdateChangeRequest{} + mi := &file_changes_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateChangeRequest) ProtoMessage() {} + +func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[75] + 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 UpdateChangeRequest.ProtoReflect.Descriptor instead. +func (*UpdateChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{75} +} + +func (x *UpdateChangeRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *UpdateChangeRequest) GetProperties() *ChangeProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Change *Change `protobuf:"bytes,1,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateChangeResponse) Reset() { + *x = UpdateChangeResponse{} + mi := &file_changes_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateChangeResponse) ProtoMessage() {} + +func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[76] + 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 UpdateChangeResponse.ProtoReflect.Descriptor instead. +func (*UpdateChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{76} +} + +func (x *UpdateChangeResponse) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +// delete a change +type DeleteChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteChangeRequest) Reset() { + *x = DeleteChangeRequest{} + mi := &file_changes_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteChangeRequest) ProtoMessage() {} + +func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[77] + 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 DeleteChangeRequest.ProtoReflect.Descriptor instead. +func (*DeleteChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{77} +} + +func (x *DeleteChangeRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +// list changes for a snapshot UUID +type ListChangesBySnapshotUUIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesBySnapshotUUIDRequest) Reset() { + *x = ListChangesBySnapshotUUIDRequest{} + mi := &file_changes_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesBySnapshotUUIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesBySnapshotUUIDRequest) ProtoMessage() {} + +func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[78] + 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 ListChangesBySnapshotUUIDRequest.ProtoReflect.Descriptor instead. +func (*ListChangesBySnapshotUUIDRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{78} +} + +func (x *ListChangesBySnapshotUUIDRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type ListChangesBySnapshotUUIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Changes []*Change `protobuf:"bytes,1,rep,name=changes,proto3" json:"changes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListChangesBySnapshotUUIDResponse) Reset() { + *x = ListChangesBySnapshotUUIDResponse{} + mi := &file_changes_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListChangesBySnapshotUUIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListChangesBySnapshotUUIDResponse) ProtoMessage() {} + +func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[79] + 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 ListChangesBySnapshotUUIDResponse.ProtoReflect.Descriptor instead. +func (*ListChangesBySnapshotUUIDResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{79} +} + +func (x *ListChangesBySnapshotUUIDResponse) GetChanges() []*Change { + if x != nil { + return x.Changes + } + return nil +} + +type DeleteChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteChangeResponse) Reset() { + *x = DeleteChangeResponse{} + mi := &file_changes_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteChangeResponse) ProtoMessage() {} + +func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[80] + 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 DeleteChangeResponse.ProtoReflect.Descriptor instead. +func (*DeleteChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{80} +} + +type RefreshStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshStateRequest) Reset() { + *x = RefreshStateRequest{} + mi := &file_changes_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshStateRequest) ProtoMessage() {} + +func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[81] + 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 RefreshStateRequest.ProtoReflect.Descriptor instead. +func (*RefreshStateRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{81} +} + +type RefreshStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RefreshStateResponse) Reset() { + *x = RefreshStateResponse{} + mi := &file_changes_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RefreshStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RefreshStateResponse) ProtoMessage() {} + +func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[82] + 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 RefreshStateResponse.ProtoReflect.Descriptor instead. +func (*RefreshStateResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{82} +} + +type StartChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartChangeRequest) Reset() { + *x = StartChangeRequest{} + mi := &file_changes_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartChangeRequest) ProtoMessage() {} + +func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[83] + 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 StartChangeRequest.ProtoReflect.Descriptor instead. +func (*StartChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{83} +} + +func (x *StartChangeRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type StartChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State StartChangeResponse_State `protobuf:"varint,1,opt,name=state,proto3,enum=changes.StartChangeResponse_State" json:"state,omitempty"` + NumItems uint32 `protobuf:"varint,2,opt,name=numItems,proto3" json:"numItems,omitempty"` + NumEdges uint32 `protobuf:"varint,3,opt,name=NumEdges,proto3" json:"NumEdges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartChangeResponse) Reset() { + *x = StartChangeResponse{} + mi := &file_changes_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartChangeResponse) ProtoMessage() {} + +func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[84] + 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 StartChangeResponse.ProtoReflect.Descriptor instead. +func (*StartChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{84} +} + +func (x *StartChangeResponse) GetState() StartChangeResponse_State { + if x != nil { + return x.State + } + return StartChangeResponse_STATE_UNSPECIFIED +} + +func (x *StartChangeResponse) GetNumItems() uint32 { + if x != nil { + return x.NumItems + } + return 0 +} + +func (x *StartChangeResponse) GetNumEdges() uint32 { + if x != nil { + return x.NumEdges + } + return 0 +} + +type EndChangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndChangeRequest) Reset() { + *x = EndChangeRequest{} + mi := &file_changes_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndChangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndChangeRequest) ProtoMessage() {} + +func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[85] + 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 EndChangeRequest.ProtoReflect.Descriptor instead. +func (*EndChangeRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{85} +} + +func (x *EndChangeRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type EndChangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State EndChangeResponse_State `protobuf:"varint,1,opt,name=state,proto3,enum=changes.EndChangeResponse_State" json:"state,omitempty"` + NumItems uint32 `protobuf:"varint,2,opt,name=numItems,proto3" json:"numItems,omitempty"` + NumEdges uint32 `protobuf:"varint,3,opt,name=NumEdges,proto3" json:"NumEdges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndChangeResponse) Reset() { + *x = EndChangeResponse{} + mi := &file_changes_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndChangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndChangeResponse) ProtoMessage() {} + +func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[86] + 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 EndChangeResponse.ProtoReflect.Descriptor instead. +func (*EndChangeResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{86} +} + +func (x *EndChangeResponse) GetState() EndChangeResponse_State { + if x != nil { + return x.State + } + return EndChangeResponse_STATE_UNSPECIFIED +} + +func (x *EndChangeResponse) GetNumItems() uint32 { + if x != nil { + return x.NumItems + } + return 0 +} + +func (x *EndChangeResponse) GetNumEdges() uint32 { + if x != nil { + return x.NumEdges + } + return 0 +} + +type StartChangeSimpleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartChangeSimpleResponse) Reset() { + *x = StartChangeSimpleResponse{} + mi := &file_changes_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartChangeSimpleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartChangeSimpleResponse) ProtoMessage() {} + +func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[87] + 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 StartChangeSimpleResponse.ProtoReflect.Descriptor instead. +func (*StartChangeSimpleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{87} +} + +type EndChangeSimpleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // True if the job was successfully enqueued (or queued to run after start-change) + Queued bool `protobuf:"varint,1,opt,name=queued,proto3" json:"queued,omitempty"` + // True if end-change was queued to run after start-change completes + QueuedAfterStart bool `protobuf:"varint,2,opt,name=queued_after_start,json=queuedAfterStart,proto3" json:"queued_after_start,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EndChangeSimpleResponse) Reset() { + *x = EndChangeSimpleResponse{} + mi := &file_changes_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EndChangeSimpleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndChangeSimpleResponse) ProtoMessage() {} + +func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[88] + 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 EndChangeSimpleResponse.ProtoReflect.Descriptor instead. +func (*EndChangeSimpleResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{88} +} + +func (x *EndChangeSimpleResponse) GetQueued() bool { + if x != nil { + return x.Queued + } + return false +} + +func (x *EndChangeSimpleResponse) GetQueuedAfterStart() bool { + if x != nil { + return x.QueuedAfterStart + } + return false +} + +type Risk struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,5,opt,name=UUID,proto3" json:"UUID,omitempty"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Severity Risk_Severity `protobuf:"varint,2,opt,name=severity,proto3,enum=changes.Risk_Severity" json:"severity,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + RelatedItems []*Reference `protobuf:"bytes,4,rep,name=relatedItems,proto3" json:"relatedItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Risk) Reset() { + *x = Risk{} + mi := &file_changes_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Risk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Risk) ProtoMessage() {} + +func (x *Risk) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[89] + 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 Risk.ProtoReflect.Descriptor instead. +func (*Risk) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{89} +} + +func (x *Risk) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *Risk) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Risk) GetSeverity() Risk_Severity { + if x != nil { + return x.Severity + } + return Risk_SEVERITY_UNSPECIFIED +} + +func (x *Risk) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Risk) GetRelatedItems() []*Reference { + if x != nil { + return x.RelatedItems + } + return nil +} + +type ChangeAnalysisStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status ChangeAnalysisStatus_Status `protobuf:"varint,1,opt,name=status,proto3,enum=changes.ChangeAnalysisStatus_Status" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeAnalysisStatus) Reset() { + *x = ChangeAnalysisStatus{} + mi := &file_changes_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeAnalysisStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeAnalysisStatus) ProtoMessage() {} + +func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[90] + 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 ChangeAnalysisStatus.ProtoReflect.Descriptor instead. +func (*ChangeAnalysisStatus) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{90} +} + +func (x *ChangeAnalysisStatus) GetStatus() ChangeAnalysisStatus_Status { + if x != nil { + return x.Status + } + return ChangeAnalysisStatus_STATUS_UNSPECIFIED +} + +// Generate fix suggestion for a risk +type GenerateRiskFixRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The UUID of the risk to generate a fix for + RiskUUID []byte `protobuf:"bytes,1,opt,name=riskUUID,proto3" json:"riskUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateRiskFixRequest) Reset() { + *x = GenerateRiskFixRequest{} + mi := &file_changes_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateRiskFixRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateRiskFixRequest) ProtoMessage() {} + +func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[91] + 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 GenerateRiskFixRequest.ProtoReflect.Descriptor instead. +func (*GenerateRiskFixRequest) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{91} +} + +func (x *GenerateRiskFixRequest) GetRiskUUID() []byte { + if x != nil { + return x.RiskUUID + } + return nil +} + +type GenerateRiskFixResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The generated fix suggestion text + FixSuggestion string `protobuf:"bytes,1,opt,name=fixSuggestion,proto3" json:"fixSuggestion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GenerateRiskFixResponse) Reset() { + *x = GenerateRiskFixResponse{} + mi := &file_changes_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GenerateRiskFixResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateRiskFixResponse) ProtoMessage() {} + +func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[92] + 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 GenerateRiskFixResponse.ProtoReflect.Descriptor instead. +func (*GenerateRiskFixResponse) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{92} +} + +func (x *GenerateRiskFixResponse) GetFixSuggestion() string { + if x != nil { + return x.FixSuggestion + } + return "" +} + +// Represents the current state of a given health state, and the amount that +// it has changed. This doesn't just look at the change in total number of +// items, but also the number of items that have been added and removed, even +// if they were to add to the same number +type ChangeMetadata_HealthChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The number of items that were added to this health state as part of the + // change + Added int32 `protobuf:"varint,1,opt,name=added,proto3" json:"added,omitempty"` + // The number of items that were removed them this health state as part of + // the change + Removed int32 `protobuf:"varint,2,opt,name=removed,proto3" json:"removed,omitempty"` + // The final number of items that were in this health state + FinalTotal int32 `protobuf:"varint,3,opt,name=finalTotal,proto3" json:"finalTotal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeMetadata_HealthChange) Reset() { + *x = ChangeMetadata_HealthChange{} + mi := &file_changes_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeMetadata_HealthChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeMetadata_HealthChange) ProtoMessage() {} + +func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[95] + 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 ChangeMetadata_HealthChange.ProtoReflect.Descriptor instead. +func (*ChangeMetadata_HealthChange) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{56, 0} +} + +func (x *ChangeMetadata_HealthChange) GetAdded() int32 { + if x != nil { + return x.Added + } + return 0 +} + +func (x *ChangeMetadata_HealthChange) GetRemoved() int32 { + if x != nil { + return x.Removed + } + return 0 +} + +func (x *ChangeMetadata_HealthChange) GetFinalTotal() int32 { + if x != nil { + return x.FinalTotal + } + return 0 +} + +var File_changes_proto protoreflect.FileDescriptor + +const file_changes_proto_rawDesc = "" + + "\n" + + "\rchanges.proto\x12\achanges\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\fconfig.proto\x1a\vitems.proto\x1a\n" + + "util.proto\"\x81\x01\n" + + "\tLabelRule\x126\n" + + "\bmetadata\x18\x01 \x01(\v2\x1a.changes.LabelRuleMetadataR\bmetadata\x12<\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + + "properties\"\xad\x01\n" + + "\x11LabelRuleMetadata\x12$\n" + + "\rLabelRuleUUID\x18\x01 \x01(\fR\rLabelRuleUUID\x128\n" + + "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x128\n" + + "\tupdatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"e\n" + + "\x13LabelRuleProperties\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" + + "\x06colour\x18\x02 \x01(\tR\x06colour\x12\"\n" + + "\finstructions\x18\x03 \x01(\tR\finstructions\"\x17\n" + + "\x15ListLabelRulesRequest\"B\n" + + "\x16ListLabelRulesResponse\x12(\n" + + "\x05rules\x18\x01 \x03(\v2\x12.changes.LabelRuleR\x05rules\"V\n" + + "\x16CreateLabelRuleRequest\x12<\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + + "properties\"A\n" + + "\x17CreateLabelRuleResponse\x12&\n" + + "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\")\n" + + "\x13GetLabelRuleRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\">\n" + + "\x14GetLabelRuleResponse\x12&\n" + + "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\"j\n" + + "\x16UpdateLabelRuleRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12<\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + + "properties\"A\n" + + "\x17UpdateLabelRuleResponse\x12&\n" + + "\x04rule\x18\x01 \x01(\v2\x12.changes.LabelRuleR\x04rule\",\n" + + "\x16DeleteLabelRuleRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x19\n" + + "\x17DeleteLabelRuleResponse\"t\n" + + "\x14TestLabelRuleRequest\x12<\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1c.changes.LabelRulePropertiesR\n" + + "properties\x12\x1e\n" + + "\n" + + "changeUUID\x18\x02 \x03(\fR\n" + + "changeUUID\"w\n" + + "\x15TestLabelRuleResponse\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\x12\x18\n" + + "\aapplied\x18\x02 \x01(\bR\aapplied\x12$\n" + + "\x05label\x18\x03 \x01(\v2\x0e.changes.LabelR\x05label\"\xa0\x01\n" + + "\"ReapplyLabelRuleInTimeRangeRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + + "\astartAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\astartAt\x120\n" + + "\x05endAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x05endAt\"E\n" + + "#ReapplyLabelRuleInTimeRangeResponse\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x03(\fR\n" + + "changeUUID\"=\n" + + "\x1bGetHypothesesDetailsRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"Z\n" + + "\x1cGetHypothesesDetailsResponse\x12:\n" + + "\n" + + "hypotheses\x18\x01 \x03(\v2\x1a.changes.HypothesesDetailsR\n" + + "hypotheses\"\xd2\x01\n" + + "\x11HypothesesDetails\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12(\n" + + "\x0fnumObservations\x18\x02 \x01(\rR\x0fnumObservations\x12\x16\n" + + "\x06detail\x18\x03 \x01(\tR\x06detail\x121\n" + + "\x06status\x18\x04 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x122\n" + + "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\"<\n" + + "\x1aGetChangeTimelineV2Request\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"W\n" + + "\x1bGetChangeTimelineV2Response\x128\n" + + "\aentries\x18\x01 \x03(\v2\x1e.changes.ChangeTimelineEntryV2R\aentries\"\xd6\b\n" + + "\x15ChangeTimelineEntryV2\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12:\n" + + "\x06status\x18\x02 \x01(\x0e2\".changes.ChangeTimelineEntryStatusR\x06status\x12=\n" + + "\tstartedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\tstartedAt\x88\x01\x01\x129\n" + + "\aendedAt\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x02R\aendedAt\x88\x01\x01\x12\x19\n" + + "\x05actor\x18\x05 \x01(\tH\x03R\x05actor\x88\x01\x01\x12E\n" + + "\vmappedItems\x18\a \x01(\v2!.changes.MappedItemsTimelineEntryH\x00R\vmappedItems\x12c\n" + + "\x15calculatedBlastRadius\x18\b \x01(\v2+.changes.CalculatedBlastRadiusTimelineEntryH\x00R\x15calculatedBlastRadius\x12Q\n" + + "\x0fcalculatedRisks\x18\t \x01(\v2%.changes.CalculatedRisksTimelineEntryH\x00R\x0fcalculatedRisks\x12\x16\n" + + "\x05error\x18\v \x01(\tH\x00R\x05error\x12&\n" + + "\rstatusMessage\x18\f \x01(\tH\x00R\rstatusMessage\x12-\n" + + "\x05empty\x18\r \x01(\v2\x15.changes.EmptyContentH\x00R\x05empty\x12T\n" + + "\x10changeValidation\x18\x0e \x01(\v2&.changes.ChangeValidationTimelineEntryH\x00R\x10changeValidation\x12T\n" + + "\x10calculatedLabels\x18\x0f \x01(\v2&.changes.CalculatedLabelsTimelineEntryH\x00R\x10calculatedLabels\x12N\n" + + "\x0eformHypotheses\x18\x10 \x01(\v2$.changes.FormHypothesesTimelineEntryH\x00R\x0eformHypotheses\x12c\n" + + "\x15investigateHypotheses\x18\x11 \x01(\v2+.changes.InvestigateHypothesesTimelineEntryH\x00R\x15investigateHypotheses\x12Z\n" + + "\x12recordObservations\x18\x12 \x01(\v2(.changes.RecordObservationsTimelineEntryH\x00R\x12recordObservationsB\t\n" + + "\acontentB\f\n" + + "\n" + + "_startedAtB\n" + + "\n" + + "\b_endedAtB\b\n" + + "\x06_actor\"\x0e\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" + + "\x1fRecordObservationsTimelineEntry\x12(\n" + + "\x0fnumObservations\x18\x01 \x01(\rR\x0fnumObservations\"\x7f\n" + + "\x1bFormHypothesesTimelineEntry\x12$\n" + + "\rnumHypotheses\x18\x01 \x01(\rR\rnumHypotheses\x12:\n" + + "\n" + + "hypotheses\x18\x02 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + + "hypotheses\"\xce\x01\n" + + "\"InvestigateHypothesesTimelineEntry\x12\x1c\n" + + "\tnumProven\x18\x01 \x01(\rR\tnumProven\x12\"\n" + + "\fnumDisproven\x18\x02 \x01(\rR\fnumDisproven\x12*\n" + + "\x10numInvestigating\x18\x03 \x01(\rR\x10numInvestigating\x12:\n" + + "\n" + + "hypotheses\x18\x04 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + + "hypotheses\"t\n" + + "\x11HypothesisSummary\x121\n" + + "\x06status\x18\x01 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + + "\x06detail\x18\x03 \x01(\tR\x06detail\"C\n" + + "\x1cCalculatedRisksTimelineEntry\x12#\n" + + "\x05risks\x18\x01 \x03(\v2\r.changes.RiskR\x05risks\"G\n" + + "\x1dCalculatedLabelsTimelineEntry\x12&\n" + + "\x06labels\x18\x01 \x03(\v2\x0e.changes.LabelR\x06labels\"\x9a\x01\n" + + "\x1dChangeValidationTimelineEntry\x12$\n" + + "\rbriefAnalysis\x18\x01 \x01(\tR\rbriefAnalysis\x12S\n" + + "\x13validationChecklist\x18\x02 \x03(\v2!.changes.ChangeValidationCategoryR\x13validationChecklist\"R\n" + + "\x18ChangeValidationCategory\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\"0\n" + + "\x0eGetDiffRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"\xdb\x01\n" + + "\x0fGetDiffResponse\x127\n" + + "\rexpectedItems\x18\x01 \x03(\v2\x11.changes.ItemDiffR\rexpectedItems\x12;\n" + + "\x0funexpectedItems\x18\x03 \x03(\v2\x11.changes.ItemDiffR\x0funexpectedItems\x12\x1b\n" + + "\x05edges\x18\x02 \x03(\v2\x05.EdgeR\x05edges\x125\n" + + "\fmissingItems\x18\x04 \x03(\v2\x11.changes.ItemDiffR\fmissingItems\"A\n" + + "\x1fListChangingItemsSummaryRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"R\n" + + " ListChangingItemsSummaryResponse\x12.\n" + + "\x05items\x18\x01 \x03(\v2\x18.changes.ItemDiffSummaryR\x05items\"\xa1\x02\n" + + "\x0eMappedItemDiff\x12%\n" + + "\x04item\x18\x01 \x01(\v2\x11.changes.ItemDiffR\x04item\x12/\n" + + "\fmappingQuery\x18\x02 \x01(\v2\x06.QueryH\x00R\fmappingQuery\x88\x01\x01\x124\n" + + "\fmappingError\x18\x03 \x01(\v2\v.QueryErrorH\x01R\fmappingError\x88\x01\x01\x12L\n" + + "\x0emapping_status\x18\x04 \x01(\x0e2 .changes.MappedItemMappingStatusH\x02R\rmappingStatus\x88\x01\x01B\x0f\n" + + "\r_mappingQueryB\x0f\n" + + "\r_mappingErrorB\x11\n" + + "\x0f_mapping_status\"\xa1\x04\n" + + "\x1aStartChangeAnalysisRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\x12=\n" + + "\rchangingItems\x18\x02 \x03(\v2\x17.changes.MappedItemDiffR\rchangingItems\x12\\\n" + + "\x19blastRadiusConfigOverride\x18\x03 \x01(\v2\x19.config.BlastRadiusConfigH\x00R\x19blastRadiusConfigOverride\x88\x01\x01\x12e\n" + + "\x1croutineChangesConfigOverride\x18\x05 \x01(\v2\x1c.config.RoutineChangesConfigH\x01R\x1croutineChangesConfigOverride\x88\x01\x01\x12t\n" + + "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01B\x1c\n" + + "\x1a_blastRadiusConfigOverrideB\x1f\n" + + "\x1d_routineChangesConfigOverrideB$\n" + + "\"_githubOrganisationProfileOverrideJ\x04\b\x04\x10\x05\"\x1d\n" + + "\x1bStartChangeAnalysisResponse\"\x96\x01\n" + + "\x16ListHomeChangesRequest\x122\n" + + "\n" + + "pagination\x18\x01 \x01(\v2\x12.PaginationRequestR\n" + + "pagination\x12<\n" + + "\afilters\x18\x02 \x01(\v2\x1d.changes.ChangeFiltersRequestH\x00R\afilters\x88\x01\x01B\n" + + "\n" + + "\b_filters\"\x82\x02\n" + + "\x14ChangeFiltersRequest\x12\x14\n" + + "\x05repos\x18\x01 \x03(\tR\x05repos\x12,\n" + + "\x05risks\x18\x03 \x03(\x0e2\x16.changes.Risk.SeverityR\x05risks\x12\x18\n" + + "\aauthors\x18\x04 \x03(\tR\aauthors\x121\n" + + "\bstatuses\x18\x05 \x03(\x0e2\x15.changes.ChangeStatusR\bstatuses\x12-\n" + + "\tsortOrder\x18\x06 \x01(\x0e2\n" + + ".SortOrderH\x00R\tsortOrder\x88\x01\x01\x12\x16\n" + + "\x06labels\x18\a \x03(\tR\x06labelsB\f\n" + + "\n" + + "_sortOrderJ\x04\b\x02\x10\x03\"\x80\x01\n" + + "\x17ListHomeChangesResponse\x120\n" + + "\achanges\x18\x01 \x03(\v2\x16.changes.ChangeSummaryR\achanges\x123\n" + + "\n" + + "pagination\x18\x02 \x01(\v2\x13.PaginationResponseR\n" + + "pagination\"\x1e\n" + + "\x1cPopulateChangeFiltersRequest\"O\n" + + "\x1dPopulateChangeFiltersResponse\x12\x14\n" + + "\x05repos\x18\x01 \x03(\tR\x05repos\x12\x18\n" + + "\aauthors\x18\x02 \x03(\tR\aauthors\"\x8d\x01\n" + + "\x0fItemDiffSummary\x12\x1e\n" + + "\x04item\x18\x01 \x01(\v2\n" + + ".ReferenceR\x04item\x12/\n" + + "\x06status\x18\x04 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\x12)\n" + + "\vhealthAfter\x18\x05 \x01(\x0e2\a.HealthR\vhealthAfter\"\xd7\x01\n" + + "\bItemDiff\x12#\n" + + "\x04item\x18\x01 \x01(\v2\n" + + ".ReferenceH\x00R\x04item\x88\x01\x01\x12/\n" + + "\x06status\x18\x02 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\x12\x1d\n" + + "\x06before\x18\x03 \x01(\v2\x05.ItemR\x06before\x12\x1b\n" + + "\x05after\x18\x04 \x01(\v2\x05.ItemR\x05after\x120\n" + + "\x13modificationSummary\x18\x05 \x01(\tR\x13modificationSummaryB\a\n" + + "\x05_item\"\x9f\x01\n" + + "\fEnrichedTags\x12?\n" + + "\btagValue\x18\x12 \x03(\v2#.changes.EnrichedTags.TagValueEntryR\btagValue\x1aN\n" + + "\rTagValueEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12'\n" + + "\x05value\x18\x02 \x01(\v2\x11.changes.TagValueR\x05value:\x028\x01\"\x8d\x01\n" + + "\bTagValue\x12;\n" + + "\fuserTagValue\x18\x01 \x01(\v2\x15.changes.UserTagValueH\x00R\fuserTagValue\x12;\n" + + "\fautoTagValue\x18\x02 \x01(\v2\x15.changes.AutoTagValueH\x00R\fautoTagValueB\a\n" + + "\x05value\"$\n" + + "\fUserTagValue\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\"B\n" + + "\fAutoTagValue\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\x12\x1c\n" + + "\treasoning\x18\x02 \x01(\tR\treasoning\"\xcb\x01\n" + + "\x05Label\x12&\n" + + "\x04type\x18\x01 \x01(\x0e2\x12.changes.LabelTypeR\x04type\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + + "\x06colour\x18\x03 \x01(\tR\x06colour\x12$\n" + + "\rlabelRuleUUID\x18\x04 \x01(\fR\rlabelRuleUUID\x12.\n" + + "\x12autoLabelReasoning\x18\x05 \x01(\tR\x12autoLabelReasoning\x12\x18\n" + + "\askipped\x18\x06 \x01(\bR\askipped\"\xa1\x06\n" + + "\rChangeSummary\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12-\n" + + "\x06status\x18\x03 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\x12\x1e\n" + + "\n" + + "ticketLink\x18\x04 \x01(\tR\n" + + "ticketLink\x128\n" + + "\tcreatedAt\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12 \n" + + "\vcreatorName\x18\x06 \x01(\tR\vcreatorName\x12\"\n" + + "\fcreatorEmail\x18\x0f \x01(\tR\fcreatorEmail\x12*\n" + + "\x10numAffectedItems\x18\t \x01(\x05R\x10numAffectedItems\x12*\n" + + "\x10numAffectedEdges\x18\n" + + " \x01(\x05R\x10numAffectedEdges\x12\x1e\n" + + "\n" + + "numLowRisk\x18\v \x01(\x05R\n" + + "numLowRisk\x12$\n" + + "\rnumMediumRisk\x18\f \x01(\x05R\rnumMediumRisk\x12 \n" + + "\vnumHighRisk\x18\r \x01(\x05R\vnumHighRisk\x12 \n" + + "\vdescription\x18\x0e \x01(\tR\vdescription\x12\x12\n" + + "\x04repo\x18\x10 \x01(\tR\x04repo\x128\n" + + "\x04tags\x18\x11 \x03(\v2 .changes.ChangeSummary.TagsEntryB\x02\x18\x01R\x04tags\x129\n" + + "\fenrichedTags\x18\x12 \x01(\v2\x15.changes.EnrichedTagsR\fenrichedTags\x12&\n" + + "\x06labels\x18\x13 \x03(\v2\x0e.changes.LabelR\x06labels\x12E\n" + + "\x10githubChangeInfo\x18\x14 \x01(\v2\x19.changes.GithubChangeInfoR\x10githubChangeInfo\x1a7\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\b\x10\t\"x\n" + + "\x06Change\x123\n" + + "\bmetadata\x18\x01 \x01(\v2\x17.changes.ChangeMetadataR\bmetadata\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.changes.ChangePropertiesR\n" + + "properties\"\xb2\n" + + "\n" + + "\x0eChangeMetadata\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x128\n" + + "\tcreatedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x128\n" + + "\tupdatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12-\n" + + "\x06status\x18\x04 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\x12 \n" + + "\vcreatorName\x18\x05 \x01(\tR\vcreatorName\x12\"\n" + + "\fcreatorEmail\x18\x13 \x01(\tR\fcreatorEmail\x12*\n" + + "\x10numAffectedItems\x18\a \x01(\x05R\x10numAffectedItems\x12*\n" + + "\x10numAffectedEdges\x18\x11 \x01(\x05R\x10numAffectedEdges\x12,\n" + + "\x11numUnchangedItems\x18\b \x01(\x05R\x11numUnchangedItems\x12(\n" + + "\x0fnumCreatedItems\x18\t \x01(\x05R\x0fnumCreatedItems\x12(\n" + + "\x0fnumUpdatedItems\x18\n" + + " \x01(\x05R\x0fnumUpdatedItems\x12*\n" + + "\x10numReplacedItems\x18\x12 \x01(\x05R\x10numReplacedItems\x12(\n" + + "\x0fnumDeletedItems\x18\v \x01(\x05R\x0fnumDeletedItems\x12V\n" + + "\x13UnknownHealthChange\x18\f \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13UnknownHealthChange\x12L\n" + + "\x0eOkHealthChange\x18\r \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x0eOkHealthChange\x12V\n" + + "\x13WarningHealthChange\x18\x0e \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13WarningHealthChange\x12R\n" + + "\x11ErrorHealthChange\x18\x0f \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x11ErrorHealthChange\x12V\n" + + "\x13PendingHealthChange\x18\x10 \x01(\v2$.changes.ChangeMetadata.HealthChangeR\x13PendingHealthChange\x12E\n" + + "\x10githubChangeInfo\x18\x14 \x01(\v2\x19.changes.GithubChangeInfoR\x10githubChangeInfo\x122\n" + + "\x12total_observations\x18\x15 \x01(\rH\x00R\x11totalObservations\x88\x01\x01\x12Q\n" + + "\x14changeAnalysisStatus\x18\x16 \x01(\v2\x1d.changes.ChangeAnalysisStatusR\x14changeAnalysisStatus\x1a^\n" + + "\fHealthChange\x12\x14\n" + + "\x05added\x18\x01 \x01(\x05R\x05added\x12\x18\n" + + "\aremoved\x18\x02 \x01(\x05R\aremoved\x12\x1e\n" + + "\n" + + "finalTotal\x18\x03 \x01(\x05R\n" + + "finalTotalB\x15\n" + + "\x13_total_observationsJ\x04\b\x06\x10\a\"\x8c\x06\n" + + "\x10ChangeProperties\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1e\n" + + "\n" + + "ticketLink\x18\x04 \x01(\tR\n" + + "ticketLink\x12\x14\n" + + "\x05owner\x18\x05 \x01(\tR\x05owner\x12\x1a\n" + + "\bccEmails\x18\x06 \x01(\tR\bccEmails\x12<\n" + + "\x19changingItemsBookmarkUUID\x18\a \x01(\fR\x19changingItemsBookmarkUUID\x128\n" + + "\x17blastRadiusSnapshotUUID\x18\v \x01(\fR\x17blastRadiusSnapshotUUID\x12:\n" + + "\x18systemBeforeSnapshotUUID\x18\b \x01(\fR\x18systemBeforeSnapshotUUID\x128\n" + + "\x17systemAfterSnapshotUUID\x18\t \x01(\fR\x17systemAfterSnapshotUUID\x129\n" + + "\x0eplannedChanges\x18\f \x03(\v2\x11.changes.ItemDiffR\x0eplannedChanges\x12\x18\n" + + "\arawPlan\x18\r \x01(\tR\arawPlan\x12 \n" + + "\vcodeChanges\x18\x0e \x01(\tR\vcodeChanges\x12\x12\n" + + "\x04repo\x18\x0f \x01(\tR\x04repo\x12;\n" + + "\x04tags\x18\x10 \x03(\v2#.changes.ChangeProperties.TagsEntryB\x02\x18\x01R\x04tags\x129\n" + + "\fenrichedTags\x18\x12 \x01(\v2\x15.changes.EnrichedTagsR\fenrichedTags\x12&\n" + + "\x06labels\x18\x15 \x03(\v2\x0e.changes.LabelR\x06labels\x1a7\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x01\x10\x02J\x04\b\n" + + "\x10\vJ\x04\b\x11\x10\x12J\x04\b\x13\x10\x14J\x04\b\x14\x10\x15\"\xb0\x01\n" + + "\x10GithubChangeInfo\x12&\n" + + "\x0eauthorUsername\x18\x01 \x01(\tR\x0eauthorUsername\x12&\n" + + "\x0eauthorFullName\x18\x02 \x01(\tR\x0eauthorFullName\x12*\n" + + "\x10authorAvatarLink\x18\x03 \x01(\tR\x10authorAvatarLink\x12 \n" + + "\vauthorEmail\x18\x04 \x01(\tR\vauthorEmail\"\x14\n" + + "\x12ListChangesRequest\"@\n" + + "\x13ListChangesResponse\x12)\n" + + "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"K\n" + + "\x1aListChangesByStatusRequest\x12-\n" + + "\x06status\x18\x01 \x01(\x0e2\x15.changes.ChangeStatusR\x06status\"H\n" + + "\x1bListChangesByStatusResponse\x12)\n" + + "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"P\n" + + "\x13CreateChangeRequest\x129\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x19.changes.ChangePropertiesR\n" + + "properties\"?\n" + + "\x14CreateChangeResponse\x12'\n" + + "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\":\n" + + "\x10GetChangeRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + + "\x04slim\x18\x02 \x01(\bR\x04slim\">\n" + + "\x1cGetChangeByTicketLinkRequest\x12\x1e\n" + + "\n" + + "ticketLink\x18\x01 \x01(\tR\n" + + "ticketLink\"\xee\x01\n" + + "\x17GetChangeSummaryRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x12\n" + + "\x04slim\x18\x02 \x01(\bR\x04slim\x12K\n" + + "\x12changeOutputFormat\x18\x03 \x01(\x0e2\x1b.changes.ChangeOutputFormatR\x12changeOutputFormat\x12F\n" + + "\x12riskSeverityFilter\x18\x04 \x03(\x0e2\x16.changes.Risk.SeverityR\x12riskSeverityFilter\x12\x16\n" + + "\x06appURL\x18\x05 \x01(\tR\x06appURL\"2\n" + + "\x18GetChangeSummaryResponse\x12\x16\n" + + "\x06change\x18\x01 \x01(\tR\x06change\"z\n" + + "\x17GetChangeSignalsRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12K\n" + + "\x12changeOutputFormat\x18\x02 \x01(\x0e2\x1b.changes.ChangeOutputFormatR\x12changeOutputFormat\"4\n" + + "\x18GetChangeSignalsResponse\x12\x18\n" + + "\asignals\x18\x01 \x01(\tR\asignals\"<\n" + + "\x11GetChangeResponse\x12'\n" + + "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\"+\n" + + "\x15GetChangeRisksRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\xf4\x01\n" + + "\x12ChangeRiskMetadata\x12Q\n" + + "\x14changeAnalysisStatus\x18\x01 \x01(\v2\x1d.changes.ChangeAnalysisStatusR\x14changeAnalysisStatus\x12#\n" + + "\x05risks\x18\x05 \x03(\v2\r.changes.RiskR\x05risks\x12\x1e\n" + + "\n" + + "numLowRisk\x18\x06 \x01(\x05R\n" + + "numLowRisk\x12$\n" + + "\rnumMediumRisk\x18\a \x01(\x05R\rnumMediumRisk\x12 \n" + + "\vnumHighRisk\x18\b \x01(\x05R\vnumHighRisk\"e\n" + + "\x16GetChangeRisksResponse\x12K\n" + + "\x12changeRiskMetadata\x18\x01 \x01(\v2\x1b.changes.ChangeRiskMetadataR\x12changeRiskMetadata\"d\n" + + "\x13UpdateChangeRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x129\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x19.changes.ChangePropertiesR\n" + + "properties\"?\n" + + "\x14UpdateChangeResponse\x12'\n" + + "\x06change\x18\x01 \x01(\v2\x0f.changes.ChangeR\x06change\")\n" + + "\x13DeleteChangeRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"6\n" + + " ListChangesBySnapshotUUIDRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"N\n" + + "!ListChangesBySnapshotUUIDResponse\x12)\n" + + "\achanges\x18\x01 \x03(\v2\x0f.changes.ChangeR\achanges\"\x16\n" + + "\x14DeleteChangeResponse\"\x15\n" + + "\x13RefreshStateRequest\"\x16\n" + + "\x14RefreshStateResponse\"4\n" + + "\x12StartChangeRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"\xed\x01\n" + + "\x13StartChangeResponse\x128\n" + + "\x05state\x18\x01 \x01(\x0e2\".changes.StartChangeResponse.StateR\x05state\x12\x1a\n" + + "\bnumItems\x18\x02 \x01(\rR\bnumItems\x12\x1a\n" + + "\bNumEdges\x18\x03 \x01(\rR\bNumEdges\"d\n" + + "\x05State\x12\x15\n" + + "\x11STATE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15STATE_TAKING_SNAPSHOT\x10\x01\x12\x19\n" + + "\x15STATE_SAVING_SNAPSHOT\x10\x02\x12\x0e\n" + + "\n" + + "STATE_DONE\x10\x03\"2\n" + + "\x10EndChangeRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"\xe9\x01\n" + + "\x11EndChangeResponse\x126\n" + + "\x05state\x18\x01 \x01(\x0e2 .changes.EndChangeResponse.StateR\x05state\x12\x1a\n" + + "\bnumItems\x18\x02 \x01(\rR\bnumItems\x12\x1a\n" + + "\bNumEdges\x18\x03 \x01(\rR\bNumEdges\"d\n" + + "\x05State\x12\x15\n" + + "\x11STATE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15STATE_TAKING_SNAPSHOT\x10\x01\x12\x19\n" + + "\x15STATE_SAVING_SNAPSHOT\x10\x02\x12\x0e\n" + + "\n" + + "STATE_DONE\x10\x03\"\x1b\n" + + "\x19StartChangeSimpleResponse\"_\n" + + "\x17EndChangeSimpleResponse\x12\x16\n" + + "\x06queued\x18\x01 \x01(\bR\x06queued\x12,\n" + + "\x12queued_after_start\x18\x02 \x01(\bR\x10queuedAfterStart\"\x96\x02\n" + + "\x04Risk\x12\x12\n" + + "\x04UUID\x18\x05 \x01(\fR\x04UUID\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x122\n" + + "\bseverity\x18\x02 \x01(\x0e2\x16.changes.Risk.SeverityR\bseverity\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12.\n" + + "\frelatedItems\x18\x04 \x03(\v2\n" + + ".ReferenceR\frelatedItems\"^\n" + + "\bSeverity\x12\x18\n" + + "\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fSEVERITY_LOW\x10\x01\x12\x13\n" + + "\x0fSEVERITY_MEDIUM\x10\x02\x12\x11\n" + + "\rSEVERITY_HIGH\x10\x03\"\xca\x01\n" + + "\x14ChangeAnalysisStatus\x12<\n" + + "\x06status\x18\x01 \x01(\x0e2$.changes.ChangeAnalysisStatus.StatusR\x06status\"n\n" + + "\x06Status\x12\x16\n" + + "\x12STATUS_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11STATUS_INPROGRESS\x10\x01\x12\x12\n" + + "\x0eSTATUS_SKIPPED\x10\x02\x12\x0f\n" + + "\vSTATUS_DONE\x10\x03\x12\x10\n" + + "\fSTATUS_ERROR\x10\x04J\x04\b\x05\x10\x06\"4\n" + + "\x16GenerateRiskFixRequest\x12\x1a\n" + + "\briskUUID\x18\x01 \x01(\fR\briskUUID\"?\n" + + "\x17GenerateRiskFixResponse\x12$\n" + + "\rfixSuggestion\x18\x01 \x01(\tR\rfixSuggestion*\xf6\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\x120\n" + + ",MAPPED_ITEM_TIMELINE_STATUS_PENDING_CREATION\x10\x04*\xf0\x01\n" + + "\x17MappedItemMappingStatus\x12*\n" + + "&MAPPED_ITEM_MAPPING_STATUS_UNSPECIFIED\x10\x00\x12&\n" + + "\"MAPPED_ITEM_MAPPING_STATUS_SUCCESS\x10\x01\x12*\n" + + "&MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED\x10\x02\x12/\n" + + "+MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION\x10\x03\x12$\n" + + " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xf9\x01\n" + + "\x10HypothesisStatus\x12.\n" + + "*INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED\x10\x00\x12*\n" + + "&INVESTIGATED_HYPOTHESIS_STATUS_FORMING\x10\x01\x120\n" + + ",INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING\x10\x02\x12)\n" + + "%INVESTIGATED_HYPOTHESIS_STATUS_PROVEN\x10\x03\x12,\n" + + "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04*_\n" + + "\x19ChangeTimelineEntryStatus\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\v\n" + + "\aPENDING\x10\x01\x12\x0f\n" + + "\vIN_PROGRESS\x10\x02\x12\b\n" + + "\x04DONE\x10\x03\x12\t\n" + + "\x05ERROR\x10\x04*\xcb\x01\n" + + "\x0eItemDiffStatus\x12 \n" + + "\x1cITEM_DIFF_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aITEM_DIFF_STATUS_UNCHANGED\x10\x01\x12\x1c\n" + + "\x18ITEM_DIFF_STATUS_CREATED\x10\x02\x12\x1c\n" + + "\x18ITEM_DIFF_STATUS_UPDATED\x10\x03\x12\x1c\n" + + "\x18ITEM_DIFF_STATUS_DELETED\x10\x04\x12\x1d\n" + + "\x19ITEM_DIFF_STATUS_REPLACED\x10\x05*|\n" + + "\x12ChangeOutputFormat\x12$\n" + + " CHANGE_OUTPUT_FORMAT_UNSPECIFIED\x10\x00\x12\x1d\n" + + "\x19CHANGE_OUTPUT_FORMAT_JSON\x10\x01\x12!\n" + + "\x1dCHANGE_OUTPUT_FORMAT_MARKDOWN\x10\x02*Q\n" + + "\tLabelType\x12\x1a\n" + + "\x16LABEL_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fLABEL_TYPE_AUTO\x10\x01\x12\x13\n" + + "\x0fLABEL_TYPE_USER\x10\x02*\xa0\x01\n" + + "\fChangeStatus\x12\x1d\n" + + "\x19CHANGE_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + + "\x16CHANGE_STATUS_DEFINING\x10\x01\x12\x1b\n" + + "\x17CHANGE_STATUS_HAPPENING\x10\x02\x12 \n" + + "\x18CHANGE_STATUS_PROCESSING\x10\x03\x1a\x02\b\x01\x12\x16\n" + + "\x12CHANGE_STATUS_DONE\x10\x042\xad\x10\n" + + "\x0eChangesService\x12H\n" + + "\vListChanges\x12\x1b.changes.ListChangesRequest\x1a\x1c.changes.ListChangesResponse\x12`\n" + + "\x13ListChangesByStatus\x12#.changes.ListChangesByStatusRequest\x1a$.changes.ListChangesByStatusResponse\x12K\n" + + "\fCreateChange\x12\x1c.changes.CreateChangeRequest\x1a\x1d.changes.CreateChangeResponse\x12B\n" + + "\tGetChange\x12\x19.changes.GetChangeRequest\x1a\x1a.changes.GetChangeResponse\x12Z\n" + + "\x15GetChangeByTicketLink\x12%.changes.GetChangeByTicketLinkRequest\x1a\x1a.changes.GetChangeResponse\x12W\n" + + "\x10GetChangeSummary\x12 .changes.GetChangeSummaryRequest\x1a!.changes.GetChangeSummaryResponse\x12`\n" + + "\x13GetChangeTimelineV2\x12#.changes.GetChangeTimelineV2Request\x1a$.changes.GetChangeTimelineV2Response\x12Q\n" + + "\x0eGetChangeRisks\x12\x1e.changes.GetChangeRisksRequest\x1a\x1f.changes.GetChangeRisksResponse\x12K\n" + + "\fUpdateChange\x12\x1c.changes.UpdateChangeRequest\x1a\x1d.changes.UpdateChangeResponse\x12K\n" + + "\fDeleteChange\x12\x1c.changes.DeleteChangeRequest\x1a\x1d.changes.DeleteChangeResponse\x12r\n" + + "\x19ListChangesBySnapshotUUID\x12).changes.ListChangesBySnapshotUUIDRequest\x1a*.changes.ListChangesBySnapshotUUIDResponse\x12K\n" + + "\fRefreshState\x12\x1c.changes.RefreshStateRequest\x1a\x1d.changes.RefreshStateResponse\x12J\n" + + "\vStartChange\x12\x1b.changes.StartChangeRequest\x1a\x1c.changes.StartChangeResponse0\x01\x12D\n" + + "\tEndChange\x12\x19.changes.EndChangeRequest\x1a\x1a.changes.EndChangeResponse0\x01\x12T\n" + + "\x11StartChangeSimple\x12\x1b.changes.StartChangeRequest\x1a\".changes.StartChangeSimpleResponse\x12N\n" + + "\x0fEndChangeSimple\x12\x19.changes.EndChangeRequest\x1a .changes.EndChangeSimpleResponse\x12T\n" + + "\x0fListHomeChanges\x12\x1f.changes.ListHomeChangesRequest\x1a .changes.ListHomeChangesResponse\x12`\n" + + "\x13StartChangeAnalysis\x12#.changes.StartChangeAnalysisRequest\x1a$.changes.StartChangeAnalysisResponse\x12o\n" + + "\x18ListChangingItemsSummary\x12(.changes.ListChangingItemsSummaryRequest\x1a).changes.ListChangingItemsSummaryResponse\x12<\n" + + "\aGetDiff\x12\x17.changes.GetDiffRequest\x1a\x18.changes.GetDiffResponse\x12f\n" + + "\x15PopulateChangeFilters\x12%.changes.PopulateChangeFiltersRequest\x1a&.changes.PopulateChangeFiltersResponse\x12T\n" + + "\x0fGenerateRiskFix\x12\x1f.changes.GenerateRiskFixRequest\x1a .changes.GenerateRiskFixResponse\x12c\n" + + "\x14GetHypothesesDetails\x12$.changes.GetHypothesesDetailsRequest\x1a%.changes.GetHypothesesDetailsResponse\x12W\n" + + "\x10GetChangeSignals\x12 .changes.GetChangeSignalsRequest\x1a!.changes.GetChangeSignalsResponse2\xfc\x04\n" + + "\fLabelService\x12Q\n" + + "\x0eListLabelRules\x12\x1e.changes.ListLabelRulesRequest\x1a\x1f.changes.ListLabelRulesResponse\x12T\n" + + "\x0fCreateLabelRule\x12\x1f.changes.CreateLabelRuleRequest\x1a .changes.CreateLabelRuleResponse\x12K\n" + + "\fGetLabelRule\x12\x1c.changes.GetLabelRuleRequest\x1a\x1d.changes.GetLabelRuleResponse\x12T\n" + + "\x0fUpdateLabelRule\x12\x1f.changes.UpdateLabelRuleRequest\x1a .changes.UpdateLabelRuleResponse\x12T\n" + + "\x0fDeleteLabelRule\x12\x1f.changes.DeleteLabelRuleRequest\x1a .changes.DeleteLabelRuleResponse\x12P\n" + + "\rTestLabelRule\x12\x1d.changes.TestLabelRuleRequest\x1a\x1e.changes.TestLabelRuleResponse0\x01\x12x\n" + + "\x1bReapplyLabelRuleInTimeRange\x12+.changes.ReapplyLabelRuleInTimeRangeRequest\x1a,.changes.ReapplyLabelRuleInTimeRangeResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_changes_proto_rawDescOnce sync.Once + file_changes_proto_rawDescData []byte +) + +func file_changes_proto_rawDescGZIP() []byte { + file_changes_proto_rawDescOnce.Do(func() { + file_changes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_changes_proto_rawDesc), len(file_changes_proto_rawDesc))) + }) + return file_changes_proto_rawDescData +} + +var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 12) +var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 97) +var file_changes_proto_goTypes = []any{ + (MappedItemTimelineStatus)(0), // 0: changes.MappedItemTimelineStatus + (MappedItemMappingStatus)(0), // 1: changes.MappedItemMappingStatus + (HypothesisStatus)(0), // 2: changes.HypothesisStatus + (ChangeTimelineEntryStatus)(0), // 3: changes.ChangeTimelineEntryStatus + (ItemDiffStatus)(0), // 4: changes.ItemDiffStatus + (ChangeOutputFormat)(0), // 5: changes.ChangeOutputFormat + (LabelType)(0), // 6: changes.LabelType + (ChangeStatus)(0), // 7: changes.ChangeStatus + (StartChangeResponse_State)(0), // 8: changes.StartChangeResponse.State + (EndChangeResponse_State)(0), // 9: changes.EndChangeResponse.State + (Risk_Severity)(0), // 10: changes.Risk.Severity + (ChangeAnalysisStatus_Status)(0), // 11: changes.ChangeAnalysisStatus.Status + (*LabelRule)(nil), // 12: changes.LabelRule + (*LabelRuleMetadata)(nil), // 13: changes.LabelRuleMetadata + (*LabelRuleProperties)(nil), // 14: changes.LabelRuleProperties + (*ListLabelRulesRequest)(nil), // 15: changes.ListLabelRulesRequest + (*ListLabelRulesResponse)(nil), // 16: changes.ListLabelRulesResponse + (*CreateLabelRuleRequest)(nil), // 17: changes.CreateLabelRuleRequest + (*CreateLabelRuleResponse)(nil), // 18: changes.CreateLabelRuleResponse + (*GetLabelRuleRequest)(nil), // 19: changes.GetLabelRuleRequest + (*GetLabelRuleResponse)(nil), // 20: changes.GetLabelRuleResponse + (*UpdateLabelRuleRequest)(nil), // 21: changes.UpdateLabelRuleRequest + (*UpdateLabelRuleResponse)(nil), // 22: changes.UpdateLabelRuleResponse + (*DeleteLabelRuleRequest)(nil), // 23: changes.DeleteLabelRuleRequest + (*DeleteLabelRuleResponse)(nil), // 24: changes.DeleteLabelRuleResponse + (*TestLabelRuleRequest)(nil), // 25: changes.TestLabelRuleRequest + (*TestLabelRuleResponse)(nil), // 26: changes.TestLabelRuleResponse + (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 27: changes.ReapplyLabelRuleInTimeRangeRequest + (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 28: changes.ReapplyLabelRuleInTimeRangeResponse + (*GetHypothesesDetailsRequest)(nil), // 29: changes.GetHypothesesDetailsRequest + (*GetHypothesesDetailsResponse)(nil), // 30: changes.GetHypothesesDetailsResponse + (*HypothesesDetails)(nil), // 31: changes.HypothesesDetails + (*GetChangeTimelineV2Request)(nil), // 32: changes.GetChangeTimelineV2Request + (*GetChangeTimelineV2Response)(nil), // 33: changes.GetChangeTimelineV2Response + (*ChangeTimelineEntryV2)(nil), // 34: changes.ChangeTimelineEntryV2 + (*EmptyContent)(nil), // 35: changes.EmptyContent + (*MappedItemTimelineSummary)(nil), // 36: changes.MappedItemTimelineSummary + (*MappedItemsTimelineEntry)(nil), // 37: changes.MappedItemsTimelineEntry + (*CalculatedBlastRadiusTimelineEntry)(nil), // 38: changes.CalculatedBlastRadiusTimelineEntry + (*RecordObservationsTimelineEntry)(nil), // 39: changes.RecordObservationsTimelineEntry + (*FormHypothesesTimelineEntry)(nil), // 40: changes.FormHypothesesTimelineEntry + (*InvestigateHypothesesTimelineEntry)(nil), // 41: changes.InvestigateHypothesesTimelineEntry + (*HypothesisSummary)(nil), // 42: changes.HypothesisSummary + (*CalculatedRisksTimelineEntry)(nil), // 43: changes.CalculatedRisksTimelineEntry + (*CalculatedLabelsTimelineEntry)(nil), // 44: changes.CalculatedLabelsTimelineEntry + (*ChangeValidationTimelineEntry)(nil), // 45: changes.ChangeValidationTimelineEntry + (*ChangeValidationCategory)(nil), // 46: changes.ChangeValidationCategory + (*GetDiffRequest)(nil), // 47: changes.GetDiffRequest + (*GetDiffResponse)(nil), // 48: changes.GetDiffResponse + (*ListChangingItemsSummaryRequest)(nil), // 49: changes.ListChangingItemsSummaryRequest + (*ListChangingItemsSummaryResponse)(nil), // 50: changes.ListChangingItemsSummaryResponse + (*MappedItemDiff)(nil), // 51: changes.MappedItemDiff + (*StartChangeAnalysisRequest)(nil), // 52: changes.StartChangeAnalysisRequest + (*StartChangeAnalysisResponse)(nil), // 53: changes.StartChangeAnalysisResponse + (*ListHomeChangesRequest)(nil), // 54: changes.ListHomeChangesRequest + (*ChangeFiltersRequest)(nil), // 55: changes.ChangeFiltersRequest + (*ListHomeChangesResponse)(nil), // 56: changes.ListHomeChangesResponse + (*PopulateChangeFiltersRequest)(nil), // 57: changes.PopulateChangeFiltersRequest + (*PopulateChangeFiltersResponse)(nil), // 58: changes.PopulateChangeFiltersResponse + (*ItemDiffSummary)(nil), // 59: changes.ItemDiffSummary + (*ItemDiff)(nil), // 60: changes.ItemDiff + (*EnrichedTags)(nil), // 61: changes.EnrichedTags + (*TagValue)(nil), // 62: changes.TagValue + (*UserTagValue)(nil), // 63: changes.UserTagValue + (*AutoTagValue)(nil), // 64: changes.AutoTagValue + (*Label)(nil), // 65: changes.Label + (*ChangeSummary)(nil), // 66: changes.ChangeSummary + (*Change)(nil), // 67: changes.Change + (*ChangeMetadata)(nil), // 68: changes.ChangeMetadata + (*ChangeProperties)(nil), // 69: changes.ChangeProperties + (*GithubChangeInfo)(nil), // 70: changes.GithubChangeInfo + (*ListChangesRequest)(nil), // 71: changes.ListChangesRequest + (*ListChangesResponse)(nil), // 72: changes.ListChangesResponse + (*ListChangesByStatusRequest)(nil), // 73: changes.ListChangesByStatusRequest + (*ListChangesByStatusResponse)(nil), // 74: changes.ListChangesByStatusResponse + (*CreateChangeRequest)(nil), // 75: changes.CreateChangeRequest + (*CreateChangeResponse)(nil), // 76: changes.CreateChangeResponse + (*GetChangeRequest)(nil), // 77: changes.GetChangeRequest + (*GetChangeByTicketLinkRequest)(nil), // 78: changes.GetChangeByTicketLinkRequest + (*GetChangeSummaryRequest)(nil), // 79: changes.GetChangeSummaryRequest + (*GetChangeSummaryResponse)(nil), // 80: changes.GetChangeSummaryResponse + (*GetChangeSignalsRequest)(nil), // 81: changes.GetChangeSignalsRequest + (*GetChangeSignalsResponse)(nil), // 82: changes.GetChangeSignalsResponse + (*GetChangeResponse)(nil), // 83: changes.GetChangeResponse + (*GetChangeRisksRequest)(nil), // 84: changes.GetChangeRisksRequest + (*ChangeRiskMetadata)(nil), // 85: changes.ChangeRiskMetadata + (*GetChangeRisksResponse)(nil), // 86: changes.GetChangeRisksResponse + (*UpdateChangeRequest)(nil), // 87: changes.UpdateChangeRequest + (*UpdateChangeResponse)(nil), // 88: changes.UpdateChangeResponse + (*DeleteChangeRequest)(nil), // 89: changes.DeleteChangeRequest + (*ListChangesBySnapshotUUIDRequest)(nil), // 90: changes.ListChangesBySnapshotUUIDRequest + (*ListChangesBySnapshotUUIDResponse)(nil), // 91: changes.ListChangesBySnapshotUUIDResponse + (*DeleteChangeResponse)(nil), // 92: changes.DeleteChangeResponse + (*RefreshStateRequest)(nil), // 93: changes.RefreshStateRequest + (*RefreshStateResponse)(nil), // 94: changes.RefreshStateResponse + (*StartChangeRequest)(nil), // 95: changes.StartChangeRequest + (*StartChangeResponse)(nil), // 96: changes.StartChangeResponse + (*EndChangeRequest)(nil), // 97: changes.EndChangeRequest + (*EndChangeResponse)(nil), // 98: changes.EndChangeResponse + (*StartChangeSimpleResponse)(nil), // 99: changes.StartChangeSimpleResponse + (*EndChangeSimpleResponse)(nil), // 100: changes.EndChangeSimpleResponse + (*Risk)(nil), // 101: changes.Risk + (*ChangeAnalysisStatus)(nil), // 102: changes.ChangeAnalysisStatus + (*GenerateRiskFixRequest)(nil), // 103: changes.GenerateRiskFixRequest + (*GenerateRiskFixResponse)(nil), // 104: changes.GenerateRiskFixResponse + nil, // 105: changes.EnrichedTags.TagValueEntry + nil, // 106: changes.ChangeSummary.TagsEntry + (*ChangeMetadata_HealthChange)(nil), // 107: changes.ChangeMetadata.HealthChange + nil, // 108: changes.ChangeProperties.TagsEntry + (*timestamppb.Timestamp)(nil), // 109: google.protobuf.Timestamp + (*Edge)(nil), // 110: Edge + (*Query)(nil), // 111: Query + (*QueryError)(nil), // 112: QueryError + (*BlastRadiusConfig)(nil), // 113: config.BlastRadiusConfig + (*RoutineChangesConfig)(nil), // 114: config.RoutineChangesConfig + (*GithubOrganisationProfile)(nil), // 115: config.GithubOrganisationProfile + (*PaginationRequest)(nil), // 116: PaginationRequest + (SortOrder)(0), // 117: SortOrder + (*PaginationResponse)(nil), // 118: PaginationResponse + (*Reference)(nil), // 119: Reference + (Health)(0), // 120: Health + (*Item)(nil), // 121: Item +} +var file_changes_proto_depIdxs = []int32{ + 13, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata + 14, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties + 109, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp + 109, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 12, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule + 14, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 12, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule + 12, // 7: changes.GetLabelRuleResponse.rule:type_name -> changes.LabelRule + 14, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 12, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule + 14, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties + 65, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label + 109, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp + 109, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp + 31, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails + 2, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus + 34, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 + 3, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus + 109, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp + 109, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp + 37, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry + 38, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry + 43, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry + 35, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent + 45, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry + 44, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry + 40, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry + 41, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry + 39, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry + 0, // 29: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus + 51, // 30: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff + 36, // 31: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary + 42, // 32: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 42, // 33: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 2, // 34: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus + 101, // 35: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk + 65, // 36: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label + 46, // 37: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory + 60, // 38: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff + 60, // 39: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff + 110, // 40: changes.GetDiffResponse.edges:type_name -> Edge + 60, // 41: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff + 59, // 42: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary + 60, // 43: changes.MappedItemDiff.item:type_name -> changes.ItemDiff + 111, // 44: changes.MappedItemDiff.mappingQuery:type_name -> Query + 112, // 45: changes.MappedItemDiff.mappingError:type_name -> QueryError + 1, // 46: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus + 51, // 47: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff + 113, // 48: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig + 114, // 49: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig + 115, // 50: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile + 116, // 51: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest + 55, // 52: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest + 10, // 53: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity + 7, // 54: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus + 117, // 55: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder + 66, // 56: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary + 118, // 57: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse + 119, // 58: changes.ItemDiffSummary.item:type_name -> Reference + 4, // 59: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus + 120, // 60: changes.ItemDiffSummary.healthAfter:type_name -> Health + 119, // 61: changes.ItemDiff.item:type_name -> Reference + 4, // 62: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus + 121, // 63: changes.ItemDiff.before:type_name -> Item + 121, // 64: changes.ItemDiff.after:type_name -> Item + 105, // 65: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry + 63, // 66: changes.TagValue.userTagValue:type_name -> changes.UserTagValue + 64, // 67: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue + 6, // 68: changes.Label.type:type_name -> changes.LabelType + 7, // 69: changes.ChangeSummary.status:type_name -> changes.ChangeStatus + 109, // 70: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp + 106, // 71: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry + 61, // 72: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags + 65, // 73: changes.ChangeSummary.labels:type_name -> changes.Label + 70, // 74: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo + 68, // 75: changes.Change.metadata:type_name -> changes.ChangeMetadata + 69, // 76: changes.Change.properties:type_name -> changes.ChangeProperties + 109, // 77: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp + 109, // 78: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 7, // 79: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus + 107, // 80: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 107, // 81: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 107, // 82: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 107, // 83: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 107, // 84: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 70, // 85: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo + 102, // 86: changes.ChangeMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 60, // 87: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff + 108, // 88: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry + 61, // 89: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags + 65, // 90: changes.ChangeProperties.labels:type_name -> changes.Label + 67, // 91: changes.ListChangesResponse.changes:type_name -> changes.Change + 7, // 92: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus + 67, // 93: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change + 69, // 94: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties + 67, // 95: changes.CreateChangeResponse.change:type_name -> changes.Change + 5, // 96: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 10, // 97: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity + 5, // 98: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 67, // 99: changes.GetChangeResponse.change:type_name -> changes.Change + 102, // 100: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 101, // 101: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk + 85, // 102: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 69, // 103: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties + 67, // 104: changes.UpdateChangeResponse.change:type_name -> changes.Change + 67, // 105: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change + 8, // 106: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State + 9, // 107: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State + 10, // 108: changes.Risk.severity:type_name -> changes.Risk.Severity + 119, // 109: changes.Risk.relatedItems:type_name -> Reference + 11, // 110: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status + 62, // 111: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue + 71, // 112: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest + 73, // 113: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest + 75, // 114: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest + 77, // 115: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest + 78, // 116: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest + 79, // 117: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest + 32, // 118: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request + 84, // 119: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest + 87, // 120: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest + 89, // 121: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest + 90, // 122: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest + 93, // 123: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest + 95, // 124: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest + 97, // 125: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest + 95, // 126: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest + 97, // 127: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest + 54, // 128: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest + 52, // 129: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest + 49, // 130: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest + 47, // 131: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest + 57, // 132: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest + 103, // 133: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest + 29, // 134: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest + 81, // 135: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest + 15, // 136: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest + 17, // 137: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest + 19, // 138: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest + 21, // 139: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest + 23, // 140: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest + 25, // 141: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest + 27, // 142: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest + 72, // 143: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse + 74, // 144: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse + 76, // 145: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse + 83, // 146: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse + 83, // 147: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse + 80, // 148: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse + 33, // 149: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response + 86, // 150: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse + 88, // 151: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse + 92, // 152: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse + 91, // 153: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse + 94, // 154: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse + 96, // 155: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse + 98, // 156: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse + 99, // 157: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse + 100, // 158: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse + 56, // 159: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse + 53, // 160: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse + 50, // 161: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse + 48, // 162: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse + 58, // 163: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse + 104, // 164: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse + 30, // 165: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse + 82, // 166: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse + 16, // 167: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse + 18, // 168: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse + 20, // 169: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse + 22, // 170: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse + 24, // 171: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse + 26, // 172: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse + 28, // 173: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse + 143, // [143:174] is the sub-list for method output_type + 112, // [112:143] is the sub-list for method input_type + 112, // [112:112] is the sub-list for extension type_name + 112, // [112:112] is the sub-list for extension extendee + 0, // [0:112] is the sub-list for field type_name +} + +func init() { file_changes_proto_init() } +func file_changes_proto_init() { + if File_changes_proto != nil { + return + } + file_config_proto_init() + file_items_proto_init() + file_util_proto_init() + file_changes_proto_msgTypes[22].OneofWrappers = []any{ + (*ChangeTimelineEntryV2_MappedItems)(nil), + (*ChangeTimelineEntryV2_CalculatedBlastRadius)(nil), + (*ChangeTimelineEntryV2_CalculatedRisks)(nil), + (*ChangeTimelineEntryV2_Error)(nil), + (*ChangeTimelineEntryV2_StatusMessage)(nil), + (*ChangeTimelineEntryV2_Empty)(nil), + (*ChangeTimelineEntryV2_ChangeValidation)(nil), + (*ChangeTimelineEntryV2_CalculatedLabels)(nil), + (*ChangeTimelineEntryV2_FormHypotheses)(nil), + (*ChangeTimelineEntryV2_InvestigateHypotheses)(nil), + (*ChangeTimelineEntryV2_RecordObservations)(nil), + } + file_changes_proto_msgTypes[24].OneofWrappers = []any{} + file_changes_proto_msgTypes[39].OneofWrappers = []any{} + file_changes_proto_msgTypes[40].OneofWrappers = []any{} + file_changes_proto_msgTypes[42].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[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: 12, + NumMessages: 97, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_changes_proto_goTypes, + DependencyIndexes: file_changes_proto_depIdxs, + EnumInfos: file_changes_proto_enumTypes, + MessageInfos: file_changes_proto_msgTypes, + }.Build() + File_changes_proto = out.File + file_changes_proto_goTypes = nil + file_changes_proto_depIdxs = nil +} diff --git a/go/sdp-go/changes_test.go b/go/sdp-go/changes_test.go new file mode 100644 index 00000000..26300a24 --- /dev/null +++ b/go/sdp-go/changes_test.go @@ -0,0 +1,474 @@ +package sdp + +import ( + "strings" + "testing" +) + +func TestFindInProgressEntry(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + entries []*ChangeTimelineEntryV2 + expectedName string + expectedStatus ChangeTimelineEntryStatus + expectError bool + }{ + { + name: "nil entries", + entries: nil, + expectedName: "", + expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, + expectError: true, + }, + { + name: "empty entries", + entries: []*ChangeTimelineEntryV2{}, + expectedName: "", + expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, + expectError: true, + }, + { + name: "in progress entry", + entries: []*ChangeTimelineEntryV2{ + { + Name: "entry1", + Status: ChangeTimelineEntryStatus_IN_PROGRESS, + }, + { + Name: "entry2", + Status: ChangeTimelineEntryStatus_PENDING, + }, + }, + expectedName: "entry1", + expectedStatus: ChangeTimelineEntryStatus_IN_PROGRESS, + expectError: false, + }, + { + name: "pending entry", + entries: []*ChangeTimelineEntryV2{ + { + Name: "entry1", + Status: ChangeTimelineEntryStatus_DONE, + }, + { + Name: "entry2", + Status: ChangeTimelineEntryStatus_PENDING, + }, + }, + expectedName: "entry2", + expectedStatus: ChangeTimelineEntryStatus_PENDING, + expectError: false, + }, + { + name: "error entry", + entries: []*ChangeTimelineEntryV2{ + { + Name: "entry1", + Status: ChangeTimelineEntryStatus_DONE, + }, + { + Name: "entry2", + Status: ChangeTimelineEntryStatus_ERROR, + }, + }, + expectedName: "entry2", + expectedStatus: ChangeTimelineEntryStatus_ERROR, + expectError: false, + }, + { + name: "no in progress entry", + entries: []*ChangeTimelineEntryV2{ + { + Name: "entry1", + Status: ChangeTimelineEntryStatus_DONE, + }, + { + Name: "entry2", + Status: ChangeTimelineEntryStatus_UNSPECIFIED, + }, + }, + expectedName: "", + expectedStatus: ChangeTimelineEntryStatus_DONE, + expectError: false, + }, + { + name: "unknown status", + entries: []*ChangeTimelineEntryV2{ + { + Name: "entry1", + Status: 100, // some unknown status + }, + }, + expectedName: "", + expectedStatus: ChangeTimelineEntryStatus_UNSPECIFIED, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, _, status, err := TimelineFindInProgressEntry(tt.entries) + + if tt.expectError && err == nil { + t.Errorf("Expected an error, got nil") + } + + if !tt.expectError && err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if name != tt.expectedName { + t.Errorf("Expected name %s, got %s", tt.expectedName, name) + } + + if status != tt.expectedStatus { + t.Errorf("Expected status %s, got %s", tt.expectedStatus, status) + } + }) + } +} + +func TestTimelineEntryContentDescription(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + entry *ChangeTimelineEntryV2 + expected string + }{ + { + name: "mapped items", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_MappedItems{ + MappedItems: &MappedItemsTimelineEntry{ + MappedItems: []*MappedItemDiff{{}, {}, {}}, + }, + }, + }, + expected: "3 mapped items", + }, + { + name: "calculated blast radius", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_CalculatedBlastRadius{ + CalculatedBlastRadius: &CalculatedBlastRadiusTimelineEntry{ + NumItems: 10, + NumEdges: 25, + }, + }, + }, + expected: "10 items, 25 edges", + }, + { + name: "calculated risks", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_CalculatedRisks{ + CalculatedRisks: &CalculatedRisksTimelineEntry{ + Risks: []*Risk{{}, {}}, + }, + }, + }, + expected: "2 risks", + }, + { + name: "calculated labels", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_CalculatedLabels{ + CalculatedLabels: &CalculatedLabelsTimelineEntry{ + Labels: []*Label{{}, {}, {}, {}}, + }, + }, + }, + expected: "4 labels", + }, + { + name: "change validation", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_ChangeValidation{ + ChangeValidation: &ChangeValidationTimelineEntry{ + ValidationChecklist: []*ChangeValidationCategory{{}}, + }, + }, + }, + expected: "1 validation categories", + }, + { + name: "form hypotheses", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_FormHypotheses{ + FormHypotheses: &FormHypothesesTimelineEntry{ + NumHypotheses: 5, + }, + }, + }, + expected: "5 hypotheses", + }, + { + name: "investigate hypotheses", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_InvestigateHypotheses{ + InvestigateHypotheses: &InvestigateHypothesesTimelineEntry{ + NumProven: 2, + NumDisproven: 3, + NumInvestigating: 1, + }, + }, + }, + expected: "2 proven, 3 disproven, 1 investigating", + }, + { + name: "record observations", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_RecordObservations{ + RecordObservations: &RecordObservationsTimelineEntry{ + NumObservations: 42, + }, + }, + }, + expected: "42 observations", + }, + { + name: "error content", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_Error{ + Error: "something went wrong", + }, + }, + expected: "something went wrong", + }, + { + name: "status message", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_StatusMessage{ + StatusMessage: "processing data", + }, + }, + expected: "processing data", + }, + { + name: "empty content", + entry: &ChangeTimelineEntryV2{ + Content: &ChangeTimelineEntryV2_Empty{ + Empty: &EmptyContent{}, + }, + }, + expected: "", + }, + { + name: "nil content", + entry: &ChangeTimelineEntryV2{ + Content: nil, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TimelineEntryContentDescription(tt.entry) + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestValidateRoutineChangesConfig(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config *RoutineChangesYAML + wantErr bool + errContains string + }{ + { + name: "valid config", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: 7.0, + Sensitivity: 0.5, + }, + wantErr: false, + }, + { + name: "valid config with minimum values", + config: &RoutineChangesYAML{ + EventsPerDay: 1.0, + DurationInDays: 1.0, + Sensitivity: 0.0, + }, + wantErr: false, + }, + { + name: "events_per_day less than 1", + config: &RoutineChangesYAML{ + EventsPerDay: 0.5, + DurationInDays: 7.0, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "events_per_day must be greater than 1", + }, + { + name: "events_per_day equals 0", + config: &RoutineChangesYAML{ + EventsPerDay: 0.0, + DurationInDays: 7.0, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "events_per_day must be greater than 1", + }, + { + name: "events_per_day negative", + config: &RoutineChangesYAML{ + EventsPerDay: -1.0, + DurationInDays: 7.0, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "events_per_day must be greater than 1", + }, + { + name: "duration_in_days less than 1", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: 0.5, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "duration_in_days must be greater than 1", + }, + { + name: "duration_in_days equals 0", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: 0.0, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "duration_in_days must be greater than 1", + }, + { + name: "duration_in_days negative", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: -1.0, + Sensitivity: 0.5, + }, + wantErr: true, + errContains: "duration_in_days must be greater than 1", + }, + { + name: "sensitivity negative", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: 7.0, + Sensitivity: -0.1, + }, + wantErr: true, + errContains: "sensitivity must be 0 or higher", + }, + { + name: "multiple invalid fields - events_per_day checked first", + config: &RoutineChangesYAML{ + EventsPerDay: 0.0, + DurationInDays: 0.0, + Sensitivity: -1.0, + }, + wantErr: true, + errContains: "events_per_day must be greater than 1", + }, + { + name: "multiple invalid fields - duration_in_days checked second", + config: &RoutineChangesYAML{ + EventsPerDay: 10.0, + DurationInDays: 0.0, + Sensitivity: -1.0, + }, + wantErr: true, + errContains: "duration_in_days must be greater than 1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRoutineChangesConfig(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("validateRoutineChangesConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && tt.errContains != "" { + if err == nil { + t.Errorf("validateRoutineChangesConfig() expected error containing %q, got nil", tt.errContains) + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("validateRoutineChangesConfig() error = %v, want error containing %q", err, tt.errContains) + } + } + }) + } +} + +func TestYamlStringToSignalConfig_NilCombinations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yamlString string + wantErr bool + wantRoutine bool + wantGithub bool + }{ + { + name: "both nil -> error", + yamlString: "{}\n", + wantErr: true, + wantRoutine: false, + wantGithub: false, + }, + { + name: "only routine present", + yamlString: "routine_changes_config:\n sensitivity: 0\n duration_in_days: 1\n events_per_day: 1\n", + wantErr: false, + wantRoutine: true, + wantGithub: false, + }, + { + name: "only github present", + yamlString: "github_organisation_profile:\n primary_branch_name: main\n", + wantErr: false, + wantRoutine: false, + wantGithub: true, + }, + { + name: "both present", + yamlString: "routine_changes_config:\n sensitivity: 0\n duration_in_days: 1\n events_per_day: 1\ngithub_organisation_profile:\n primary_branch_name: main\n", + wantErr: false, + wantRoutine: true, + wantGithub: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := YamlStringToSignalConfig(tt.yamlString) + if (err != nil) != tt.wantErr { + t.Errorf("YamlStringToSignalConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + return + } + + if (got.RoutineChangesConfig != nil) != tt.wantRoutine { + t.Errorf("RoutineChangesConfig presence = %v, want %v", got.RoutineChangesConfig != nil, tt.wantRoutine) + } + if (got.GithubOrganisationProfile != nil) != tt.wantGithub { + t.Errorf("GithubOrganisationProfile presence = %v, want %v", got.GithubOrganisationProfile != nil, tt.wantGithub) + } + }) + } +} diff --git a/go/sdp-go/changetimeline.go b/go/sdp-go/changetimeline.go new file mode 100644 index 00000000..d76c5b4f --- /dev/null +++ b/go/sdp-go/changetimeline.go @@ -0,0 +1,164 @@ +package sdp + +// If you add/delete/move an entry here, make sure to update/check the following: +// - the PopulateChangeTimelineV2 function +// - 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. +// Use the .Label field for database lookups and the .Name field for user-facing display. + +type ChangeTimelineEntryV2ID struct { + // The internal label for the entry, this is used to identify the entry in + // the database and tell whether two entries are the same type of thing. + // This means that if we want to change the way an entry behaves, we can + // create a new label and keep the old one for backwards compatibility. + Label string + // The name of the entry, this is the user facing name of the entry and can + // be changed safely. This is stored in both the code and the database, the + // reason we store it in the code is so that we know what value to populate + // in the database when we create the timeline entries in the first place, + // when returning the timeline to the user we use the name from the database + // which means that old changes will still show the old name. + 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 + // the mapping queries that were sent up, and running them against the + // gateway to see whether any of them resolve into real items. + ChangeTimelineEntryV2IDMapResources = ChangeTimelineEntryV2ID{ + Label: "mapped_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 + // taking the mapped resources and running them through the blast radius + // simulation to see how many items are in the blast radius. + ChangeTimelineEntryV2IDCalculatedBlastRadius = ChangeTimelineEntryV2ID{ + Label: "calculated_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: "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 + // no longer showing risks directly in the timeline. The risk calculation + // still happens, but the timeline focuses on Observations -> Hypotheses -> + // Investigations instead. This means that this step will be no longer used + // after Dec '25 + ChangeTimelineEntryV2IDCalculatedRisks = ChangeTimelineEntryV2ID{ + Label: "calculated_risks", + Name: "Calculated Risks", + } + // Tracks the application of auto-label rules for a change + ChangeTimelineEntryV2IDCalculatedLabels = ChangeTimelineEntryV2ID{ + Label: "calculated_labels", + Name: "Apply auto labels", + } + // Tracks the application of auto tags for a change + ChangeTimelineEntryV2IDAutoTagging = ChangeTimelineEntryV2ID{ + Label: "auto_tagging", + Name: "Auto Tagging", + } + // Tracks the validation of a change. This happens after the change is + // complete and at time of writing is not generally available + ChangeTimelineEntryV2IDChangeValidation = ChangeTimelineEntryV2ID{ + Label: "change_validation", + Name: "Change Validation", + } + // This is the entry that tracks observations being recorded during blast radius simulation + ChangeTimelineEntryV2IDRecordObservations = ChangeTimelineEntryV2ID{ + Label: "record_observations", + Name: "Record observations", + } + // This is the entry that tracks hypotheses being formed from observations via batch processing + ChangeTimelineEntryV2IDFormHypotheses = ChangeTimelineEntryV2ID{ + Label: "form_hypotheses", + Name: "Form hypotheses", + } + // This is the entry that tracks investigation of hypotheses via one-shot analysis + ChangeTimelineEntryV2IDInvestigateHypotheses = ChangeTimelineEntryV2ID{ + Label: "investigate_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/go/sdp-go/changetimeline_test.go b/go/sdp-go/changetimeline_test.go new file mode 100644 index 00000000..391d4cb4 --- /dev/null +++ b/go/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/go/sdp-go/cli.pb.go b/go/sdp-go/cli.pb.go new file mode 100644 index 00000000..e5acc064 --- /dev/null +++ b/go/sdp-go/cli.pb.go @@ -0,0 +1,270 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: cli.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigRequest) Reset() { + *x = GetConfigRequest{} + mi := &file_cli_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigRequest) ProtoMessage() {} + +func (x *GetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_cli_proto_msgTypes[0] + 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 GetConfigRequest.ProtoReflect.Descriptor instead. +func (*GetConfigRequest) Descriptor() ([]byte, []int) { + return file_cli_proto_rawDescGZIP(), []int{0} +} + +func (x *GetConfigRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type GetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetConfigResponse) Reset() { + *x = GetConfigResponse{} + mi := &file_cli_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigResponse) ProtoMessage() {} + +func (x *GetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_cli_proto_msgTypes[1] + 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 GetConfigResponse.ProtoReflect.Descriptor instead. +func (*GetConfigResponse) Descriptor() ([]byte, []int) { + return file_cli_proto_rawDescGZIP(), []int{1} +} + +func (x *GetConfigResponse) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type SetConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetConfigRequest) Reset() { + *x = SetConfigRequest{} + mi := &file_cli_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetConfigRequest) ProtoMessage() {} + +func (x *SetConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_cli_proto_msgTypes[2] + 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 SetConfigRequest.ProtoReflect.Descriptor instead. +func (*SetConfigRequest) Descriptor() ([]byte, []int) { + return file_cli_proto_rawDescGZIP(), []int{2} +} + +func (x *SetConfigRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *SetConfigRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type SetConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetConfigResponse) Reset() { + *x = SetConfigResponse{} + mi := &file_cli_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetConfigResponse) ProtoMessage() {} + +func (x *SetConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_cli_proto_msgTypes[3] + 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 SetConfigResponse.ProtoReflect.Descriptor instead. +func (*SetConfigResponse) Descriptor() ([]byte, []int) { + return file_cli_proto_rawDescGZIP(), []int{3} +} + +var File_cli_proto protoreflect.FileDescriptor + +const file_cli_proto_rawDesc = "" + + "\n" + + "\tcli.proto\x12\x03cli\"$\n" + + "\x10GetConfigRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\")\n" + + "\x11GetConfigResponse\x12\x14\n" + + "\x05value\x18\x01 \x01(\tR\x05value\":\n" + + "\x10SetConfigRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"\x13\n" + + "\x11SetConfigResponse2\x8b\x01\n" + + "\rConfigService\x12<\n" + + "\tGetConfig\x12\x15.cli.GetConfigRequest\x1a\x16.cli.GetConfigResponse\"\x00\x12<\n" + + "\tSetConfig\x12\x15.cli.SetConfigRequest\x1a\x16.cli.SetConfigResponse\"\x00B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_cli_proto_rawDescOnce sync.Once + file_cli_proto_rawDescData []byte +) + +func file_cli_proto_rawDescGZIP() []byte { + file_cli_proto_rawDescOnce.Do(func() { + file_cli_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cli_proto_rawDesc), len(file_cli_proto_rawDesc))) + }) + return file_cli_proto_rawDescData +} + +var file_cli_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_cli_proto_goTypes = []any{ + (*GetConfigRequest)(nil), // 0: cli.GetConfigRequest + (*GetConfigResponse)(nil), // 1: cli.GetConfigResponse + (*SetConfigRequest)(nil), // 2: cli.SetConfigRequest + (*SetConfigResponse)(nil), // 3: cli.SetConfigResponse +} +var file_cli_proto_depIdxs = []int32{ + 0, // 0: cli.ConfigService.GetConfig:input_type -> cli.GetConfigRequest + 2, // 1: cli.ConfigService.SetConfig:input_type -> cli.SetConfigRequest + 1, // 2: cli.ConfigService.GetConfig:output_type -> cli.GetConfigResponse + 3, // 3: cli.ConfigService.SetConfig:output_type -> cli.SetConfigResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_cli_proto_init() } +func file_cli_proto_init() { + if File_cli_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_cli_proto_rawDesc), len(file_cli_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_cli_proto_goTypes, + DependencyIndexes: file_cli_proto_depIdxs, + MessageInfos: file_cli_proto_msgTypes, + }.Build() + File_cli_proto = out.File + file_cli_proto_goTypes = nil + file_cli_proto_depIdxs = nil +} diff --git a/go/sdp-go/compare.go b/go/sdp-go/compare.go new file mode 100644 index 00000000..211d4b95 --- /dev/null +++ b/go/sdp-go/compare.go @@ -0,0 +1,41 @@ +package sdp + +import "fmt" + +// Comparer is an object that can be compared for the purposes of sorting. +// Basically anything that implements this interface is sortable +type Comparer interface { + Compare(b *Item) int +} + +// Compare compares two Items for the purposes of sorting. This sorts based on +// the string conversion of the Type, followed by the UniqueAttribute +func (i *Item) Compare(r *Item) (int, error) { + // Convert to strings + right := fmt.Sprintf("%v: %v", r.GetType(), r.UniqueAttributeValue()) + left := fmt.Sprintf("%v: %v", i.GetType(), i.UniqueAttributeValue()) + + // Compare the strings and return the value + switch { + case left > right: + return 1, nil + case left < right: + return -1, nil + default: + return 0, nil + } +} + +// CompareError is returned when two Items cannot be compared because their +// UniqueAttributeValue() is not sortable +type CompareError Item + +// Error returns the string when the error is handled +func (c *CompareError) Error() string { + return (fmt.Sprintf( + "Item %v unique attribute: %v of type %v does not implement interface fmt.Stringer. Cannot sort.", + c.Type, + c.UniqueAttribute, + c.Type, + )) +} diff --git a/go/sdp-go/config.pb.go b/go/sdp-go/config.pb.go new file mode 100644 index 00000000..c20030ae --- /dev/null +++ b/go/sdp-go/config.pb.go @@ -0,0 +1,1931 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: config.proto + +package sdp + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AccountConfig_BlastRadiusPreset int32 + +const ( + // 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 + // An optimised balance between time taken and discovery. + AccountConfig_DETAILED AccountConfig_BlastRadiusPreset = 2 + // Discovers all possible dependencies, might take a long time and + // discover items that are less likely to be relevant to a change. + AccountConfig_FULL AccountConfig_BlastRadiusPreset = 3 +) + +// Enum value maps for AccountConfig_BlastRadiusPreset. +var ( + AccountConfig_BlastRadiusPreset_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "QUICK", + 2: "DETAILED", + 3: "FULL", + } + AccountConfig_BlastRadiusPreset_value = map[string]int32{ + "UNSPECIFIED": 0, + "QUICK": 1, + "DETAILED": 2, + "FULL": 3, + } +) + +func (x AccountConfig_BlastRadiusPreset) Enum() *AccountConfig_BlastRadiusPreset { + p := new(AccountConfig_BlastRadiusPreset) + *p = x + return p +} + +func (x AccountConfig_BlastRadiusPreset) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccountConfig_BlastRadiusPreset) Descriptor() protoreflect.EnumDescriptor { + return file_config_proto_enumTypes[0].Descriptor() +} + +func (AccountConfig_BlastRadiusPreset) Type() protoreflect.EnumType { + return &file_config_proto_enumTypes[0] +} + +func (x AccountConfig_BlastRadiusPreset) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccountConfig_BlastRadiusPreset.Descriptor instead. +func (AccountConfig_BlastRadiusPreset) EnumDescriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1, 0} +} + +type GetHcpConfigResponse_Status int32 + +const ( + // The HCP Run Task configuration is active and can be used + GetHcpConfigResponse_CONFIGURED GetHcpConfigResponse_Status = 0 + // The HCP Run Task configuration is not fully configured and needs to + // be recreated, this is usually due to the API key being revoked or the + // user not completing the authorisation process + GetHcpConfigResponse_ERROR GetHcpConfigResponse_Status = 1 +) + +// Enum value maps for GetHcpConfigResponse_Status. +var ( + GetHcpConfigResponse_Status_name = map[int32]string{ + 0: "CONFIGURED", + 1: "ERROR", + } + GetHcpConfigResponse_Status_value = map[string]int32{ + "CONFIGURED": 0, + "ERROR": 1, + } +) + +func (x GetHcpConfigResponse_Status) Enum() *GetHcpConfigResponse_Status { + p := new(GetHcpConfigResponse_Status) + *p = x + return p +} + +func (x GetHcpConfigResponse_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GetHcpConfigResponse_Status) Descriptor() protoreflect.EnumDescriptor { + return file_config_proto_enumTypes[1].Descriptor() +} + +func (GetHcpConfigResponse_Status) Type() protoreflect.EnumType { + return &file_config_proto_enumTypes[1] +} + +func (x GetHcpConfigResponse_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GetHcpConfigResponse_Status.Descriptor instead. +func (GetHcpConfigResponse_Status) EnumDescriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{10, 0} +} + +type RoutineChangesConfig_DurationUnit int32 + +const ( + // Days + RoutineChangesConfig_DAYS RoutineChangesConfig_DurationUnit = 0 + // Weeks + RoutineChangesConfig_WEEKS RoutineChangesConfig_DurationUnit = 1 + // Months + RoutineChangesConfig_MONTHS RoutineChangesConfig_DurationUnit = 2 +) + +// Enum value maps for RoutineChangesConfig_DurationUnit. +var ( + RoutineChangesConfig_DurationUnit_name = map[int32]string{ + 0: "DAYS", + 1: "WEEKS", + 2: "MONTHS", + } + RoutineChangesConfig_DurationUnit_value = map[string]int32{ + "DAYS": 0, + "WEEKS": 1, + "MONTHS": 2, + } +) + +func (x RoutineChangesConfig_DurationUnit) Enum() *RoutineChangesConfig_DurationUnit { + p := new(RoutineChangesConfig_DurationUnit) + *p = x + return p +} + +func (x RoutineChangesConfig_DurationUnit) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RoutineChangesConfig_DurationUnit) Descriptor() protoreflect.EnumDescriptor { + return file_config_proto_enumTypes[2].Descriptor() +} + +func (RoutineChangesConfig_DurationUnit) Type() protoreflect.EnumType { + return &file_config_proto_enumTypes[2] +} + +func (x RoutineChangesConfig_DurationUnit) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RoutineChangesConfig_DurationUnit.Descriptor instead. +func (RoutineChangesConfig_DurationUnit) EnumDescriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{21, 0} +} + +// The config that is used when calculating the blast radius for a change, this +// does not affect manually requested blast radii vie the "Explore" view or the +// API +type BlastRadiusConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The maximum number of items that can be returned in a single blast radius + // request. Once a request has hit this limit, all currently running + // requests will be cancelled and the blast radius returned as-is + MaxItems int32 `protobuf:"varint,1,opt,name=maxItems,proto3" json:"maxItems,omitempty"` + // How deeply to link when calculating the blast radius for a change. This + // 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"` + // 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() { + *x = BlastRadiusConfig{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlastRadiusConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlastRadiusConfig) ProtoMessage() {} + +func (x *BlastRadiusConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + 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 BlastRadiusConfig.ProtoReflect.Descriptor instead. +func (*BlastRadiusConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *BlastRadiusConfig) GetMaxItems() int32 { + if x != nil { + return x.MaxItems + } + return 0 +} + +func (x *BlastRadiusConfig) GetLinkDepth() int32 { + if x != nil { + return x.LinkDepth + } + return 0 +} + +func (x *BlastRadiusConfig) GetChangeAnalysisTargetDuration() *durationpb.Duration { + if x != nil { + return x.ChangeAnalysisTargetDuration + } + return nil +} + +// 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. 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 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 + // reduces the number low-quality and low-severity risks, and focuses more + // on risks that we have additional context for. If you find that Overmind's + // risks are "too obvious" then this might be a good setting to enable. + SkipUnmappedChangesForRisks bool `protobuf:"varint,3,opt,name=skipUnmappedChangesForRisks,proto3" json:"skipUnmappedChangesForRisks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccountConfig) Reset() { + *x = AccountConfig{} + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccountConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountConfig) ProtoMessage() {} + +func (x *AccountConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + 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 AccountConfig.ProtoReflect.Descriptor instead. +func (*AccountConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *AccountConfig) GetBlastRadiusPreset() AccountConfig_BlastRadiusPreset { + if x != nil { + return x.BlastRadiusPreset + } + return AccountConfig_UNSPECIFIED +} + +func (x *AccountConfig) GetBlastRadius() *BlastRadiusConfig { + if x != nil { + return x.BlastRadius + } + return nil +} + +func (x *AccountConfig) GetSkipUnmappedChangesForRisks() bool { + if x != nil { + return x.SkipUnmappedChangesForRisks + } + return false +} + +type GetAccountConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAccountConfigRequest) Reset() { + *x = GetAccountConfigRequest{} + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAccountConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAccountConfigRequest) ProtoMessage() {} + +func (x *GetAccountConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + 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 GetAccountConfigRequest.ProtoReflect.Descriptor instead. +func (*GetAccountConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +type GetAccountConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAccountConfigResponse) Reset() { + *x = GetAccountConfigResponse{} + mi := &file_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAccountConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAccountConfigResponse) ProtoMessage() {} + +func (x *GetAccountConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[3] + 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 GetAccountConfigResponse.ProtoReflect.Descriptor instead. +func (*GetAccountConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{3} +} + +func (x *GetAccountConfigResponse) GetConfig() *AccountConfig { + if x != nil { + return x.Config + } + return nil +} + +// Updates the account config for the user's account. +type UpdateAccountConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAccountConfigRequest) Reset() { + *x = UpdateAccountConfigRequest{} + mi := &file_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAccountConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAccountConfigRequest) ProtoMessage() {} + +func (x *UpdateAccountConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[4] + 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 UpdateAccountConfigRequest.ProtoReflect.Descriptor instead. +func (*UpdateAccountConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateAccountConfigRequest) GetConfig() *AccountConfig { + if x != nil { + return x.Config + } + return nil +} + +type UpdateAccountConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *AccountConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAccountConfigResponse) Reset() { + *x = UpdateAccountConfigResponse{} + mi := &file_config_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAccountConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAccountConfigResponse) ProtoMessage() {} + +func (x *UpdateAccountConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[5] + 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 UpdateAccountConfigResponse.ProtoReflect.Descriptor instead. +func (*UpdateAccountConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateAccountConfigResponse) GetConfig() *AccountConfig { + if x != nil { + return x.Config + } + return nil +} + +type CreateHcpConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The URL that the user should be redirected to after the whole process is + // over. This should be a page in the frontend, probably the HCP Terraform + // Integration page. + FinalFrontendRedirect string `protobuf:"bytes,1,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateHcpConfigRequest) Reset() { + *x = CreateHcpConfigRequest{} + mi := &file_config_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateHcpConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateHcpConfigRequest) ProtoMessage() {} + +func (x *CreateHcpConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[6] + 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 CreateHcpConfigRequest.ProtoReflect.Descriptor instead. +func (*CreateHcpConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateHcpConfigRequest) GetFinalFrontendRedirect() string { + if x != nil { + return x.FinalFrontendRedirect + } + return "" +} + +type CreateHcpConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The configuration of the HCP Run Task that was created + Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + // The API Key response for the API key that backs this integration. This + // API will have been created but not yet authorised, the user must still be + // redirected to the authorizeURL to complete the process. + ApiKey *CreateAPIKeyResponse `protobuf:"bytes,2,opt,name=apiKey,proto3" json:"apiKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateHcpConfigResponse) Reset() { + *x = CreateHcpConfigResponse{} + mi := &file_config_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateHcpConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateHcpConfigResponse) ProtoMessage() {} + +func (x *CreateHcpConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[7] + 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 CreateHcpConfigResponse.ProtoReflect.Descriptor instead. +func (*CreateHcpConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{7} +} + +func (x *CreateHcpConfigResponse) GetConfig() *HcpConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *CreateHcpConfigResponse) GetApiKey() *CreateAPIKeyResponse { + if x != nil { + return x.ApiKey + } + return nil +} + +type HcpConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the Endpoint URL for the HCP Run Task configuration + Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + // the HMAC secret for the HCP Run Task configuration + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HcpConfig) Reset() { + *x = HcpConfig{} + mi := &file_config_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HcpConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HcpConfig) ProtoMessage() {} + +func (x *HcpConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[8] + 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 HcpConfig.ProtoReflect.Descriptor instead. +func (*HcpConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{8} +} + +func (x *HcpConfig) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *HcpConfig) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +type GetHcpConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHcpConfigRequest) Reset() { + *x = GetHcpConfigRequest{} + mi := &file_config_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHcpConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHcpConfigRequest) ProtoMessage() {} + +func (x *GetHcpConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[9] + 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 GetHcpConfigRequest.ProtoReflect.Descriptor instead. +func (*GetHcpConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{9} +} + +type GetHcpConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Status GetHcpConfigResponse_Status `protobuf:"varint,2,opt,name=status,proto3,enum=config.GetHcpConfigResponse_Status" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHcpConfigResponse) Reset() { + *x = GetHcpConfigResponse{} + mi := &file_config_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHcpConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHcpConfigResponse) ProtoMessage() {} + +func (x *GetHcpConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[10] + 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 GetHcpConfigResponse.ProtoReflect.Descriptor instead. +func (*GetHcpConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{10} +} + +func (x *GetHcpConfigResponse) GetConfig() *HcpConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *GetHcpConfigResponse) GetStatus() GetHcpConfigResponse_Status { + if x != nil { + return x.Status + } + return GetHcpConfigResponse_CONFIGURED +} + +type DeleteHcpConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteHcpConfigRequest) Reset() { + *x = DeleteHcpConfigRequest{} + mi := &file_config_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteHcpConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHcpConfigRequest) ProtoMessage() {} + +func (x *DeleteHcpConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[11] + 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 DeleteHcpConfigRequest.ProtoReflect.Descriptor instead. +func (*DeleteHcpConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{11} +} + +type DeleteHcpConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteHcpConfigResponse) Reset() { + *x = DeleteHcpConfigResponse{} + mi := &file_config_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteHcpConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHcpConfigResponse) ProtoMessage() {} + +func (x *DeleteHcpConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[12] + 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 DeleteHcpConfigResponse.ProtoReflect.Descriptor instead. +func (*DeleteHcpConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{12} +} + +type ReplaceHcpApiKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The URL that the user should be redirected to after the OAuth process is + // over. This should be a page in the frontend, probably the HCP Terraform + // Integration page. + FinalFrontendRedirect string `protobuf:"bytes,1,opt,name=finalFrontendRedirect,proto3" json:"finalFrontendRedirect,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReplaceHcpApiKeyRequest) Reset() { + *x = ReplaceHcpApiKeyRequest{} + mi := &file_config_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReplaceHcpApiKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReplaceHcpApiKeyRequest) ProtoMessage() {} + +func (x *ReplaceHcpApiKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReplaceHcpApiKeyRequest.ProtoReflect.Descriptor instead. +func (*ReplaceHcpApiKeyRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{13} +} + +func (x *ReplaceHcpApiKeyRequest) GetFinalFrontendRedirect() string { + if x != nil { + return x.FinalFrontendRedirect + } + return "" +} + +type ReplaceHcpApiKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The configuration of the HCP Run Task (endpoint URL and HMAC secret are + // preserved from the existing config) + Config *HcpConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + // The API Key response for the newly created API key. This API key has been + // created but not yet authorised, the user must still be redirected to the + // authorizeURL to complete the process. + ApiKey *CreateAPIKeyResponse `protobuf:"bytes,2,opt,name=apiKey,proto3" json:"apiKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReplaceHcpApiKeyResponse) Reset() { + *x = ReplaceHcpApiKeyResponse{} + mi := &file_config_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReplaceHcpApiKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReplaceHcpApiKeyResponse) ProtoMessage() {} + +func (x *ReplaceHcpApiKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReplaceHcpApiKeyResponse.ProtoReflect.Descriptor instead. +func (*ReplaceHcpApiKeyResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{14} +} + +func (x *ReplaceHcpApiKeyResponse) GetConfig() *HcpConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *ReplaceHcpApiKeyResponse) GetApiKey() *CreateAPIKeyResponse { + if x != nil { + return x.ApiKey + } + return nil +} + +// Account Signal config +type GetSignalConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSignalConfigRequest) Reset() { + *x = GetSignalConfigRequest{} + mi := &file_config_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSignalConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignalConfigRequest) ProtoMessage() {} + +func (x *GetSignalConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[15] + 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 GetSignalConfigRequest.ProtoReflect.Descriptor instead. +func (*GetSignalConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{15} +} + +type GetSignalConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSignalConfigResponse) Reset() { + *x = GetSignalConfigResponse{} + mi := &file_config_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSignalConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignalConfigResponse) ProtoMessage() {} + +func (x *GetSignalConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[16] + 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 GetSignalConfigResponse.ProtoReflect.Descriptor instead. +func (*GetSignalConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{16} +} + +func (x *GetSignalConfigResponse) GetConfig() *SignalConfig { + if x != nil { + return x.Config + } + return nil +} + +// Updates the signal config for the account. +type UpdateSignalConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSignalConfigRequest) Reset() { + *x = UpdateSignalConfigRequest{} + mi := &file_config_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSignalConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSignalConfigRequest) ProtoMessage() {} + +func (x *UpdateSignalConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateSignalConfigRequest.ProtoReflect.Descriptor instead. +func (*UpdateSignalConfigRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{17} +} + +func (x *UpdateSignalConfigRequest) GetConfig() *SignalConfig { + if x != nil { + return x.Config + } + return nil +} + +type UpdateSignalConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *SignalConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSignalConfigResponse) Reset() { + *x = UpdateSignalConfigResponse{} + mi := &file_config_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSignalConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSignalConfigResponse) ProtoMessage() {} + +func (x *UpdateSignalConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateSignalConfigResponse.ProtoReflect.Descriptor instead. +func (*UpdateSignalConfigResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{18} +} + +func (x *UpdateSignalConfigResponse) GetConfig() *SignalConfig { + if x != nil { + return x.Config + } + return nil +} + +type SignalConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Config for aggregation parameters, such as alpha + AggregationConfig *AggregationConfig `protobuf:"bytes,1,opt,name=aggregationConfig,proto3" json:"aggregationConfig,omitempty"` + // Config for routine changes, such as events per day and duration + RoutineChangesConfig *RoutineChangesConfig `protobuf:"bytes,2,opt,name=routineChangesConfig,proto3" json:"routineChangesConfig,omitempty"` + // Config for Github app profile, such as primary branch name + GithubOrganisationProfile *GithubOrganisationProfile `protobuf:"bytes,3,opt,name=githubOrganisationProfile,proto3,oneof" json:"githubOrganisationProfile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalConfig) Reset() { + *x = SignalConfig{} + mi := &file_config_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalConfig) ProtoMessage() {} + +func (x *SignalConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_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 SignalConfig.ProtoReflect.Descriptor instead. +func (*SignalConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{19} +} + +func (x *SignalConfig) GetAggregationConfig() *AggregationConfig { + if x != nil { + return x.AggregationConfig + } + return nil +} + +func (x *SignalConfig) GetRoutineChangesConfig() *RoutineChangesConfig { + if x != nil { + return x.RoutineChangesConfig + } + return nil +} + +func (x *SignalConfig) GetGithubOrganisationProfile() *GithubOrganisationProfile { + if x != nil { + return x.GithubOrganisationProfile + } + return nil +} + +type AggregationConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Alpha parameter for aggregation: controls the weighting of recent data versus older data + // Must be positive (greater than 0) as it's the temperature parameter for exponential decay + Alpha float32 `protobuf:"fixed32,1,opt,name=alpha,proto3" json:"alpha,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AggregationConfig) Reset() { + *x = AggregationConfig{} + mi := &file_config_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AggregationConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AggregationConfig) ProtoMessage() {} + +func (x *AggregationConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[20] + 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 AggregationConfig.ProtoReflect.Descriptor instead. +func (*AggregationConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{20} +} + +func (x *AggregationConfig) GetAlpha() float32 { + if x != nil { + return x.Alpha + } + return 0 +} + +type RoutineChangesConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user will see the format of "12 changes per day for 3 weeks" with the user able to change these values i.e. + // Events per days, weeks, or months + EventsPer float32 `protobuf:"fixed32,1,opt,name=eventsPer,proto3" json:"eventsPer,omitempty"` + EventsPerUnit RoutineChangesConfig_DurationUnit `protobuf:"varint,2,opt,name=eventsPerUnit,proto3,enum=config.RoutineChangesConfig_DurationUnit" json:"eventsPerUnit,omitempty"` + // Duration the number of days, weeks, or months over which routine changes are considered. + Duration float32 `protobuf:"fixed32,3,opt,name=duration,proto3" json:"duration,omitempty"` + // Specifies the unit of time for the duration field in routine changes. + // The available units are days, weeks, and months. + DurationUnit RoutineChangesConfig_DurationUnit `protobuf:"varint,4,opt,name=durationUnit,proto3,enum=config.RoutineChangesConfig_DurationUnit" json:"durationUnit,omitempty"` + // Sensitivity parameter that controls the threshold for detecting routine changes. + // A higher sensitivity value makes the detection more responsive to smaller changes, + // while a lower value makes it less responsive. + Sensitivity float32 `protobuf:"fixed32,5,opt,name=sensitivity,proto3" json:"sensitivity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoutineChangesConfig) Reset() { + *x = RoutineChangesConfig{} + mi := &file_config_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoutineChangesConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoutineChangesConfig) ProtoMessage() {} + +func (x *RoutineChangesConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[21] + 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 RoutineChangesConfig.ProtoReflect.Descriptor instead. +func (*RoutineChangesConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{21} +} + +func (x *RoutineChangesConfig) GetEventsPer() float32 { + if x != nil { + return x.EventsPer + } + return 0 +} + +func (x *RoutineChangesConfig) GetEventsPerUnit() RoutineChangesConfig_DurationUnit { + if x != nil { + return x.EventsPerUnit + } + return RoutineChangesConfig_DAYS +} + +func (x *RoutineChangesConfig) GetDuration() float32 { + if x != nil { + return x.Duration + } + return 0 +} + +func (x *RoutineChangesConfig) GetDurationUnit() RoutineChangesConfig_DurationUnit { + if x != nil { + return x.DurationUnit + } + return RoutineChangesConfig_DAYS +} + +func (x *RoutineChangesConfig) GetSensitivity() float32 { + if x != nil { + return x.Sensitivity + } + return 0 +} + +// no parameters required, we will look up the installation ID from the account ID +type GetGithubAppInformationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGithubAppInformationRequest) Reset() { + *x = GetGithubAppInformationRequest{} + mi := &file_config_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGithubAppInformationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGithubAppInformationRequest) ProtoMessage() {} + +func (x *GetGithubAppInformationRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[22] + 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 GetGithubAppInformationRequest.ProtoReflect.Descriptor instead. +func (*GetGithubAppInformationRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{22} +} + +// it will be used to display information to the github integrations page, it is not used for signal processing +type GithubAppInformation struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstallationID int64 `protobuf:"varint,1,opt,name=installationID,proto3" json:"installationID,omitempty"` + InstalledBy string `protobuf:"bytes,2,opt,name=installedBy,proto3" json:"installedBy,omitempty"` + InstalledAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=installedAt,proto3" json:"installedAt,omitempty"` + OrganisationName string `protobuf:"bytes,4,opt,name=organisationName,proto3" json:"organisationName,omitempty"` + ActiveRepositoryCount int64 `protobuf:"varint,5,opt,name=activeRepositoryCount,proto3" json:"activeRepositoryCount,omitempty"` + ContributorCount int64 `protobuf:"varint,6,opt,name=contributorCount,proto3" json:"contributorCount,omitempty"` + BotAutomationPercentage int64 `protobuf:"varint,7,opt,name=botAutomationPercentage,proto3" json:"botAutomationPercentage,omitempty"` + AverageMergeTime string `protobuf:"bytes,8,opt,name=averageMergeTime,proto3" json:"averageMergeTime,omitempty"` + AverageCommitFrequency string `protobuf:"bytes,9,opt,name=averageCommitFrequency,proto3" json:"averageCommitFrequency,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GithubAppInformation) Reset() { + *x = GithubAppInformation{} + mi := &file_config_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GithubAppInformation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GithubAppInformation) ProtoMessage() {} + +func (x *GithubAppInformation) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[23] + 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 GithubAppInformation.ProtoReflect.Descriptor instead. +func (*GithubAppInformation) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{23} +} + +func (x *GithubAppInformation) GetInstallationID() int64 { + if x != nil { + return x.InstallationID + } + return 0 +} + +func (x *GithubAppInformation) GetInstalledBy() string { + if x != nil { + return x.InstalledBy + } + return "" +} + +func (x *GithubAppInformation) GetInstalledAt() *timestamppb.Timestamp { + if x != nil { + return x.InstalledAt + } + return nil +} + +func (x *GithubAppInformation) GetOrganisationName() string { + if x != nil { + return x.OrganisationName + } + return "" +} + +func (x *GithubAppInformation) GetActiveRepositoryCount() int64 { + if x != nil { + return x.ActiveRepositoryCount + } + return 0 +} + +func (x *GithubAppInformation) GetContributorCount() int64 { + if x != nil { + return x.ContributorCount + } + return 0 +} + +func (x *GithubAppInformation) GetBotAutomationPercentage() int64 { + if x != nil { + return x.BotAutomationPercentage + } + return 0 +} + +func (x *GithubAppInformation) GetAverageMergeTime() string { + if x != nil { + return x.AverageMergeTime + } + return "" +} + +func (x *GithubAppInformation) GetAverageCommitFrequency() string { + if x != nil { + return x.AverageCommitFrequency + } + return "" +} + +// this is all the information required to display the github app information +type GetGithubAppInformationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GithubAppInformation *GithubAppInformation `protobuf:"bytes,1,opt,name=githubAppInformation,proto3" json:"githubAppInformation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGithubAppInformationResponse) Reset() { + *x = GetGithubAppInformationResponse{} + mi := &file_config_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGithubAppInformationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGithubAppInformationResponse) ProtoMessage() {} + +func (x *GetGithubAppInformationResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_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 GetGithubAppInformationResponse.ProtoReflect.Descriptor instead. +func (*GetGithubAppInformationResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{24} +} + +func (x *GetGithubAppInformationResponse) GetGithubAppInformation() *GithubAppInformation { + if x != nil { + return x.GithubAppInformation + } + return nil +} + +// no parameters required, we will look up the installation ID from the account ID +type RegenerateGithubAppProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegenerateGithubAppProfileRequest) Reset() { + *x = RegenerateGithubAppProfileRequest{} + mi := &file_config_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegenerateGithubAppProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegenerateGithubAppProfileRequest) ProtoMessage() {} + +func (x *RegenerateGithubAppProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[25] + 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 RegenerateGithubAppProfileRequest.ProtoReflect.Descriptor instead. +func (*RegenerateGithubAppProfileRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{25} +} + +// information stored in the database, used by signal processing in change analysis +type GithubOrganisationProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrimaryBranchName string `protobuf:"bytes,1,opt,name=primaryBranchName,proto3" json:"primaryBranchName,omitempty"` + // signal scores that will be given for each hour of the day, 0-23, from -5.0 to 5.0 + HourlyScores []float64 `protobuf:"fixed64,2,rep,packed,name=hourlyScores,proto3" json:"hourlyScores,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GithubOrganisationProfile) Reset() { + *x = GithubOrganisationProfile{} + mi := &file_config_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GithubOrganisationProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GithubOrganisationProfile) ProtoMessage() {} + +func (x *GithubOrganisationProfile) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[26] + 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 GithubOrganisationProfile.ProtoReflect.Descriptor instead. +func (*GithubOrganisationProfile) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{26} +} + +func (x *GithubOrganisationProfile) GetPrimaryBranchName() string { + if x != nil { + return x.PrimaryBranchName + } + return "" +} + +func (x *GithubOrganisationProfile) GetHourlyScores() []float64 { + if x != nil { + return x.HourlyScores + } + return nil +} + +type RegenerateGithubAppProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GithubOrganisationProfile *GithubOrganisationProfile `protobuf:"bytes,1,opt,name=githubOrganisationProfile,proto3" json:"githubOrganisationProfile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegenerateGithubAppProfileResponse) Reset() { + *x = RegenerateGithubAppProfileResponse{} + mi := &file_config_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegenerateGithubAppProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegenerateGithubAppProfileResponse) ProtoMessage() {} + +func (x *RegenerateGithubAppProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[27] + 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 RegenerateGithubAppProfileResponse.ProtoReflect.Descriptor instead. +func (*RegenerateGithubAppProfileResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{27} +} + +func (x *RegenerateGithubAppProfileResponse) GetGithubOrganisationProfile() *GithubOrganisationProfile { + if x != nil { + return x.GithubOrganisationProfile + } + return nil +} + +// no parameters required, we will look up the installation ID from the account ID +type DeleteGithubAppProfileAndGithubInstallationIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) Reset() { + *x = DeleteGithubAppProfileAndGithubInstallationIDRequest{} + mi := &file_config_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoMessage() {} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[28] + 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 DeleteGithubAppProfileAndGithubInstallationIDRequest.ProtoReflect.Descriptor instead. +func (*DeleteGithubAppProfileAndGithubInstallationIDRequest) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{28} +} + +// status codes to indicate if the deletion was successful +type DeleteGithubAppProfileAndGithubInstallationIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) Reset() { + *x = DeleteGithubAppProfileAndGithubInstallationIDResponse{} + mi := &file_config_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoMessage() {} + +func (x *DeleteGithubAppProfileAndGithubInstallationIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[29] + 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 DeleteGithubAppProfileAndGithubInstallationIDResponse.ProtoReflect.Descriptor instead. +func (*DeleteGithubAppProfileAndGithubInstallationIDResponse) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{29} +} + +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\"\xd8\x01\n" + + "\x11BlastRadiusConfig\x12\x1a\n" + + "\bmaxItems\x18\x01 \x01(\x05R\bmaxItems\x12\x1c\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\"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" + + "\f_blastRadius\"\x19\n" + + "\x17GetAccountConfigRequest\"I\n" + + "\x18GetAccountConfigResponse\x12-\n" + + "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"K\n" + + "\x1aUpdateAccountConfigRequest\x12-\n" + + "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"L\n" + + "\x1bUpdateAccountConfigResponse\x12-\n" + + "\x06config\x18\x01 \x01(\v2\x15.config.AccountConfigR\x06config\"N\n" + + "\x16CreateHcpConfigRequest\x124\n" + + "\x15finalFrontendRedirect\x18\x01 \x01(\tR\x15finalFrontendRedirect\"{\n" + + "\x17CreateHcpConfigResponse\x12)\n" + + "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x125\n" + + "\x06apiKey\x18\x02 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\x06apiKey\"?\n" + + "\tHcpConfig\x12\x1a\n" + + "\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" + + "\x06secret\x18\x02 \x01(\tR\x06secret\"\x15\n" + + "\x13GetHcpConfigRequest\"\xa3\x01\n" + + "\x14GetHcpConfigResponse\x12)\n" + + "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x12;\n" + + "\x06status\x18\x02 \x01(\x0e2#.config.GetHcpConfigResponse.StatusR\x06status\"#\n" + + "\x06Status\x12\x0e\n" + + "\n" + + "CONFIGURED\x10\x00\x12\t\n" + + "\x05ERROR\x10\x01\"\x18\n" + + "\x16DeleteHcpConfigRequest\"\x19\n" + + "\x17DeleteHcpConfigResponse\"O\n" + + "\x17ReplaceHcpApiKeyRequest\x124\n" + + "\x15finalFrontendRedirect\x18\x01 \x01(\tR\x15finalFrontendRedirect\"|\n" + + "\x18ReplaceHcpApiKeyResponse\x12)\n" + + "\x06config\x18\x01 \x01(\v2\x11.config.HcpConfigR\x06config\x125\n" + + "\x06apiKey\x18\x02 \x01(\v2\x1d.apikeys.CreateAPIKeyResponseR\x06apiKey\"\x18\n" + + "\x16GetSignalConfigRequest\"G\n" + + "\x17GetSignalConfigResponse\x12,\n" + + "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"I\n" + + "\x19UpdateSignalConfigRequest\x12,\n" + + "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"J\n" + + "\x1aUpdateSignalConfigResponse\x12,\n" + + "\x06config\x18\x01 \x01(\v2\x14.config.SignalConfigR\x06config\"\xad\x02\n" + + "\fSignalConfig\x12G\n" + + "\x11aggregationConfig\x18\x01 \x01(\v2\x19.config.AggregationConfigR\x11aggregationConfig\x12P\n" + + "\x14routineChangesConfig\x18\x02 \x01(\v2\x1c.config.RoutineChangesConfigR\x14routineChangesConfig\x12d\n" + + "\x19githubOrganisationProfile\x18\x03 \x01(\v2!.config.GithubOrganisationProfileH\x00R\x19githubOrganisationProfile\x88\x01\x01B\x1c\n" + + "\x1a_githubOrganisationProfile\"5\n" + + "\x11AggregationConfig\x12 \n" + + "\x05alpha\x18\x01 \x01(\x02B\n" + + "\xbaH\a\n" + + "\x05%\x00\x00\x00\x00R\x05alpha\"\xc3\x02\n" + + "\x14RoutineChangesConfig\x12\x1c\n" + + "\teventsPer\x18\x01 \x01(\x02R\teventsPer\x12O\n" + + "\reventsPerUnit\x18\x02 \x01(\x0e2).config.RoutineChangesConfig.DurationUnitR\reventsPerUnit\x12\x1a\n" + + "\bduration\x18\x03 \x01(\x02R\bduration\x12M\n" + + "\fdurationUnit\x18\x04 \x01(\x0e2).config.RoutineChangesConfig.DurationUnitR\fdurationUnit\x12 \n" + + "\vsensitivity\x18\x05 \x01(\x02R\vsensitivity\"/\n" + + "\fDurationUnit\x12\b\n" + + "\x04DAYS\x10\x00\x12\t\n" + + "\x05WEEKS\x10\x01\x12\n" + + "\n" + + "\x06MONTHS\x10\x02\" \n" + + "\x1eGetGithubAppInformationRequest\"\xca\x03\n" + + "\x14GithubAppInformation\x12&\n" + + "\x0einstallationID\x18\x01 \x01(\x03R\x0einstallationID\x12 \n" + + "\vinstalledBy\x18\x02 \x01(\tR\vinstalledBy\x12<\n" + + "\vinstalledAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\vinstalledAt\x12*\n" + + "\x10organisationName\x18\x04 \x01(\tR\x10organisationName\x124\n" + + "\x15activeRepositoryCount\x18\x05 \x01(\x03R\x15activeRepositoryCount\x12*\n" + + "\x10contributorCount\x18\x06 \x01(\x03R\x10contributorCount\x128\n" + + "\x17botAutomationPercentage\x18\a \x01(\x03R\x17botAutomationPercentage\x12*\n" + + "\x10averageMergeTime\x18\b \x01(\tR\x10averageMergeTime\x126\n" + + "\x16averageCommitFrequency\x18\t \x01(\tR\x16averageCommitFrequency\"s\n" + + "\x1fGetGithubAppInformationResponse\x12P\n" + + "\x14githubAppInformation\x18\x01 \x01(\v2\x1c.config.GithubAppInformationR\x14githubAppInformation\"#\n" + + "!RegenerateGithubAppProfileRequest\"\x8f\x01\n" + + "\x19GithubOrganisationProfile\x12,\n" + + "\x11primaryBranchName\x18\x01 \x01(\tR\x11primaryBranchName\x12D\n" + + "\fhourlyScores\x18\x02 \x03(\x01B \xbaH\x1d\x92\x01\x1a\b\x18\x10\x18\"\x14\x12\x12\x19\x00\x00\x00\x00\x00\x00\x14@)\x00\x00\x00\x00\x00\x00\x14\xc0R\fhourlyScores\"\x85\x01\n" + + "\"RegenerateGithubAppProfileResponse\x12_\n" + + "\x19githubOrganisationProfile\x18\x01 \x01(\v2!.config.GithubOrganisationProfileR\x19githubOrganisationProfile\"6\n" + + "4DeleteGithubAppProfileAndGithubInstallationIDRequest\"7\n" + + "5DeleteGithubAppProfileAndGithubInstallationIDResponse2\xd8\b\n" + + "\x14ConfigurationService\x12U\n" + + "\x10GetAccountConfig\x12\x1f.config.GetAccountConfigRequest\x1a .config.GetAccountConfigResponse\x12^\n" + + "\x13UpdateAccountConfig\x12\".config.UpdateAccountConfigRequest\x1a#.config.UpdateAccountConfigResponse\x12R\n" + + "\x0fCreateHcpConfig\x12\x1e.config.CreateHcpConfigRequest\x1a\x1f.config.CreateHcpConfigResponse\x12I\n" + + "\fGetHcpConfig\x12\x1b.config.GetHcpConfigRequest\x1a\x1c.config.GetHcpConfigResponse\x12R\n" + + "\x0fDeleteHcpConfig\x12\x1e.config.DeleteHcpConfigRequest\x1a\x1f.config.DeleteHcpConfigResponse\x12U\n" + + "\x10ReplaceHcpApiKey\x12\x1f.config.ReplaceHcpApiKeyRequest\x1a .config.ReplaceHcpApiKeyResponse\x12R\n" + + "\x0fGetSignalConfig\x12\x1e.config.GetSignalConfigRequest\x1a\x1f.config.GetSignalConfigResponse\x12[\n" + + "\x12UpdateSignalConfig\x12!.config.UpdateSignalConfigRequest\x1a\".config.UpdateSignalConfigResponse\x12j\n" + + "\x17GetGithubAppInformation\x12&.config.GetGithubAppInformationRequest\x1a'.config.GetGithubAppInformationResponse\x12s\n" + + "\x1aRegenerateGithubAppProfile\x12).config.RegenerateGithubAppProfileRequest\x1a*.config.RegenerateGithubAppProfileResponse\x12\xac\x01\n" + + "-DeleteGithubAppProfileAndGithubInstallationID\x12<.config.DeleteGithubAppProfileAndGithubInstallationIDRequest\x1a=.config.DeleteGithubAppProfileAndGithubInstallationIDResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData []byte +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc))) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_config_proto_goTypes = []any{ + (AccountConfig_BlastRadiusPreset)(0), // 0: config.AccountConfig.BlastRadiusPreset + (GetHcpConfigResponse_Status)(0), // 1: config.GetHcpConfigResponse.Status + (RoutineChangesConfig_DurationUnit)(0), // 2: config.RoutineChangesConfig.DurationUnit + (*BlastRadiusConfig)(nil), // 3: config.BlastRadiusConfig + (*AccountConfig)(nil), // 4: config.AccountConfig + (*GetAccountConfigRequest)(nil), // 5: config.GetAccountConfigRequest + (*GetAccountConfigResponse)(nil), // 6: config.GetAccountConfigResponse + (*UpdateAccountConfigRequest)(nil), // 7: config.UpdateAccountConfigRequest + (*UpdateAccountConfigResponse)(nil), // 8: config.UpdateAccountConfigResponse + (*CreateHcpConfigRequest)(nil), // 9: config.CreateHcpConfigRequest + (*CreateHcpConfigResponse)(nil), // 10: config.CreateHcpConfigResponse + (*HcpConfig)(nil), // 11: config.HcpConfig + (*GetHcpConfigRequest)(nil), // 12: config.GetHcpConfigRequest + (*GetHcpConfigResponse)(nil), // 13: config.GetHcpConfigResponse + (*DeleteHcpConfigRequest)(nil), // 14: config.DeleteHcpConfigRequest + (*DeleteHcpConfigResponse)(nil), // 15: config.DeleteHcpConfigResponse + (*ReplaceHcpApiKeyRequest)(nil), // 16: config.ReplaceHcpApiKeyRequest + (*ReplaceHcpApiKeyResponse)(nil), // 17: config.ReplaceHcpApiKeyResponse + (*GetSignalConfigRequest)(nil), // 18: config.GetSignalConfigRequest + (*GetSignalConfigResponse)(nil), // 19: config.GetSignalConfigResponse + (*UpdateSignalConfigRequest)(nil), // 20: config.UpdateSignalConfigRequest + (*UpdateSignalConfigResponse)(nil), // 21: config.UpdateSignalConfigResponse + (*SignalConfig)(nil), // 22: config.SignalConfig + (*AggregationConfig)(nil), // 23: config.AggregationConfig + (*RoutineChangesConfig)(nil), // 24: config.RoutineChangesConfig + (*GetGithubAppInformationRequest)(nil), // 25: config.GetGithubAppInformationRequest + (*GithubAppInformation)(nil), // 26: config.GithubAppInformation + (*GetGithubAppInformationResponse)(nil), // 27: config.GetGithubAppInformationResponse + (*RegenerateGithubAppProfileRequest)(nil), // 28: config.RegenerateGithubAppProfileRequest + (*GithubOrganisationProfile)(nil), // 29: config.GithubOrganisationProfile + (*RegenerateGithubAppProfileResponse)(nil), // 30: config.RegenerateGithubAppProfileResponse + (*DeleteGithubAppProfileAndGithubInstallationIDRequest)(nil), // 31: config.DeleteGithubAppProfileAndGithubInstallationIDRequest + (*DeleteGithubAppProfileAndGithubInstallationIDResponse)(nil), // 32: config.DeleteGithubAppProfileAndGithubInstallationIDResponse + (*durationpb.Duration)(nil), // 33: google.protobuf.Duration + (*CreateAPIKeyResponse)(nil), // 34: apikeys.CreateAPIKeyResponse + (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp +} +var file_config_proto_depIdxs = []int32{ + 33, // 0: config.BlastRadiusConfig.changeAnalysisTargetDuration:type_name -> google.protobuf.Duration + 0, // 1: config.AccountConfig.blastRadiusPreset:type_name -> config.AccountConfig.BlastRadiusPreset + 3, // 2: config.AccountConfig.blastRadius:type_name -> config.BlastRadiusConfig + 4, // 3: config.GetAccountConfigResponse.config:type_name -> config.AccountConfig + 4, // 4: config.UpdateAccountConfigRequest.config:type_name -> config.AccountConfig + 4, // 5: config.UpdateAccountConfigResponse.config:type_name -> config.AccountConfig + 11, // 6: config.CreateHcpConfigResponse.config:type_name -> config.HcpConfig + 34, // 7: config.CreateHcpConfigResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse + 11, // 8: config.GetHcpConfigResponse.config:type_name -> config.HcpConfig + 1, // 9: config.GetHcpConfigResponse.status:type_name -> config.GetHcpConfigResponse.Status + 11, // 10: config.ReplaceHcpApiKeyResponse.config:type_name -> config.HcpConfig + 34, // 11: config.ReplaceHcpApiKeyResponse.apiKey:type_name -> apikeys.CreateAPIKeyResponse + 22, // 12: config.GetSignalConfigResponse.config:type_name -> config.SignalConfig + 22, // 13: config.UpdateSignalConfigRequest.config:type_name -> config.SignalConfig + 22, // 14: config.UpdateSignalConfigResponse.config:type_name -> config.SignalConfig + 23, // 15: config.SignalConfig.aggregationConfig:type_name -> config.AggregationConfig + 24, // 16: config.SignalConfig.routineChangesConfig:type_name -> config.RoutineChangesConfig + 29, // 17: config.SignalConfig.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile + 2, // 18: config.RoutineChangesConfig.eventsPerUnit:type_name -> config.RoutineChangesConfig.DurationUnit + 2, // 19: config.RoutineChangesConfig.durationUnit:type_name -> config.RoutineChangesConfig.DurationUnit + 35, // 20: config.GithubAppInformation.installedAt:type_name -> google.protobuf.Timestamp + 26, // 21: config.GetGithubAppInformationResponse.githubAppInformation:type_name -> config.GithubAppInformation + 29, // 22: config.RegenerateGithubAppProfileResponse.githubOrganisationProfile:type_name -> config.GithubOrganisationProfile + 5, // 23: config.ConfigurationService.GetAccountConfig:input_type -> config.GetAccountConfigRequest + 7, // 24: config.ConfigurationService.UpdateAccountConfig:input_type -> config.UpdateAccountConfigRequest + 9, // 25: config.ConfigurationService.CreateHcpConfig:input_type -> config.CreateHcpConfigRequest + 12, // 26: config.ConfigurationService.GetHcpConfig:input_type -> config.GetHcpConfigRequest + 14, // 27: config.ConfigurationService.DeleteHcpConfig:input_type -> config.DeleteHcpConfigRequest + 16, // 28: config.ConfigurationService.ReplaceHcpApiKey:input_type -> config.ReplaceHcpApiKeyRequest + 18, // 29: config.ConfigurationService.GetSignalConfig:input_type -> config.GetSignalConfigRequest + 20, // 30: config.ConfigurationService.UpdateSignalConfig:input_type -> config.UpdateSignalConfigRequest + 25, // 31: config.ConfigurationService.GetGithubAppInformation:input_type -> config.GetGithubAppInformationRequest + 28, // 32: config.ConfigurationService.RegenerateGithubAppProfile:input_type -> config.RegenerateGithubAppProfileRequest + 31, // 33: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:input_type -> config.DeleteGithubAppProfileAndGithubInstallationIDRequest + 6, // 34: config.ConfigurationService.GetAccountConfig:output_type -> config.GetAccountConfigResponse + 8, // 35: config.ConfigurationService.UpdateAccountConfig:output_type -> config.UpdateAccountConfigResponse + 10, // 36: config.ConfigurationService.CreateHcpConfig:output_type -> config.CreateHcpConfigResponse + 13, // 37: config.ConfigurationService.GetHcpConfig:output_type -> config.GetHcpConfigResponse + 15, // 38: config.ConfigurationService.DeleteHcpConfig:output_type -> config.DeleteHcpConfigResponse + 17, // 39: config.ConfigurationService.ReplaceHcpApiKey:output_type -> config.ReplaceHcpApiKeyResponse + 19, // 40: config.ConfigurationService.GetSignalConfig:output_type -> config.GetSignalConfigResponse + 21, // 41: config.ConfigurationService.UpdateSignalConfig:output_type -> config.UpdateSignalConfigResponse + 27, // 42: config.ConfigurationService.GetGithubAppInformation:output_type -> config.GetGithubAppInformationResponse + 30, // 43: config.ConfigurationService.RegenerateGithubAppProfile:output_type -> config.RegenerateGithubAppProfileResponse + 32, // 44: config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID:output_type -> config.DeleteGithubAppProfileAndGithubInstallationIDResponse + 34, // [34:45] is the sub-list for method output_type + 23, // [23:34] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + file_apikeys_proto_init() + file_config_proto_msgTypes[0].OneofWrappers = []any{} + file_config_proto_msgTypes[1].OneofWrappers = []any{} + file_config_proto_msgTypes[19].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_config_proto_rawDesc), len(file_config_proto_rawDesc)), + NumEnums: 3, + NumMessages: 30, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + EnumInfos: file_config_proto_enumTypes, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/go/sdp-go/connection.go b/go/sdp-go/connection.go new file mode 100644 index 00000000..f125afc0 --- /dev/null +++ b/go/sdp-go/connection.go @@ -0,0 +1,180 @@ +package sdp + +import ( + "context" + "fmt" + reflect "reflect" + + "github.com/nats-io/nats.go" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// EncodedConnection is an interface that allows messages to be published to it. +// In production this would always be filled by a *nats.EncodedConn, however in +// testing we will mock this with something that does nothing +type EncodedConnection interface { + Publish(ctx context.Context, subj string, m proto.Message) error + PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error + PublishMsg(ctx context.Context, msg *nats.Msg) error + Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) + QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) + RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) + + Status() nats.Status + Stats() nats.Statistics + LastError() error + + Drain() error + Close() + + Underlying() *nats.Conn + Drop() +} + +type EncodedConnectionImpl struct { + Conn *nats.Conn +} + +// assert interface implementation +var _ EncodedConnection = (*EncodedConnectionImpl)(nil) + +func recordMessage(ctx context.Context, name, subj, typ, msg string) { + log.WithContext(ctx).WithFields(log.Fields{ + "msg": msg, + "subj": subj, + "typ": typ, + }).Trace(name) + // avoid spamming honeycomb + if log.GetLevel() == log.TraceLevel { + span := trace.SpanFromContext(ctx) + span.AddEvent(name, trace.WithAttributes( + attribute.String("ovm.sdp.subject", subj), + attribute.String("ovm.sdp.message", msg), + )) + } +} + +func (ec *EncodedConnectionImpl) Publish(ctx context.Context, subj string, m proto.Message) error { + // TODO: protojson.Format is pretty expensive, replace with summarized data + recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) + + data, err := proto.Marshal(m) + if err != nil { + return err + } + + msg := &nats.Msg{ + Subject: subj, + Data: data, + } + InjectOtelTraceContext(ctx, msg) + return ec.Conn.PublishMsg(msg) +} + +func (ec *EncodedConnectionImpl) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { + // TODO: protojson.Format is pretty expensive, replace with summarized data + recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) + + data, err := proto.Marshal(m) + if err != nil { + return err + } + + msg := &nats.Msg{ + Subject: subj, + Data: data, + } + msg.Header.Add("reply-to", replyTo) + InjectOtelTraceContext(ctx, msg) + return ec.Conn.PublishMsg(msg) +} + +func (ec *EncodedConnectionImpl) PublishMsg(ctx context.Context, msg *nats.Msg) error { + recordMessage(ctx, "Publish", msg.Subject, "[]byte", "binary") + + InjectOtelTraceContext(ctx, msg) + return ec.Conn.PublishMsg(msg) +} + +// Subscribe Use genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling +func (ec *EncodedConnectionImpl) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { + return ec.Conn.Subscribe(subj, cb) +} + +// QueueSubscribe Use genhandler to get a nats.MsgHandler with otel propagation and protobuf marshaling +func (ec *EncodedConnectionImpl) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { + return ec.Conn.QueueSubscribe(subj, queue, cb) +} + +func (ec *EncodedConnectionImpl) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { + recordMessage(ctx, "RequestMsg", msg.Subject, "[]byte", "binary") + InjectOtelTraceContext(ctx, msg) + reply, err := ec.Conn.RequestMsgWithContext(ctx, msg) + + if err != nil { + recordMessage(ctx, "RequestMsg Error", msg.Subject, fmt.Sprint(reflect.TypeOf(err)), err.Error()) + } else { + recordMessage(ctx, "RequestMsg Reply", msg.Subject, "[]byte", "binary") + } + return reply, err +} + +func (ec *EncodedConnectionImpl) Drain() error { + return ec.Conn.Drain() +} +func (ec *EncodedConnectionImpl) Close() { + ec.Conn.Close() +} + +func (ec *EncodedConnectionImpl) Status() nats.Status { + return ec.Conn.Status() +} + +func (ec *EncodedConnectionImpl) Stats() nats.Statistics { + return ec.Conn.Stats() +} + +func (ec *EncodedConnectionImpl) LastError() error { + return ec.Conn.LastError() +} + +func (ec *EncodedConnectionImpl) Underlying() *nats.Conn { + return ec.Conn +} + +// Drop Drops the underlying connection completely +func (ec *EncodedConnectionImpl) Drop() { + ec.Conn = nil +} + +// Unmarshal Does a proto.Unmarshal and logs errors in a consistent way. The +// user should still validate that the message is valid as it's possible to +// unmarshal data from one message format into another without an error. +// Validation should be based on the type that the data is being unmarshaled +// into. +func Unmarshal(ctx context.Context, b []byte, m proto.Message) error { + err := proto.Unmarshal(b, m) + if err != nil { + recordMessage(ctx, "Unmarshal err", "unknown", fmt.Sprint(reflect.TypeOf(err)), err.Error()) + log.WithContext(ctx).Errorf("Error parsing message: %v", err) + trace.SpanFromContext(ctx).SetStatus(codes.Error, fmt.Sprintf("Error parsing message: %v", err)) + return err + } + + recordMessage(ctx, "Unmarshal", "unknown", fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) + return nil +} + +//go:generate go run genhandler.go Query +//go:generate go run genhandler.go QueryResponse +//go:generate go run genhandler.go CancelQuery + +//go:generate go run genhandler.go GatewayResponse + +//go:generate go run genhandler.go NATSGetLogRecordsRequest +//go:generate go run genhandler.go NATSGetLogRecordsResponse diff --git a/go/sdp-go/connection_test.go b/go/sdp-go/connection_test.go new file mode 100644 index 00000000..13661c22 --- /dev/null +++ b/go/sdp-go/connection_test.go @@ -0,0 +1,27 @@ +package sdp + +import ( + "context" + "testing" +) + +// This is an example of a Query with a timeout. This attribute was removed and +// replaced with a `reserved` field. Therefore it is being used to test how we +// handle older messages +var exampleRemovedAttribute = []byte{ + 0xa, 0x3, 0x66, 0x6f, 0x6f, 0x1a, 0x3, 0x66, 0x6f, 0x6f, 0x22, 0x4, 0x8, + 0xa, 0x10, 0x1, 0x2a, 0x3, 0x66, 0x6f, 0x6f, 0x30, 0x1, 0x3a, 0x10, 0x4e, + 0x43, 0x68, 0xd9, 0x17, 0xd4, 0x4d, 0x83, 0xa9, 0xe6, 0xf5, 0x3a, 0xec, + 0xc7, 0xe7, 0xf0, 0x42, 0x2, 0x8, 0xa, +} + +func TestUnmarshal(t *testing.T) { + ctx := context.Background() + query := new(Query) + + err := Unmarshal(ctx, exampleRemovedAttribute, query) + + if err != nil { + t.Error(err) + } +} diff --git a/go/sdp-go/encoder_test.go b/go/sdp-go/encoder_test.go new file mode 100644 index 00000000..c92b03bf --- /dev/null +++ b/go/sdp-go/encoder_test.go @@ -0,0 +1,175 @@ +package sdp + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var _u = uuid.New() + +var query = Query{ + Type: "user", + Method: QueryMethod_LIST, + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 10, + }, + Scope: "test", + UUID: _u[:], + Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), +} + +var itemAttributes = ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": { + Kind: &structpb.Value_StringValue{ + StringValue: "bar", + }, + }, + }, + }, +} + +var metadata = Metadata{ + SourceName: "users", + SourceQuery: &Query{ + Type: "user", + Method: QueryMethod_LIST, + Query: "*", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 12, + }, + Scope: "testScope", + }, + Timestamp: timestamppb.Now(), + SourceDuration: &durationpb.Duration{ + Seconds: 1, + Nanos: 1, + }, + SourceDurationPerItem: &durationpb.Duration{ + Seconds: 0, + Nanos: 500, + }, +} + +var item = Item{ + Type: "user", + UniqueAttribute: "name", + Attributes: &itemAttributes, + Metadata: &metadata, +} + +var items = Items{ + Items: []*Item{ + &item, + }, +} + +var reference = Reference{ + Type: "user", + UniqueAttributeValue: "dylan", + Scope: "test", +} + +var queryError = QueryError{ + ErrorType: QueryError_OTHER, + ErrorString: "uh oh", + Scope: "test", +} + +var ru = uuid.New() + +var response = Response{ + Responder: "test", + ResponderUUID: ru[:], + State: ResponderState_WORKING, + NextUpdateIn: &durationpb.Duration{ + Seconds: 10, + Nanos: 0, + }, +} + +var messages = []proto.Message{ + &query, + &itemAttributes, + &metadata, + &item, + &items, + &reference, + &queryError, + &response, +} + +// TestEncode Make sure that we can encode all of the message types without +// raising any errors +func TestEncode(t *testing.T) { + for _, message := range messages { + _, err := proto.Marshal(message) + if err != nil { + t.Error(err) + } + } +} + +var decodeTests = []struct { + Message proto.Message + Target proto.Message +}{ + { + Message: &query, + Target: &Query{}, + }, + { + Message: &itemAttributes, + Target: &ItemAttributes{}, + }, + { + Message: &metadata, + Target: &Metadata{}, + }, + { + Message: &item, + Target: &Item{}, + }, + { + Message: &items, + Target: &Items{}, + }, + { + Message: &reference, + Target: &Reference{}, + }, + { + Message: &queryError, + Target: &QueryError{}, + }, + { + Message: &response, + Target: &Response{}, + }, +} + +// TestDecode Make sure that we can decode all of the message +func TestDecode(t *testing.T) { + for _, decTest := range decodeTests { + // Marshal to binary + b, err := proto.Marshal(decTest.Message) + + if err != nil { + t.Fatal(err) + } + + err = Unmarshal(context.Background(), b, decTest.Target) + + if err != nil { + t.Error(err) + } + } +} diff --git a/go/sdp-go/errors.go b/go/sdp-go/errors.go new file mode 100644 index 00000000..4bfe3a53 --- /dev/null +++ b/go/sdp-go/errors.go @@ -0,0 +1,54 @@ +package sdp + +import ( + "errors" + "fmt" + + "github.com/google/uuid" +) + +const ErrorTemplate string = `%v + +ErrorType: %v +Scope: %v +SourceName: %v +ItemType: %v +ResponderName: %v` + +// assert interface +var _ error = (*QueryError)(nil) + +func (e *QueryError) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(e.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// Ensure that the QueryError is seen as a valid error in golang +func (e *QueryError) Error() string { + return fmt.Sprintf( + ErrorTemplate, + e.GetErrorString(), + e.GetErrorType().String(), + e.GetScope(), + e.GetSourceName(), + e.GetItemType(), + e.GetResponderName(), + ) +} + +// NewQueryError converts a regular error to a QueryError of type +// OTHER. If the input error is already a QueryError then it is preserved +func NewQueryError(err error) *QueryError { + var sdpErr *QueryError + if errors.As(err, &sdpErr) { + return sdpErr + } + + return &QueryError{ + ErrorType: QueryError_OTHER, + ErrorString: err.Error(), + } +} diff --git a/go/sdp-go/gateway.go b/go/sdp-go/gateway.go new file mode 100644 index 00000000..4bd5abf8 --- /dev/null +++ b/go/sdp-go/gateway.go @@ -0,0 +1,190 @@ +package sdp + +import ( + "encoding/hex" + + "github.com/google/uuid" +) + +// Equal Returns whether two statuses are functionally equal +func (x *GatewayRequestStatus) Equal(y *GatewayRequestStatus) bool { + if x == nil { + if y == nil { + return true + } else { + return false + } + } + + if (x.GetSummary() == nil || y.GetSummary() == nil) && x.GetSummary() != y.GetSummary() { + // If one of them is nil, and they aren't both nil + return false + } + + if x.GetSummary() != nil && y.GetSummary() != nil { + if x.GetSummary().GetWorking() != y.GetSummary().GetWorking() { + return false + } + if x.GetSummary().GetStalled() != y.GetSummary().GetStalled() { + return false + } + if x.GetSummary().GetComplete() != y.GetSummary().GetComplete() { + return false + } + if x.GetSummary().GetError() != y.GetSummary().GetError() { + return false + } + if x.GetSummary().GetCancelled() != y.GetSummary().GetCancelled() { + return false + } + if x.GetSummary().GetResponders() != y.GetSummary().GetResponders() { + return false + } + } + + if x.GetPostProcessingComplete() != y.GetPostProcessingComplete() { + return false + } + + return true +} + +// Whether the gateway request is complete +func (x *GatewayRequestStatus) Done() bool { + return x.GetPostProcessingComplete() && x.GetSummary().GetWorking() == 0 +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *StoreBookmark) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err != nil { + return "" + } + return u.String() + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *BookmarkStoreResult) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *LoadBookmark) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *BookmarkLoadResult) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *StoreSnapshot) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *SnapshotStoreResult) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *LoadSnapshot) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *SnapshotLoadResult) GetMsgIDLogString() string { + bs := x.GetMsgID() + if len(bs) == 0 { + return "" + } + if len(bs) == 16 { + u, err := uuid.FromBytes(bs) + if err == nil { + return u.String() + } + } + return hex.EncodeToString(bs) +} + +// GetMsgIDLogString returns the correlation ID as string for logging +func (x *QueryStatus) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetUUID()) + if err != nil { + return nil + } + return &u +} + +func (x *LoadSnapshot) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetUUID()) + if err != nil { + return nil + } + return &u +} diff --git a/go/sdp-go/gateway.pb.go b/go/sdp-go/gateway.pb.go new file mode 100644 index 00000000..3728f320 --- /dev/null +++ b/go/sdp-go/gateway.pb.go @@ -0,0 +1,2336 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: gateway.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// A union of all request made to the gateway. +type GatewayRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to RequestType: + // + // *GatewayRequest_Query + // *GatewayRequest_CancelQuery + // *GatewayRequest_Expand + // *GatewayRequest_StoreSnapshot + // *GatewayRequest_LoadSnapshot + // *GatewayRequest_StoreBookmark + // *GatewayRequest_LoadBookmark + // *GatewayRequest_ChatMessage + RequestType isGatewayRequest_RequestType `protobuf_oneof:"request_type"` + MinStatusInterval *durationpb.Duration `protobuf:"bytes,2,opt,name=minStatusInterval,proto3,oneof" json:"minStatusInterval,omitempty"` // Minimum time between status updates. Setting this value too low can result in too many status messages + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayRequest) Reset() { + *x = GatewayRequest{} + mi := &file_gateway_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayRequest) ProtoMessage() {} + +func (x *GatewayRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[0] + 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 GatewayRequest.ProtoReflect.Descriptor instead. +func (*GatewayRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{0} +} + +func (x *GatewayRequest) GetRequestType() isGatewayRequest_RequestType { + if x != nil { + return x.RequestType + } + return nil +} + +func (x *GatewayRequest) GetQuery() *Query { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_Query); ok { + return x.Query + } + } + return nil +} + +func (x *GatewayRequest) GetCancelQuery() *CancelQuery { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_CancelQuery); ok { + return x.CancelQuery + } + } + return nil +} + +func (x *GatewayRequest) GetExpand() *Expand { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_Expand); ok { + return x.Expand + } + } + return nil +} + +func (x *GatewayRequest) GetStoreSnapshot() *StoreSnapshot { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_StoreSnapshot); ok { + return x.StoreSnapshot + } + } + return nil +} + +func (x *GatewayRequest) GetLoadSnapshot() *LoadSnapshot { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_LoadSnapshot); ok { + return x.LoadSnapshot + } + } + return nil +} + +func (x *GatewayRequest) GetStoreBookmark() *StoreBookmark { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_StoreBookmark); ok { + return x.StoreBookmark + } + } + return nil +} + +func (x *GatewayRequest) GetLoadBookmark() *LoadBookmark { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_LoadBookmark); ok { + return x.LoadBookmark + } + } + return nil +} + +func (x *GatewayRequest) GetChatMessage() *ChatMessage { + if x != nil { + if x, ok := x.RequestType.(*GatewayRequest_ChatMessage); ok { + return x.ChatMessage + } + } + return nil +} + +func (x *GatewayRequest) GetMinStatusInterval() *durationpb.Duration { + if x != nil { + return x.MinStatusInterval + } + return nil +} + +type isGatewayRequest_RequestType interface { + isGatewayRequest_RequestType() +} + +type GatewayRequest_Query struct { + // Adds a new query for items to the session, starting it immediately + Query *Query `protobuf:"bytes,1,opt,name=query,proto3,oneof"` +} + +type GatewayRequest_CancelQuery struct { + // Cancel a running query + CancelQuery *CancelQuery `protobuf:"bytes,3,opt,name=cancelQuery,proto3,oneof"` +} + +type GatewayRequest_Expand struct { + // Expand all linked items for the given item + Expand *Expand `protobuf:"bytes,7,opt,name=expand,proto3,oneof"` +} + +type GatewayRequest_StoreSnapshot struct { + // store the current session state as snapshot + StoreSnapshot *StoreSnapshot `protobuf:"bytes,10,opt,name=storeSnapshot,proto3,oneof"` +} + +type GatewayRequest_LoadSnapshot struct { + // load a snapshot into the current state + LoadSnapshot *LoadSnapshot `protobuf:"bytes,11,opt,name=loadSnapshot,proto3,oneof"` +} + +type GatewayRequest_StoreBookmark struct { + // store the current set of queries as bookmarks + StoreBookmark *StoreBookmark `protobuf:"bytes,14,opt,name=storeBookmark,proto3,oneof"` +} + +type GatewayRequest_LoadBookmark struct { + // load and execute a bookmark into the current state + LoadBookmark *LoadBookmark `protobuf:"bytes,15,opt,name=loadBookmark,proto3,oneof"` +} + +type GatewayRequest_ChatMessage struct { + // // cancel the loading of a Bookmark + // CancelLoadBookmark cancelLoadBookmark = ??; + // // undo the loading of a Bookmark + // UndoLoadBookmark undoLoadBookmark = ??; + ChatMessage *ChatMessage `protobuf:"bytes,16,opt,name=chatMessage,proto3,oneof"` +} + +func (*GatewayRequest_Query) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_CancelQuery) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_Expand) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_StoreSnapshot) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_LoadSnapshot) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_StoreBookmark) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_LoadBookmark) isGatewayRequest_RequestType() {} + +func (*GatewayRequest_ChatMessage) isGatewayRequest_RequestType() {} + +// The gateway will always respond with this type of message, +// however the purpose of it is purely as a wrapper to the many different types +// of messages that the gateway can send +type GatewayResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to ResponseType: + // + // *GatewayResponse_NewItem + // *GatewayResponse_NewEdge + // *GatewayResponse_Status + // *GatewayResponse_Error + // *GatewayResponse_QueryError + // *GatewayResponse_DeleteItem + // *GatewayResponse_DeleteEdge + // *GatewayResponse_UpdateItem + // *GatewayResponse_SnapshotStoreResult + // *GatewayResponse_SnapshotLoadResult + // *GatewayResponse_BookmarkStoreResult + // *GatewayResponse_BookmarkLoadResult + // *GatewayResponse_QueryStatus + // *GatewayResponse_ChatResponse + // *GatewayResponse_ToolStart + // *GatewayResponse_ToolFinish + ResponseType isGatewayResponse_ResponseType `protobuf_oneof:"response_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayResponse) Reset() { + *x = GatewayResponse{} + mi := &file_gateway_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayResponse) ProtoMessage() {} + +func (x *GatewayResponse) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[1] + 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 GatewayResponse.ProtoReflect.Descriptor instead. +func (*GatewayResponse) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{1} +} + +func (x *GatewayResponse) GetResponseType() isGatewayResponse_ResponseType { + if x != nil { + return x.ResponseType + } + return nil +} + +func (x *GatewayResponse) GetNewItem() *Item { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_NewItem); ok { + return x.NewItem + } + } + return nil +} + +func (x *GatewayResponse) GetNewEdge() *Edge { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_NewEdge); ok { + return x.NewEdge + } + } + return nil +} + +func (x *GatewayResponse) GetStatus() *GatewayRequestStatus { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_Status); ok { + return x.Status + } + } + return nil +} + +func (x *GatewayResponse) GetError() string { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_Error); ok { + return x.Error + } + } + return "" +} + +func (x *GatewayResponse) GetQueryError() *QueryError { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_QueryError); ok { + return x.QueryError + } + } + return nil +} + +func (x *GatewayResponse) GetDeleteItem() *Reference { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_DeleteItem); ok { + return x.DeleteItem + } + } + return nil +} + +func (x *GatewayResponse) GetDeleteEdge() *Edge { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_DeleteEdge); ok { + return x.DeleteEdge + } + } + return nil +} + +func (x *GatewayResponse) GetUpdateItem() *Item { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_UpdateItem); ok { + return x.UpdateItem + } + } + return nil +} + +func (x *GatewayResponse) GetSnapshotStoreResult() *SnapshotStoreResult { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_SnapshotStoreResult); ok { + return x.SnapshotStoreResult + } + } + return nil +} + +func (x *GatewayResponse) GetSnapshotLoadResult() *SnapshotLoadResult { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_SnapshotLoadResult); ok { + return x.SnapshotLoadResult + } + } + return nil +} + +func (x *GatewayResponse) GetBookmarkStoreResult() *BookmarkStoreResult { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_BookmarkStoreResult); ok { + return x.BookmarkStoreResult + } + } + return nil +} + +func (x *GatewayResponse) GetBookmarkLoadResult() *BookmarkLoadResult { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_BookmarkLoadResult); ok { + return x.BookmarkLoadResult + } + } + return nil +} + +func (x *GatewayResponse) GetQueryStatus() *QueryStatus { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_QueryStatus); ok { + return x.QueryStatus + } + } + return nil +} + +func (x *GatewayResponse) GetChatResponse() *ChatResponse { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_ChatResponse); ok { + return x.ChatResponse + } + } + return nil +} + +func (x *GatewayResponse) GetToolStart() *ToolStart { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_ToolStart); ok { + return x.ToolStart + } + } + return nil +} + +func (x *GatewayResponse) GetToolFinish() *ToolFinish { + if x != nil { + if x, ok := x.ResponseType.(*GatewayResponse_ToolFinish); ok { + return x.ToolFinish + } + } + return nil +} + +type isGatewayResponse_ResponseType interface { + isGatewayResponse_ResponseType() +} + +type GatewayResponse_NewItem struct { + NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered +} + +type GatewayResponse_NewEdge struct { + NewEdge *Edge `protobuf:"bytes,3,opt,name=newEdge,proto3,oneof"` // A new edge between two items +} + +type GatewayResponse_Status struct { + Status *GatewayRequestStatus `protobuf:"bytes,4,opt,name=status,proto3,oneof"` // Status of the overall request +} + +type GatewayResponse_Error struct { + Error string `protobuf:"bytes,5,opt,name=error,proto3,oneof"` // An error that means the request couldn't be executed +} + +type GatewayResponse_QueryError struct { + QueryError *QueryError `protobuf:"bytes,6,opt,name=queryError,proto3,oneof"` // A new error that was encountered as part of a query +} + +type GatewayResponse_DeleteItem struct { + DeleteItem *Reference `protobuf:"bytes,7,opt,name=deleteItem,proto3,oneof"` // An item that should be deleted from local state +} + +type GatewayResponse_DeleteEdge struct { + DeleteEdge *Edge `protobuf:"bytes,8,opt,name=deleteEdge,proto3,oneof"` // An edge that should be deleted form local state +} + +type GatewayResponse_UpdateItem struct { + UpdateItem *Item `protobuf:"bytes,9,opt,name=updateItem,proto3,oneof"` // An item that has already been sent, but contains new data, it should be updated to reflect this version +} + +type GatewayResponse_SnapshotStoreResult struct { + SnapshotStoreResult *SnapshotStoreResult `protobuf:"bytes,11,opt,name=snapshotStoreResult,proto3,oneof"` +} + +type GatewayResponse_SnapshotLoadResult struct { + SnapshotLoadResult *SnapshotLoadResult `protobuf:"bytes,12,opt,name=snapshotLoadResult,proto3,oneof"` +} + +type GatewayResponse_BookmarkStoreResult struct { + BookmarkStoreResult *BookmarkStoreResult `protobuf:"bytes,15,opt,name=bookmarkStoreResult,proto3,oneof"` +} + +type GatewayResponse_BookmarkLoadResult struct { + BookmarkLoadResult *BookmarkLoadResult `protobuf:"bytes,16,opt,name=bookmarkLoadResult,proto3,oneof"` +} + +type GatewayResponse_QueryStatus struct { + QueryStatus *QueryStatus `protobuf:"bytes,17,opt,name=queryStatus,proto3,oneof"` // Status of requested queries +} + +type GatewayResponse_ChatResponse struct { + ChatResponse *ChatResponse `protobuf:"bytes,18,opt,name=chatResponse,proto3,oneof"` +} + +type GatewayResponse_ToolStart struct { + ToolStart *ToolStart `protobuf:"bytes,19,opt,name=toolStart,proto3,oneof"` +} + +type GatewayResponse_ToolFinish struct { + ToolFinish *ToolFinish `protobuf:"bytes,20,opt,name=toolFinish,proto3,oneof"` +} + +func (*GatewayResponse_NewItem) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_NewEdge) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_Status) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_Error) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_QueryError) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_DeleteItem) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_DeleteEdge) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_UpdateItem) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_SnapshotStoreResult) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_SnapshotLoadResult) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_BookmarkStoreResult) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_BookmarkLoadResult) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_QueryStatus) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_ChatResponse) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_ToolStart) isGatewayResponse_ResponseType() {} + +func (*GatewayResponse_ToolFinish) isGatewayResponse_ResponseType() {} + +// Contains the status of the gateway request. +type GatewayRequestStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Summary *GatewayRequestStatus_Summary `protobuf:"bytes,3,opt,name=summary,proto3" json:"summary,omitempty"` + // Whether all items have finished being processed by the gateway. It is + // possible for all responders to be complete, but the gateway is still + // working. A request should only be considered complete when all working == + // 0 and postProcessingComplete == true + PostProcessingComplete bool `protobuf:"varint,4,opt,name=postProcessingComplete,proto3" json:"postProcessingComplete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayRequestStatus) Reset() { + *x = GatewayRequestStatus{} + mi := &file_gateway_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayRequestStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayRequestStatus) ProtoMessage() {} + +func (x *GatewayRequestStatus) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[2] + 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 GatewayRequestStatus.ProtoReflect.Descriptor instead. +func (*GatewayRequestStatus) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{2} +} + +func (x *GatewayRequestStatus) GetSummary() *GatewayRequestStatus_Summary { + if x != nil { + return x.Summary + } + return nil +} + +func (x *GatewayRequestStatus) GetPostProcessingComplete() bool { + if x != nil { + return x.PostProcessingComplete + } + return false +} + +// Ask the gateway to store the current state as bookmark with the specified details. +// Returns a BookmarkStored message when the bookmark is stored +type StoreBookmark struct { + state protoimpl.MessageState `protogen:"open.v1"` + // user supplied name of this bookmark + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // user supplied description of this bookmark + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // a correlation ID to match up requests and responses. set this to a value unique per connection + MsgID []byte `protobuf:"bytes,3,opt,name=msgID,proto3" json:"msgID,omitempty"` + // whether this bookmark should be stored as a system bookmark. System + // bookmarks are hidden and can only be returned via the UUID, they don't + // show up in lists + IsSystem bool `protobuf:"varint,4,opt,name=isSystem,proto3" json:"isSystem,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StoreBookmark) Reset() { + *x = StoreBookmark{} + mi := &file_gateway_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StoreBookmark) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoreBookmark) ProtoMessage() {} + +func (x *StoreBookmark) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[3] + 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 StoreBookmark.ProtoReflect.Descriptor instead. +func (*StoreBookmark) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{3} +} + +func (x *StoreBookmark) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StoreBookmark) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *StoreBookmark) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +func (x *StoreBookmark) GetIsSystem() bool { + if x != nil { + return x.IsSystem + } + return false +} + +// After a bookmark is successfully stored, this reply with the new bookmark's details is sent. +type BookmarkStoreResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID + MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` + // UUID of the newly created bookmark + BookmarkID []byte `protobuf:"bytes,5,opt,name=bookmarkID,proto3" json:"bookmarkID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BookmarkStoreResult) Reset() { + *x = BookmarkStoreResult{} + mi := &file_gateway_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BookmarkStoreResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BookmarkStoreResult) ProtoMessage() {} + +func (x *BookmarkStoreResult) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[4] + 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 BookmarkStoreResult.ProtoReflect.Descriptor instead. +func (*BookmarkStoreResult) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{4} +} + +func (x *BookmarkStoreResult) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *BookmarkStoreResult) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *BookmarkStoreResult) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +func (x *BookmarkStoreResult) GetBookmarkID() []byte { + if x != nil { + return x.BookmarkID + } + return nil +} + +// Ask the gateway to load the specified bookmark into the current state. +// Results are streamed to the client in the same way query results are. +type LoadBookmark struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id of the bookmark to load + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // a correlation ID to match up requests and responses. set this to a value unique per connection + MsgID []byte `protobuf:"bytes,2,opt,name=msgID,proto3" json:"msgID,omitempty"` + // set to true to force fetching fresh data + IgnoreCache bool `protobuf:"varint,3,opt,name=ignoreCache,proto3" json:"ignoreCache,omitempty"` + // The time at which the gateway should stop processing the queries spawned by this request + Deadline *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deadline,proto3" json:"deadline,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoadBookmark) Reset() { + *x = LoadBookmark{} + mi := &file_gateway_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoadBookmark) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadBookmark) ProtoMessage() {} + +func (x *LoadBookmark) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[5] + 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 LoadBookmark.ProtoReflect.Descriptor instead. +func (*LoadBookmark) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{5} +} + +func (x *LoadBookmark) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *LoadBookmark) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +func (x *LoadBookmark) GetIgnoreCache() bool { + if x != nil { + return x.IgnoreCache + } + return false +} + +func (x *LoadBookmark) GetDeadline() *timestamppb.Timestamp { + if x != nil { + return x.Deadline + } + return nil +} + +type BookmarkLoadResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + // UUIDs of all queries that have been started as a result of loading this bookmark + StartedQueryUUIDs [][]byte `protobuf:"bytes,3,rep,name=startedQueryUUIDs,proto3" json:"startedQueryUUIDs,omitempty"` + // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID + MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BookmarkLoadResult) Reset() { + *x = BookmarkLoadResult{} + mi := &file_gateway_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BookmarkLoadResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BookmarkLoadResult) ProtoMessage() {} + +func (x *BookmarkLoadResult) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[6] + 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 BookmarkLoadResult.ProtoReflect.Descriptor instead. +func (*BookmarkLoadResult) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{6} +} + +func (x *BookmarkLoadResult) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *BookmarkLoadResult) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *BookmarkLoadResult) GetStartedQueryUUIDs() [][]byte { + if x != nil { + return x.StartedQueryUUIDs + } + return nil +} + +func (x *BookmarkLoadResult) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +// Ask the gateway to store the current state as snapshot with the specified details. +// Returns a SnapshotStored message when the snapshot is stored +type StoreSnapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + // user supplied name of this snapshot + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // user supplied description of this snapshot + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // a correlation ID to match up requests and responses. set this to a value unique per connection + MsgID []byte `protobuf:"bytes,3,opt,name=msgID,proto3" json:"msgID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StoreSnapshot) Reset() { + *x = StoreSnapshot{} + mi := &file_gateway_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StoreSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoreSnapshot) ProtoMessage() {} + +func (x *StoreSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[7] + 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 StoreSnapshot.ProtoReflect.Descriptor instead. +func (*StoreSnapshot) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{7} +} + +func (x *StoreSnapshot) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StoreSnapshot) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *StoreSnapshot) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +// After a snapshot is successfully stored, this reply with the new snapshot's details is sent. +type SnapshotStoreResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID + MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` + SnapshotID []byte `protobuf:"bytes,5,opt,name=snapshotID,proto3" json:"snapshotID,omitempty"` // The UUID of the newly stored snapshot + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SnapshotStoreResult) Reset() { + *x = SnapshotStoreResult{} + mi := &file_gateway_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SnapshotStoreResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotStoreResult) ProtoMessage() {} + +func (x *SnapshotStoreResult) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[8] + 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 SnapshotStoreResult.ProtoReflect.Descriptor instead. +func (*SnapshotStoreResult) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{8} +} + +func (x *SnapshotStoreResult) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *SnapshotStoreResult) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *SnapshotStoreResult) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +func (x *SnapshotStoreResult) GetSnapshotID() []byte { + if x != nil { + return x.SnapshotID + } + return nil +} + +// Ask the gateway to load the specified snapshot into the current state. +// Results are streamed to the client in the same way query results are. +type LoadSnapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id of the snapshot to load + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // a correlation ID to match up requests and responses. set this to a value unique per connection + MsgID []byte `protobuf:"bytes,2,opt,name=msgID,proto3" json:"msgID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoadSnapshot) Reset() { + *x = LoadSnapshot{} + mi := &file_gateway_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoadSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadSnapshot) ProtoMessage() {} + +func (x *LoadSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[9] + 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 LoadSnapshot.ProtoReflect.Descriptor instead. +func (*LoadSnapshot) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{9} +} + +func (x *LoadSnapshot) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *LoadSnapshot) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +type SnapshotLoadResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` + // a correlation ID to match up requests and responses. this field returns the contents of the request's msgID + MsgID []byte `protobuf:"bytes,4,opt,name=msgID,proto3" json:"msgID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SnapshotLoadResult) Reset() { + *x = SnapshotLoadResult{} + mi := &file_gateway_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SnapshotLoadResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotLoadResult) ProtoMessage() {} + +func (x *SnapshotLoadResult) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[10] + 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 SnapshotLoadResult.ProtoReflect.Descriptor instead. +func (*SnapshotLoadResult) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{10} +} + +func (x *SnapshotLoadResult) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *SnapshotLoadResult) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +func (x *SnapshotLoadResult) GetMsgID() []byte { + if x != nil { + return x.MsgID + } + return nil +} + +type ChatMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The message to create + // + // Types that are valid to be assigned to RequestType: + // + // *ChatMessage_Text + // *ChatMessage_Cancel + RequestType isChatMessage_RequestType `protobuf_oneof:"request_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatMessage) Reset() { + *x = ChatMessage{} + mi := &file_gateway_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatMessage) ProtoMessage() {} + +func (x *ChatMessage) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[11] + 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 ChatMessage.ProtoReflect.Descriptor instead. +func (*ChatMessage) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{11} +} + +func (x *ChatMessage) GetRequestType() isChatMessage_RequestType { + if x != nil { + return x.RequestType + } + return nil +} + +func (x *ChatMessage) GetText() string { + if x != nil { + if x, ok := x.RequestType.(*ChatMessage_Text); ok { + return x.Text + } + } + return "" +} + +func (x *ChatMessage) GetCancel() bool { + if x != nil { + if x, ok := x.RequestType.(*ChatMessage_Cancel); ok { + return x.Cancel + } + } + return false +} + +type isChatMessage_RequestType interface { + isChatMessage_RequestType() +} + +type ChatMessage_Text struct { + Text string `protobuf:"bytes,1,opt,name=text,proto3,oneof"` +} + +type ChatMessage_Cancel struct { + // Cancel the last message sent to openAI, includes the message and tools that were started + Cancel bool `protobuf:"varint,2,opt,name=cancel,proto3,oneof"` +} + +func (*ChatMessage_Text) isChatMessage_RequestType() {} + +func (*ChatMessage_Cancel) isChatMessage_RequestType() {} + +type ToolMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A unique ID that tracks this tool call and can be used to correlate messages + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolMetadata) Reset() { + *x = ToolMetadata{} + mi := &file_gateway_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolMetadata) ProtoMessage() {} + +func (x *ToolMetadata) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[12] + 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 ToolMetadata.ProtoReflect.Descriptor instead. +func (*ToolMetadata) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{12} +} + +func (x *ToolMetadata) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type QueryToolStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Method QueryMethod `protobuf:"varint,2,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryToolStart) Reset() { + *x = QueryToolStart{} + mi := &file_gateway_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryToolStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryToolStart) ProtoMessage() {} + +func (x *QueryToolStart) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryToolStart.ProtoReflect.Descriptor instead. +func (*QueryToolStart) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{13} +} + +func (x *QueryToolStart) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *QueryToolStart) GetMethod() QueryMethod { + if x != nil { + return x.Method + } + return QueryMethod_GET +} + +func (x *QueryToolStart) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *QueryToolStart) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +type QueryToolFinish struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumItems int32 `protobuf:"varint,1,opt,name=numItems,proto3" json:"numItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryToolFinish) Reset() { + *x = QueryToolFinish{} + mi := &file_gateway_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryToolFinish) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryToolFinish) ProtoMessage() {} + +func (x *QueryToolFinish) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryToolFinish.ProtoReflect.Descriptor instead. +func (*QueryToolFinish) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{14} +} + +func (x *QueryToolFinish) GetNumItems() int32 { + if x != nil { + return x.NumItems + } + return 0 +} + +type RelationshipToolStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationshipToolStart) Reset() { + *x = RelationshipToolStart{} + mi := &file_gateway_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationshipToolStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationshipToolStart) ProtoMessage() {} + +func (x *RelationshipToolStart) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[15] + 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 RelationshipToolStart.ProtoReflect.Descriptor instead. +func (*RelationshipToolStart) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{15} +} + +func (x *RelationshipToolStart) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *RelationshipToolStart) GetUniqueAttributeValue() string { + if x != nil { + return x.UniqueAttributeValue + } + return "" +} + +func (x *RelationshipToolStart) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +type RelationshipToolFinish struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumItems int32 `protobuf:"varint,1,opt,name=numItems,proto3" json:"numItems,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationshipToolFinish) Reset() { + *x = RelationshipToolFinish{} + mi := &file_gateway_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationshipToolFinish) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationshipToolFinish) ProtoMessage() {} + +func (x *RelationshipToolFinish) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[16] + 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 RelationshipToolFinish.ProtoReflect.Descriptor instead. +func (*RelationshipToolFinish) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{16} +} + +func (x *RelationshipToolFinish) GetNumItems() int32 { + if x != nil { + return x.NumItems + } + return 0 +} + +type ChangesByReferenceToolStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangesByReferenceToolStart) Reset() { + *x = ChangesByReferenceToolStart{} + mi := &file_gateway_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangesByReferenceToolStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangesByReferenceToolStart) ProtoMessage() {} + +func (x *ChangesByReferenceToolStart) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangesByReferenceToolStart.ProtoReflect.Descriptor instead. +func (*ChangesByReferenceToolStart) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{17} +} + +func (x *ChangesByReferenceToolStart) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ChangesByReferenceToolStart) GetUniqueAttributeValue() string { + if x != nil { + return x.UniqueAttributeValue + } + return "" +} + +func (x *ChangesByReferenceToolStart) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +type ChangeByReferenceSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` // from ChangeProperties + UUID []byte `protobuf:"bytes,2,opt,name=UUID,proto3" json:"UUID,omitempty"` // from ChangeMetadata + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=createdAt,proto3" json:"createdAt,omitempty"` // From ChangeMetadata + Owner string `protobuf:"bytes,4,opt,name=owner,proto3" json:"owner,omitempty"` // From ChangeProperties + NumAffectedItems int32 `protobuf:"varint,5,opt,name=numAffectedItems,proto3" json:"numAffectedItems,omitempty"` // From ChangeMetadata + ChangeStatus ChangeStatus `protobuf:"varint,6,opt,name=changeStatus,proto3,enum=changes.ChangeStatus" json:"changeStatus,omitempty"` // From ChangeMetadata + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangeByReferenceSummary) Reset() { + *x = ChangeByReferenceSummary{} + mi := &file_gateway_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangeByReferenceSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeByReferenceSummary) ProtoMessage() {} + +func (x *ChangeByReferenceSummary) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeByReferenceSummary.ProtoReflect.Descriptor instead. +func (*ChangeByReferenceSummary) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{18} +} + +func (x *ChangeByReferenceSummary) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ChangeByReferenceSummary) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *ChangeByReferenceSummary) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ChangeByReferenceSummary) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *ChangeByReferenceSummary) GetNumAffectedItems() int32 { + if x != nil { + return x.NumAffectedItems + } + return 0 +} + +func (x *ChangeByReferenceSummary) GetChangeStatus() ChangeStatus { + if x != nil { + return x.ChangeStatus + } + return ChangeStatus_CHANGE_STATUS_UNSPECIFIED +} + +type ChangesByReferenceToolFinish struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeSummaries []*ChangeByReferenceSummary `protobuf:"bytes,1,rep,name=changeSummaries,proto3" json:"changeSummaries,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChangesByReferenceToolFinish) Reset() { + *x = ChangesByReferenceToolFinish{} + mi := &file_gateway_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChangesByReferenceToolFinish) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangesByReferenceToolFinish) ProtoMessage() {} + +func (x *ChangesByReferenceToolFinish) ProtoReflect() protoreflect.Message { + mi := &file_gateway_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 ChangesByReferenceToolFinish.ProtoReflect.Descriptor instead. +func (*ChangesByReferenceToolFinish) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{19} +} + +func (x *ChangesByReferenceToolFinish) GetChangeSummaries() []*ChangeByReferenceSummary { + if x != nil { + return x.ChangeSummaries + } + return nil +} + +type ToolStart struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *ToolMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Types that are valid to be assigned to ToolType: + // + // *ToolStart_Query + // *ToolStart_Relationship + // *ToolStart_ChangesByReference + ToolType isToolStart_ToolType `protobuf_oneof:"tool_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolStart) Reset() { + *x = ToolStart{} + mi := &file_gateway_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolStart) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolStart) ProtoMessage() {} + +func (x *ToolStart) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[20] + 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 ToolStart.ProtoReflect.Descriptor instead. +func (*ToolStart) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{20} +} + +func (x *ToolStart) GetMetadata() *ToolMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *ToolStart) GetToolType() isToolStart_ToolType { + if x != nil { + return x.ToolType + } + return nil +} + +func (x *ToolStart) GetQuery() *QueryToolStart { + if x != nil { + if x, ok := x.ToolType.(*ToolStart_Query); ok { + return x.Query + } + } + return nil +} + +func (x *ToolStart) GetRelationship() *RelationshipToolStart { + if x != nil { + if x, ok := x.ToolType.(*ToolStart_Relationship); ok { + return x.Relationship + } + } + return nil +} + +func (x *ToolStart) GetChangesByReference() *ChangesByReferenceToolStart { + if x != nil { + if x, ok := x.ToolType.(*ToolStart_ChangesByReference); ok { + return x.ChangesByReference + } + } + return nil +} + +type isToolStart_ToolType interface { + isToolStart_ToolType() +} + +type ToolStart_Query struct { + Query *QueryToolStart `protobuf:"bytes,2,opt,name=query,proto3,oneof"` +} + +type ToolStart_Relationship struct { + Relationship *RelationshipToolStart `protobuf:"bytes,3,opt,name=relationship,proto3,oneof"` +} + +type ToolStart_ChangesByReference struct { + ChangesByReference *ChangesByReferenceToolStart `protobuf:"bytes,4,opt,name=changesByReference,proto3,oneof"` +} + +func (*ToolStart_Query) isToolStart_ToolType() {} + +func (*ToolStart_Relationship) isToolStart_ToolType() {} + +func (*ToolStart_ChangesByReference) isToolStart_ToolType() {} + +type ToolFinish struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *ToolMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + // Types that are valid to be assigned to ToolType: + // + // *ToolFinish_Query + // *ToolFinish_Relationship + // *ToolFinish_ChangesByReference + ToolType isToolFinish_ToolType `protobuf_oneof:"tool_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToolFinish) Reset() { + *x = ToolFinish{} + mi := &file_gateway_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToolFinish) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToolFinish) ProtoMessage() {} + +func (x *ToolFinish) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[21] + 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 ToolFinish.ProtoReflect.Descriptor instead. +func (*ToolFinish) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{21} +} + +func (x *ToolFinish) GetMetadata() *ToolMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *ToolFinish) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *ToolFinish) GetToolType() isToolFinish_ToolType { + if x != nil { + return x.ToolType + } + return nil +} + +func (x *ToolFinish) GetQuery() *QueryToolFinish { + if x != nil { + if x, ok := x.ToolType.(*ToolFinish_Query); ok { + return x.Query + } + } + return nil +} + +func (x *ToolFinish) GetRelationship() *RelationshipToolFinish { + if x != nil { + if x, ok := x.ToolType.(*ToolFinish_Relationship); ok { + return x.Relationship + } + } + return nil +} + +func (x *ToolFinish) GetChangesByReference() *ChangesByReferenceToolFinish { + if x != nil { + if x, ok := x.ToolType.(*ToolFinish_ChangesByReference); ok { + return x.ChangesByReference + } + } + return nil +} + +type isToolFinish_ToolType interface { + isToolFinish_ToolType() +} + +type ToolFinish_Query struct { + Query *QueryToolFinish `protobuf:"bytes,3,opt,name=query,proto3,oneof"` +} + +type ToolFinish_Relationship struct { + Relationship *RelationshipToolFinish `protobuf:"bytes,4,opt,name=relationship,proto3,oneof"` +} + +type ToolFinish_ChangesByReference struct { + ChangesByReference *ChangesByReferenceToolFinish `protobuf:"bytes,5,opt,name=changesByReference,proto3,oneof"` +} + +func (*ToolFinish_Query) isToolFinish_ToolType() {} + +func (*ToolFinish_Relationship) isToolFinish_ToolType() {} + +func (*ToolFinish_ChangesByReference) isToolFinish_ToolType() {} + +type ChatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatResponse) Reset() { + *x = ChatResponse{} + mi := &file_gateway_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatResponse) ProtoMessage() {} + +func (x *ChatResponse) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[22] + 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 ChatResponse.ProtoReflect.Descriptor instead. +func (*ChatResponse) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{22} +} + +func (x *ChatResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *ChatResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type GatewayRequestStatus_Summary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Working int32 `protobuf:"varint,1,opt,name=working,proto3" json:"working,omitempty"` + Stalled int32 `protobuf:"varint,2,opt,name=stalled,proto3" json:"stalled,omitempty"` + Complete int32 `protobuf:"varint,3,opt,name=complete,proto3" json:"complete,omitempty"` + Error int32 `protobuf:"varint,4,opt,name=error,proto3" json:"error,omitempty"` + Cancelled int32 `protobuf:"varint,5,opt,name=cancelled,proto3" json:"cancelled,omitempty"` + Responders int32 `protobuf:"varint,6,opt,name=responders,proto3" json:"responders,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayRequestStatus_Summary) Reset() { + *x = GatewayRequestStatus_Summary{} + mi := &file_gateway_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayRequestStatus_Summary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayRequestStatus_Summary) ProtoMessage() {} + +func (x *GatewayRequestStatus_Summary) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[23] + 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 GatewayRequestStatus_Summary.ProtoReflect.Descriptor instead. +func (*GatewayRequestStatus_Summary) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *GatewayRequestStatus_Summary) GetWorking() int32 { + if x != nil { + return x.Working + } + return 0 +} + +func (x *GatewayRequestStatus_Summary) GetStalled() int32 { + if x != nil { + return x.Stalled + } + return 0 +} + +func (x *GatewayRequestStatus_Summary) GetComplete() int32 { + if x != nil { + return x.Complete + } + return 0 +} + +func (x *GatewayRequestStatus_Summary) GetError() int32 { + if x != nil { + return x.Error + } + return 0 +} + +func (x *GatewayRequestStatus_Summary) GetCancelled() int32 { + if x != nil { + return x.Cancelled + } + return 0 +} + +func (x *GatewayRequestStatus_Summary) GetResponders() int32 { + if x != nil { + return x.Responders + } + return 0 +} + +var File_gateway_proto protoreflect.FileDescriptor + +const file_gateway_proto_rawDesc = "" + + "\n" + + "\rgateway.proto\x12\agateway\x1a\rchanges.proto\x1a\vitems.proto\x1a\x0fresponses.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xad\x04\n" + + "\x0eGatewayRequest\x12\x1e\n" + + "\x05query\x18\x01 \x01(\v2\x06.QueryH\x00R\x05query\x120\n" + + "\vcancelQuery\x18\x03 \x01(\v2\f.CancelQueryH\x00R\vcancelQuery\x12!\n" + + "\x06expand\x18\a \x01(\v2\a.ExpandH\x00R\x06expand\x12>\n" + + "\rstoreSnapshot\x18\n" + + " \x01(\v2\x16.gateway.StoreSnapshotH\x00R\rstoreSnapshot\x12;\n" + + "\floadSnapshot\x18\v \x01(\v2\x15.gateway.LoadSnapshotH\x00R\floadSnapshot\x12>\n" + + "\rstoreBookmark\x18\x0e \x01(\v2\x16.gateway.StoreBookmarkH\x00R\rstoreBookmark\x12;\n" + + "\floadBookmark\x18\x0f \x01(\v2\x15.gateway.LoadBookmarkH\x00R\floadBookmark\x128\n" + + "\vchatMessage\x18\x10 \x01(\v2\x14.gateway.ChatMessageH\x00R\vchatMessage\x12L\n" + + "\x11minStatusInterval\x18\x02 \x01(\v2\x19.google.protobuf.DurationH\x01R\x11minStatusInterval\x88\x01\x01B\x0e\n" + + "\frequest_typeB\x14\n" + + "\x12_minStatusInterval\"\x84\a\n" + + "\x0fGatewayResponse\x12!\n" + + "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12!\n" + + "\anewEdge\x18\x03 \x01(\v2\x05.EdgeH\x00R\anewEdge\x127\n" + + "\x06status\x18\x04 \x01(\v2\x1d.gateway.GatewayRequestStatusH\x00R\x06status\x12\x16\n" + + "\x05error\x18\x05 \x01(\tH\x00R\x05error\x12-\n" + + "\n" + + "queryError\x18\x06 \x01(\v2\v.QueryErrorH\x00R\n" + + "queryError\x12,\n" + + "\n" + + "deleteItem\x18\a \x01(\v2\n" + + ".ReferenceH\x00R\n" + + "deleteItem\x12'\n" + + "\n" + + "deleteEdge\x18\b \x01(\v2\x05.EdgeH\x00R\n" + + "deleteEdge\x12'\n" + + "\n" + + "updateItem\x18\t \x01(\v2\x05.ItemH\x00R\n" + + "updateItem\x12P\n" + + "\x13snapshotStoreResult\x18\v \x01(\v2\x1c.gateway.SnapshotStoreResultH\x00R\x13snapshotStoreResult\x12M\n" + + "\x12snapshotLoadResult\x18\f \x01(\v2\x1b.gateway.SnapshotLoadResultH\x00R\x12snapshotLoadResult\x12P\n" + + "\x13bookmarkStoreResult\x18\x0f \x01(\v2\x1c.gateway.BookmarkStoreResultH\x00R\x13bookmarkStoreResult\x12M\n" + + "\x12bookmarkLoadResult\x18\x10 \x01(\v2\x1b.gateway.BookmarkLoadResultH\x00R\x12bookmarkLoadResult\x120\n" + + "\vqueryStatus\x18\x11 \x01(\v2\f.QueryStatusH\x00R\vqueryStatus\x12;\n" + + "\fchatResponse\x18\x12 \x01(\v2\x15.gateway.ChatResponseH\x00R\fchatResponse\x122\n" + + "\ttoolStart\x18\x13 \x01(\v2\x12.gateway.ToolStartH\x00R\ttoolStart\x125\n" + + "\n" + + "toolFinish\x18\x14 \x01(\v2\x13.gateway.ToolFinishH\x00R\n" + + "toolFinishB\x0f\n" + + "\rresponse_type\"\xc5\x02\n" + + "\x14GatewayRequestStatus\x12?\n" + + "\asummary\x18\x03 \x01(\v2%.gateway.GatewayRequestStatus.SummaryR\asummary\x126\n" + + "\x16postProcessingComplete\x18\x04 \x01(\bR\x16postProcessingComplete\x1a\xad\x01\n" + + "\aSummary\x12\x18\n" + + "\aworking\x18\x01 \x01(\x05R\aworking\x12\x18\n" + + "\astalled\x18\x02 \x01(\x05R\astalled\x12\x1a\n" + + "\bcomplete\x18\x03 \x01(\x05R\bcomplete\x12\x14\n" + + "\x05error\x18\x04 \x01(\x05R\x05error\x12\x1c\n" + + "\tcancelled\x18\x05 \x01(\x05R\tcancelled\x12\x1e\n" + + "\n" + + "responders\x18\x06 \x01(\x05R\n" + + "respondersJ\x04\b\x01\x10\x02\"w\n" + + "\rStoreBookmark\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x14\n" + + "\x05msgID\x18\x03 \x01(\fR\x05msgID\x12\x1a\n" + + "\bisSystem\x18\x04 \x01(\bR\bisSystem\"\x8f\x01\n" + + "\x13BookmarkStoreResult\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + + "\x05msgID\x18\x04 \x01(\fR\x05msgID\x12\x1e\n" + + "\n" + + "bookmarkID\x18\x05 \x01(\fR\n" + + "bookmarkIDJ\x04\b\x03\x10\x04\"\x92\x01\n" + + "\fLoadBookmark\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + + "\x05msgID\x18\x02 \x01(\fR\x05msgID\x12 \n" + + "\vignoreCache\x18\x03 \x01(\bR\vignoreCache\x126\n" + + "\bdeadline\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\"\x96\x01\n" + + "\x12BookmarkLoadResult\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12,\n" + + "\x11startedQueryUUIDs\x18\x03 \x03(\fR\x11startedQueryUUIDs\x12\x14\n" + + "\x05msgID\x18\x04 \x01(\fR\x05msgID\"[\n" + + "\rStoreSnapshot\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x14\n" + + "\x05msgID\x18\x03 \x01(\fR\x05msgID\"\x8f\x01\n" + + "\x13SnapshotStoreResult\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + + "\x05msgID\x18\x04 \x01(\fR\x05msgID\x12\x1e\n" + + "\n" + + "snapshotID\x18\x05 \x01(\fR\n" + + "snapshotIDJ\x04\b\x03\x10\x04\"8\n" + + "\fLoadSnapshot\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12\x14\n" + + "\x05msgID\x18\x02 \x01(\fR\x05msgID\"h\n" + + "\x12SnapshotLoadResult\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\"\n" + + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\x12\x14\n" + + "\x05msgID\x18\x04 \x01(\fR\x05msgID\"M\n" + + "\vChatMessage\x12\x14\n" + + "\x04text\x18\x01 \x01(\tH\x00R\x04text\x12\x18\n" + + "\x06cancel\x18\x02 \x01(\bH\x00R\x06cancelB\x0e\n" + + "\frequest_type\"\x1e\n" + + "\fToolMetadata\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"v\n" + + "\x0eQueryToolStart\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + + "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + + "\x05query\x18\x03 \x01(\tR\x05query\x12\x14\n" + + "\x05scope\x18\x04 \x01(\tR\x05scope\"-\n" + + "\x0fQueryToolFinish\x12\x1a\n" + + "\bnumItems\x18\x01 \x01(\x05R\bnumItems\"u\n" + + "\x15RelationshipToolStart\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + + "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\"4\n" + + "\x16RelationshipToolFinish\x12\x1a\n" + + "\bnumItems\x18\x01 \x01(\x05R\bnumItems\"{\n" + + "\x1bChangesByReferenceToolStart\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + + "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\"\xfb\x01\n" + + "\x18ChangeByReferenceSummary\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" + + "\x04UUID\x18\x02 \x01(\fR\x04UUID\x128\n" + + "\tcreatedAt\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x14\n" + + "\x05owner\x18\x04 \x01(\tR\x05owner\x12*\n" + + "\x10numAffectedItems\x18\x05 \x01(\x05R\x10numAffectedItems\x129\n" + + "\fchangeStatus\x18\x06 \x01(\x0e2\x15.changes.ChangeStatusR\fchangeStatus\"k\n" + + "\x1cChangesByReferenceToolFinish\x12K\n" + + "\x0fchangeSummaries\x18\x01 \x03(\v2!.gateway.ChangeByReferenceSummaryR\x0fchangeSummaries\"\x9a\x02\n" + + "\tToolStart\x121\n" + + "\bmetadata\x18\x01 \x01(\v2\x15.gateway.ToolMetadataR\bmetadata\x12/\n" + + "\x05query\x18\x02 \x01(\v2\x17.gateway.QueryToolStartH\x00R\x05query\x12D\n" + + "\frelationship\x18\x03 \x01(\v2\x1e.gateway.RelationshipToolStartH\x00R\frelationship\x12V\n" + + "\x12changesByReference\x18\x04 \x01(\v2$.gateway.ChangesByReferenceToolStartH\x00R\x12changesByReferenceB\v\n" + + "\ttool_type\"\xb4\x02\n" + + "\n" + + "ToolFinish\x121\n" + + "\bmetadata\x18\x01 \x01(\v2\x15.gateway.ToolMetadataR\bmetadata\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\x120\n" + + "\x05query\x18\x03 \x01(\v2\x18.gateway.QueryToolFinishH\x00R\x05query\x12E\n" + + "\frelationship\x18\x04 \x01(\v2\x1f.gateway.RelationshipToolFinishH\x00R\frelationship\x12W\n" + + "\x12changesByReference\x18\x05 \x01(\v2%.gateway.ChangesByReferenceToolFinishH\x00R\x12changesByReferenceB\v\n" + + "\ttool_type\"8\n" + + "\fChatResponse\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05errorB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_gateway_proto_rawDescOnce sync.Once + file_gateway_proto_rawDescData []byte +) + +func file_gateway_proto_rawDescGZIP() []byte { + file_gateway_proto_rawDescOnce.Do(func() { + file_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gateway_proto_rawDesc), len(file_gateway_proto_rawDesc))) + }) + return file_gateway_proto_rawDescData +} + +var file_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_gateway_proto_goTypes = []any{ + (*GatewayRequest)(nil), // 0: gateway.GatewayRequest + (*GatewayResponse)(nil), // 1: gateway.GatewayResponse + (*GatewayRequestStatus)(nil), // 2: gateway.GatewayRequestStatus + (*StoreBookmark)(nil), // 3: gateway.StoreBookmark + (*BookmarkStoreResult)(nil), // 4: gateway.BookmarkStoreResult + (*LoadBookmark)(nil), // 5: gateway.LoadBookmark + (*BookmarkLoadResult)(nil), // 6: gateway.BookmarkLoadResult + (*StoreSnapshot)(nil), // 7: gateway.StoreSnapshot + (*SnapshotStoreResult)(nil), // 8: gateway.SnapshotStoreResult + (*LoadSnapshot)(nil), // 9: gateway.LoadSnapshot + (*SnapshotLoadResult)(nil), // 10: gateway.SnapshotLoadResult + (*ChatMessage)(nil), // 11: gateway.ChatMessage + (*ToolMetadata)(nil), // 12: gateway.ToolMetadata + (*QueryToolStart)(nil), // 13: gateway.QueryToolStart + (*QueryToolFinish)(nil), // 14: gateway.QueryToolFinish + (*RelationshipToolStart)(nil), // 15: gateway.RelationshipToolStart + (*RelationshipToolFinish)(nil), // 16: gateway.RelationshipToolFinish + (*ChangesByReferenceToolStart)(nil), // 17: gateway.ChangesByReferenceToolStart + (*ChangeByReferenceSummary)(nil), // 18: gateway.ChangeByReferenceSummary + (*ChangesByReferenceToolFinish)(nil), // 19: gateway.ChangesByReferenceToolFinish + (*ToolStart)(nil), // 20: gateway.ToolStart + (*ToolFinish)(nil), // 21: gateway.ToolFinish + (*ChatResponse)(nil), // 22: gateway.ChatResponse + (*GatewayRequestStatus_Summary)(nil), // 23: gateway.GatewayRequestStatus.Summary + (*Query)(nil), // 24: Query + (*CancelQuery)(nil), // 25: CancelQuery + (*Expand)(nil), // 26: Expand + (*durationpb.Duration)(nil), // 27: google.protobuf.Duration + (*Item)(nil), // 28: Item + (*Edge)(nil), // 29: Edge + (*QueryError)(nil), // 30: QueryError + (*Reference)(nil), // 31: Reference + (*QueryStatus)(nil), // 32: QueryStatus + (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp + (QueryMethod)(0), // 34: QueryMethod + (ChangeStatus)(0), // 35: changes.ChangeStatus +} +var file_gateway_proto_depIdxs = []int32{ + 24, // 0: gateway.GatewayRequest.query:type_name -> Query + 25, // 1: gateway.GatewayRequest.cancelQuery:type_name -> CancelQuery + 26, // 2: gateway.GatewayRequest.expand:type_name -> Expand + 7, // 3: gateway.GatewayRequest.storeSnapshot:type_name -> gateway.StoreSnapshot + 9, // 4: gateway.GatewayRequest.loadSnapshot:type_name -> gateway.LoadSnapshot + 3, // 5: gateway.GatewayRequest.storeBookmark:type_name -> gateway.StoreBookmark + 5, // 6: gateway.GatewayRequest.loadBookmark:type_name -> gateway.LoadBookmark + 11, // 7: gateway.GatewayRequest.chatMessage:type_name -> gateway.ChatMessage + 27, // 8: gateway.GatewayRequest.minStatusInterval:type_name -> google.protobuf.Duration + 28, // 9: gateway.GatewayResponse.newItem:type_name -> Item + 29, // 10: gateway.GatewayResponse.newEdge:type_name -> Edge + 2, // 11: gateway.GatewayResponse.status:type_name -> gateway.GatewayRequestStatus + 30, // 12: gateway.GatewayResponse.queryError:type_name -> QueryError + 31, // 13: gateway.GatewayResponse.deleteItem:type_name -> Reference + 29, // 14: gateway.GatewayResponse.deleteEdge:type_name -> Edge + 28, // 15: gateway.GatewayResponse.updateItem:type_name -> Item + 8, // 16: gateway.GatewayResponse.snapshotStoreResult:type_name -> gateway.SnapshotStoreResult + 10, // 17: gateway.GatewayResponse.snapshotLoadResult:type_name -> gateway.SnapshotLoadResult + 4, // 18: gateway.GatewayResponse.bookmarkStoreResult:type_name -> gateway.BookmarkStoreResult + 6, // 19: gateway.GatewayResponse.bookmarkLoadResult:type_name -> gateway.BookmarkLoadResult + 32, // 20: gateway.GatewayResponse.queryStatus:type_name -> QueryStatus + 22, // 21: gateway.GatewayResponse.chatResponse:type_name -> gateway.ChatResponse + 20, // 22: gateway.GatewayResponse.toolStart:type_name -> gateway.ToolStart + 21, // 23: gateway.GatewayResponse.toolFinish:type_name -> gateway.ToolFinish + 23, // 24: gateway.GatewayRequestStatus.summary:type_name -> gateway.GatewayRequestStatus.Summary + 33, // 25: gateway.LoadBookmark.deadline:type_name -> google.protobuf.Timestamp + 34, // 26: gateway.QueryToolStart.method:type_name -> QueryMethod + 33, // 27: gateway.ChangeByReferenceSummary.createdAt:type_name -> google.protobuf.Timestamp + 35, // 28: gateway.ChangeByReferenceSummary.changeStatus:type_name -> changes.ChangeStatus + 18, // 29: gateway.ChangesByReferenceToolFinish.changeSummaries:type_name -> gateway.ChangeByReferenceSummary + 12, // 30: gateway.ToolStart.metadata:type_name -> gateway.ToolMetadata + 13, // 31: gateway.ToolStart.query:type_name -> gateway.QueryToolStart + 15, // 32: gateway.ToolStart.relationship:type_name -> gateway.RelationshipToolStart + 17, // 33: gateway.ToolStart.changesByReference:type_name -> gateway.ChangesByReferenceToolStart + 12, // 34: gateway.ToolFinish.metadata:type_name -> gateway.ToolMetadata + 14, // 35: gateway.ToolFinish.query:type_name -> gateway.QueryToolFinish + 16, // 36: gateway.ToolFinish.relationship:type_name -> gateway.RelationshipToolFinish + 19, // 37: gateway.ToolFinish.changesByReference:type_name -> gateway.ChangesByReferenceToolFinish + 38, // [38:38] is the sub-list for method output_type + 38, // [38:38] is the sub-list for method input_type + 38, // [38:38] is the sub-list for extension type_name + 38, // [38:38] is the sub-list for extension extendee + 0, // [0:38] is the sub-list for field type_name +} + +func init() { file_gateway_proto_init() } +func file_gateway_proto_init() { + if File_gateway_proto != nil { + return + } + file_changes_proto_init() + file_items_proto_init() + file_responses_proto_init() + file_gateway_proto_msgTypes[0].OneofWrappers = []any{ + (*GatewayRequest_Query)(nil), + (*GatewayRequest_CancelQuery)(nil), + (*GatewayRequest_Expand)(nil), + (*GatewayRequest_StoreSnapshot)(nil), + (*GatewayRequest_LoadSnapshot)(nil), + (*GatewayRequest_StoreBookmark)(nil), + (*GatewayRequest_LoadBookmark)(nil), + (*GatewayRequest_ChatMessage)(nil), + } + file_gateway_proto_msgTypes[1].OneofWrappers = []any{ + (*GatewayResponse_NewItem)(nil), + (*GatewayResponse_NewEdge)(nil), + (*GatewayResponse_Status)(nil), + (*GatewayResponse_Error)(nil), + (*GatewayResponse_QueryError)(nil), + (*GatewayResponse_DeleteItem)(nil), + (*GatewayResponse_DeleteEdge)(nil), + (*GatewayResponse_UpdateItem)(nil), + (*GatewayResponse_SnapshotStoreResult)(nil), + (*GatewayResponse_SnapshotLoadResult)(nil), + (*GatewayResponse_BookmarkStoreResult)(nil), + (*GatewayResponse_BookmarkLoadResult)(nil), + (*GatewayResponse_QueryStatus)(nil), + (*GatewayResponse_ChatResponse)(nil), + (*GatewayResponse_ToolStart)(nil), + (*GatewayResponse_ToolFinish)(nil), + } + file_gateway_proto_msgTypes[11].OneofWrappers = []any{ + (*ChatMessage_Text)(nil), + (*ChatMessage_Cancel)(nil), + } + file_gateway_proto_msgTypes[20].OneofWrappers = []any{ + (*ToolStart_Query)(nil), + (*ToolStart_Relationship)(nil), + (*ToolStart_ChangesByReference)(nil), + } + file_gateway_proto_msgTypes[21].OneofWrappers = []any{ + (*ToolFinish_Query)(nil), + (*ToolFinish_Relationship)(nil), + (*ToolFinish_ChangesByReference)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gateway_proto_rawDesc), len(file_gateway_proto_rawDesc)), + NumEnums: 0, + NumMessages: 24, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_gateway_proto_goTypes, + DependencyIndexes: file_gateway_proto_depIdxs, + MessageInfos: file_gateway_proto_msgTypes, + }.Build() + File_gateway_proto = out.File + file_gateway_proto_goTypes = nil + file_gateway_proto_depIdxs = nil +} diff --git a/go/sdp-go/gateway_test.go b/go/sdp-go/gateway_test.go new file mode 100644 index 00000000..d82c11c6 --- /dev/null +++ b/go/sdp-go/gateway_test.go @@ -0,0 +1,120 @@ +package sdp + +import "testing" + +func TestEqual(t *testing.T) { + x := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 1, + Stalled: 0, + Complete: 1, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + } + + t.Run("with nil summary", func(t *testing.T) { + y := &GatewayRequestStatus{} + + if x.Equal(y) { + t.Error("expected items to be nonequal") + } + }) + + t.Run("with mismatched summary", func(t *testing.T) { + y := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 1, + Stalled: 0, + Complete: 3, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + } + + if x.Equal(y) { + t.Error("expected items to be nonequal") + } + }) + + t.Run("with different postprocessing states", func(t *testing.T) { + y := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 1, + Stalled: 0, + Complete: 1, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + PostProcessingComplete: true, + } + + if x.Equal(y) { + t.Error("expected items to be different") + } + }) + + t.Run("with same everything", func(t *testing.T) { + y := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 1, + Stalled: 0, + Complete: 1, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + } + + if !x.Equal(y) { + t.Error("expected items to be equal") + } + }) +} + +func TestDone(t *testing.T) { + t.Run("with a request that should be done", func(t *testing.T) { + r := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 0, + Stalled: 1, + Complete: 1, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + PostProcessingComplete: true, + } + + if !r.Done() { + t.Error("expected request .Done() to be true") + } + }) + + t.Run("with a request that shouldn't be done", func(t *testing.T) { + r := &GatewayRequestStatus{ + Summary: &GatewayRequestStatus_Summary{ + Working: 1, + Stalled: 0, + Complete: 1, + Error: 1, + Cancelled: 0, + Responders: 3, + }, + PostProcessingComplete: false, + } + + if r.Done() { + t.Error("expected request .Done() to be false") + } + + r.PostProcessingComplete = true + + if r.Done() { + t.Error("expected request .Done() to be false") + } + }) +} diff --git a/go/sdp-go/genhandler.go b/go/sdp-go/genhandler.go new file mode 100644 index 00000000..24025a74 --- /dev/null +++ b/go/sdp-go/genhandler.go @@ -0,0 +1,108 @@ +//go:build ignore + +package main + +import ( + "fmt" + "html/template" + "os" + "strings" +) + +type Args struct { + Type string +} + +func main() { + fmt.Printf("Running %s go on %s\n", os.Args[0], os.Getenv("GOFILE")) + + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + fmt.Printf(" cwd = %s\n", cwd) + fmt.Printf(" os.Args = %#v\n", os.Args) + + for _, ev := range []string{"GOARCH", "GOOS", "GOFILE", "GOLINE", "GOPACKAGE", "DOLLAR"} { + fmt.Println(" ", ev, "=", os.Getenv(ev)) + } + + if len(os.Args) < 2 { + panic("Missing argument, aborting") + } + + v := Args{Type: os.Args[1]} + t := template.New("simple") + t, err = t.Parse(`// Code generated by "genhandler {{.Type}}"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func New{{.Type}}Handler(spanName string, h func(ctx context.Context, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i {{.Type}} + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRaw{{.Type}}Handler(spanName string, h func(ctx context.Context, m *nats.Msg, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i {{.Type}} + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRaw{{.Type}}Handler(spanName string, h func(ctx context.Context, m *nats.Msg, i *{{.Type}}), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i {{.Type}} + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} +`) + if err != nil { + panic(err) + } + + f, err := os.Create(fmt.Sprintf("handler_%v.go", strings.ToLower(v.Type))) + if err != nil { + panic(err) + } + defer f.Close() + + fmt.Printf("Generating handler for %v\n", v) + err = t.Execute(f, v) + if err != nil { + panic(err) + } +} diff --git a/go/sdp-go/graph/main.go b/go/sdp-go/graph/main.go new file mode 100644 index 00000000..ee6d4838 --- /dev/null +++ b/go/sdp-go/graph/main.go @@ -0,0 +1,362 @@ +// This was written as part of an experiment That required the use of the +// pagerank algorithm on Overmind data. This satisfies the interfaces inside the +// gonum package, which means that we can use any of the code in +// [gonum.org/v1/gonum/graph](https://pkg.go.dev/gonum.org/v1/gonum/graph@v0.15.0) +// to analyse our data. +package graph + +import ( + "github.com/overmindtech/cli/go/sdp-go" + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/set/uid" +) + +/////////// +// Nodes // +/////////// + +var _ graph.Node = &Node{} + +// A node is always an item +type Node struct { + Item *sdp.Item + Weight float64 + Id int64 +} + +// A graph-unique integer ID +func (n *Node) ID() int64 { + return n.Id +} + +var _ graph.Nodes = &Nodes{} + +type Nodes struct { + // The nodes in the iterator + nodes []*Node + + // The current position in the iterator + i int +} + +// Adds a new node to the list +func (n *Nodes) Append(node *Node) { + n.nodes = append(n.nodes, node) +} + +// Next advances the iterator and returns whether the next call to the item +// method will return a non-nil item. +// +// Next should be called prior to any call to the iterator's item retrieval +// method after the iterator has been obtained or reset. +// +// The order of iteration is implementation dependent. +func (n *Nodes) Next() bool { + n.i++ + return n.i-1 < len(n.nodes) +} + +// Len returns the number of items remaining in the iterator. +// +// If the number of items in the iterator is unknown, too large to materialize +// or too costly to calculate then Len may return a negative value. In this case +// the consuming function must be able to operate on the items of the iterator +// directly without materializing the items into a slice. The magnitude of a +// negative length has implementation-dependent semantics. +func (n *Nodes) Len() int { + return len(n.nodes) - n.i +} + +// Reset returns the iterator to its start position. +func (n *Nodes) Reset() { + n.i = 0 +} + +// Node returns the current Node from the iterator. +func (n *Nodes) Node() graph.Node { + // The Next() function gets called *before* the first item is returned, so + // we need to return the item at position i (e.g. 1 is 1st position) rather + // than the actual index i. This allows us to start i at zero which makes a + // lot more sense + getIndex := n.i - 1 + + if getIndex >= len(n.nodes) || getIndex < 0 { + return nil + } + + return n.nodes[getIndex] +} + +/////////// +// Edges // +/////////// + +var _ graph.WeightedEdge = &Edge{} + +type Edge struct { + from *Node + to *Node + weight float64 +} + +// Creates a new edge. The weight of an edge is the sum of the weights of the +// two nodes +func NewEdge(from, to *Node) *Edge { + return &Edge{ + from: from, + to: to, + weight: from.Weight + to.Weight, + } +} + +// From returns the from node of the edge. +func (e *Edge) From() graph.Node { + return e.from +} + +// To returns the to node of the edge. +func (e *Edge) To() graph.Node { + return e.to +} + +// ReversedEdge returns the edge reversal of the receiver if a reversal is valid +// for the data type. When a reversal is valid an edge of the same type as the +// receiver with nodes of the receiver swapped should be returned, otherwise the +// receiver should be returned unaltered. +func (e *Edge) ReversedEdge() graph.Edge { + return nil +} + +func (e *Edge) Weight() float64 { + return e.weight +} + +/////////// +// Graph // +/////////// + +// Assert that SDPGraph satisfies the graph.WeightedDirected interface +var _ graph.WeightedDirected = &SDPGraph{} + +type SDPGraph struct { + uidSet *uid.Set + + nodesByID map[int64]*Node + nodesByGUN map[string]*Node + + // A map of items that have not been seen yet. The key is the GUN of the + // "To" end of the edge, and the value is a slice of nodes that are the + // "From" edges + unseenEdges map[string][]*Node + + edges []*Edge + + undirected bool +} + +// NewSDPGraph creates a new SDPGraph. If undirected is true, the graph will be +// treated as undirected, meaning that all edges will be bidirectional +func NewSDPGraph(undirected bool) *SDPGraph { + return &SDPGraph{ + uidSet: uid.NewSet(), + nodesByID: make(map[int64]*Node), + nodesByGUN: make(map[string]*Node), + unseenEdges: make(map[string][]*Node), + edges: make([]*Edge, 0), + undirected: undirected, + } +} + +// AddItem adds an item to the graph including processing of its edges, returns +// the ID the node was assigned. +func (g *SDPGraph) AddItem(item *sdp.Item, weight float64) int64 { + id := g.uidSet.NewID() + g.uidSet.Use(id) + + // Add the node to the storage + node := Node{ + Item: item, + Weight: weight, + Id: id, + } + g.nodesByID[id] = &node + g.nodesByGUN[item.GloballyUniqueName()] = &node + + // TODO(LIQs): https://github.com/overmindtech/workspace/issues/1228 + // Find all edges and add them + for _, linkedItem := range item.GetLinkedItems() { + // Check if the linked item node exists + linkedItemNode, exists := g.nodesByGUN[linkedItem.GetItem().GloballyUniqueName()] + + if exists { + // Add the edge + g.edges = append(g.edges, NewEdge(&node, linkedItemNode)) + + if g.undirected { + // Also add the reverse edge + g.edges = append(g.edges, NewEdge(linkedItemNode, &node)) + } + } else { + // If the target for the edge doesn't exist, add this to the list to + // be created later + if _, exists := g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()]; !exists { + g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()] = []*Node{&node} + } else { + g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()] = append(g.unseenEdges[linkedItem.GetItem().GloballyUniqueName()], &node) + } + } + } + + // If there are any unseen edges that are now seen, add them + if unseenEdges, exists := g.unseenEdges[item.GloballyUniqueName()]; exists { + for _, unseenEdge := range unseenEdges { + // Add the edge + g.edges = append(g.edges, NewEdge(unseenEdge, &node)) + + if g.undirected { + // Also add the reverse edge + g.edges = append(g.edges, NewEdge(&node, unseenEdge)) + } + } + } + + return id +} + +// HasEdgeFromTo returns whether an edge exists in the graph from u to v with +// the IDs uid and vid. +func (g *SDPGraph) HasEdgeFromTo(uid, vid int64) bool { + for _, edge := range g.edges { + if edge.from.Id == uid && edge.to.Id == vid { + return true + } + } + + return false +} + +// To returns all nodes that can reach directly to the node with the given ID. +// +// To must not return nil. +func (g *SDPGraph) To(id int64) graph.Nodes { + nodes := Nodes{} + + for _, edge := range g.edges { + if edge.to.Id == id { + nodes.Append(edge.to) + } + } + + return &nodes +} + +// WeightedEdge returns the weighted edge from u to v with IDs uid and vid if +// such an edge exists and nil otherwise. The node v must be directly reachable +// from u as defined by the From method. +func (g *SDPGraph) WeightedEdge(uid, vid int64) graph.WeightedEdge { + for _, edge := range g.edges { + if edge.from.Id == uid && edge.to.Id == vid { + return edge + } + } + + return nil +} + +// Weight returns the weight for the edge between x and y with IDs xid and yid +// if Edge(xid, yid) returns a non-nil Edge. If x and y are the same node or +// there is no joining edge between the two nodes the weight value returned is +// implementation dependent. Weight returns true if an edge exists between x and +// y or if x and y have the same ID, false otherwise. +func (g *SDPGraph) Weight(xid, yid int64) (w float64, ok bool) { + edge := g.WeightedEdge(xid, yid) + + if edge == nil { + return 0, false + } + + return edge.Weight(), true +} + +// Node returns the node with the given ID if it exists in the graph, and nil +// otherwise. +func (g *SDPGraph) Node(id int64) graph.Node { + node, exists := g.nodesByID[id] + + if !exists { + return nil + } + + return node +} + +// Gets a node from the graph by it's globally unique name +func (g *SDPGraph) NodeByGloballyUniqueName(globallyUniqueName string) *Node { + node, exists := g.nodesByGUN[globallyUniqueName] + + if !exists { + return nil + } + + return node +} + +// Nodes returns all the nodes in the graph. +// +// Nodes must not return nil. +func (g *SDPGraph) Nodes() graph.Nodes { + nodes := Nodes{} + + for _, node := range g.nodesByID { + nodes.Append(node) + } + + return &nodes +} + +// From returns all nodes that can be reached directly from the node with the +// given ID. +// +// From must not return nil. +func (g *SDPGraph) From(id int64) graph.Nodes { + nodes := Nodes{} + + for _, edge := range g.edges { + if edge.From().ID() == id { + nodes.Append(edge.to) + } + } + + return &nodes +} + +// HasEdgeBetween returns whether an edge exists between nodes with IDs xid and +// yid without considering direction. +func (g *SDPGraph) HasEdgeBetween(xid, yid int64) bool { + var fromID int64 + var toID int64 + + for _, edge := range g.edges { + fromID = edge.From().ID() + toID = edge.To().ID() + + if (fromID == xid && toID == yid) || (fromID == yid && toID == xid) { + return true + } + } + + return false +} + +// Edge returns the edge from u to v, with IDs uid and vid, if such an edge +// exists and nil otherwise. The node v must be directly reachable from u as +// defined by the From method. +func (g *SDPGraph) Edge(uid, vid int64) graph.Edge { + for _, edge := range g.edges { + if (edge.From().ID() == uid) && (edge.To().ID() == vid) { + return edge + } + } + + return nil +} diff --git a/go/sdp-go/graph/main_test.go b/go/sdp-go/graph/main_test.go new file mode 100644 index 00000000..12500713 --- /dev/null +++ b/go/sdp-go/graph/main_test.go @@ -0,0 +1,300 @@ +package graph + +import ( + "testing" + + "github.com/overmindtech/cli/go/sdp-go" + "gonum.org/v1/gonum/graph/network" + "google.golang.org/protobuf/types/known/structpb" +) + +func makeTestItem(name string) *sdp.Item { + return &sdp.Item{ + Type: "test", + UniqueAttribute: "name", + Scope: "test", + Attributes: &sdp.ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "name": { + Kind: &structpb.Value_StringValue{ + StringValue: name, + }, + }, + }, + }, + }, + } +} + +func TestNode(t *testing.T) { + node := Node{ + Item: makeTestItem("test"), + Weight: 1.5, + Id: 1, + } + + if node.ID() != 1 { + t.Errorf("expected ID to be 1, got %v", node.ID()) + } +} + +func TestNodes(t *testing.T) { + nodes := Nodes{} + + nodes.Append(&Node{ + Item: makeTestItem("a"), + Weight: 1.5, + Id: 1, + }) + nodes.Append(&Node{ + Item: makeTestItem("b"), + Weight: 1.5, + Id: 2, + }) + + if nodes.Len() != 2 { + t.Errorf("expected length to be 2, got %v", nodes.Len()) + } + + // Call node before next should return nil + if nodes.Node() != nil { + t.Errorf("expected Node to be nil") + } + + // Call next + if nodes.Next() != true { + t.Errorf("expected Next to be true") + } + + // A + if nodes.Node().ID() != 1 { + t.Errorf("expected ID to be 1, got %v", nodes.Node().ID()) + } + + if nodes.Len() != 1 { + t.Errorf("expected length to be 1, got %v", nodes.Len()) + } + + if nodes.Next() != true { + t.Errorf("expected Next to be true") + } + + // B + if nodes.Node().ID() != 2 { + t.Errorf("expected ID to be 2, got %v", nodes.Node().ID()) + } + + if nodes.Len() != 0 { + t.Errorf("expected length to be 0, got %v", nodes.Len()) + } + + if nodes.Next() != false { + t.Errorf("expected Next to be false") + } + + if nodes.Node() != nil { + t.Errorf("expected Node to be nil") + + } + + nodes.Reset() + + if nodes.Len() != 2 { + t.Errorf("expected length to be 2, got %v", nodes.Len()) + } +} + +func TestGraph(t *testing.T) { + // A list of items that form the following graph: + // + // ┌────┐ + // ┌──┤ A ├──┐ + // │ └────┘ │ + // │ │ + // ┌──▼───┐ ┌──▼─┐ + // │ B ├──►│ C │ + // └──┬───┘ └────┘ + // │ + // │ + // ┌──▼───┐ + // │ D │ + // └──────┘ + // + a := makeTestItem("a") + b := makeTestItem("b") + c := makeTestItem("c") + d := makeTestItem("d") + + // TODO(LIQs): https://github.com/overmindtech/workspace/issues/1228 + a.LinkedItems = []*sdp.LinkedItem{ + { + Item: b.Reference(), + }, + { + Item: c.Reference(), + }, + } + + b.LinkedItems = []*sdp.LinkedItem{ + { + Item: c.Reference(), + }, + { + Item: d.Reference(), + }, + } + + graph := NewSDPGraph(false) + + aID := graph.AddItem(a, 1) + bID := graph.AddItem(b, 1) + cID := graph.AddItem(c, 1) + dID := graph.AddItem(d, 1) + + t.Run("To", func(t *testing.T) { + nodes := graph.To(cID) + + if nodes.Len() != 2 { + t.Errorf("expected length to be 2, got %v", nodes.Len()) + } + }) + + t.Run("WeightedEdge", func(t *testing.T) { + t.Run("with a real edge", func(t *testing.T) { + e := graph.WeightedEdge(aID, cID) + + if e == nil { + t.Fatal("expected edge to be non-nil") + } + + if e.Weight() != 2 { + t.Errorf("expected weight to be 2, got %v", e.Weight()) + } + }) + + t.Run("with a non-existent edge", func(t *testing.T) { + e := graph.WeightedEdge(aID, dID) + + if e != nil { + t.Errorf("expected edge to be nil") + } + }) + }) + + t.Run("Weight", func(t *testing.T) { + t.Run("with a real edge", func(t *testing.T) { + w, ok := graph.Weight(aID, cID) + + if !ok { + t.Fatal("expected edge to be non-nil") + } + + if w != 2 { + t.Errorf("expected weight to be 2, got %v", w) + } + }) + + t.Run("with a non-existent edge", func(t *testing.T) { + w, ok := graph.Weight(aID, dID) + + if ok { + t.Errorf("expected edge to be nil") + } + + if w != 0 { + t.Errorf("expected weight to be 0, got %v", w) + } + }) + }) + + t.Run("Node", func(t *testing.T) { + t.Run("with a node that exists", func(t *testing.T) { + n := graph.Node(aID) + + if n == nil { + t.Fatal("expected node to be non-nil") + } + + if n.ID() != aID { + t.Errorf("expected ID to be %v, got %v", aID, n.ID()) + } + }) + + t.Run("with a node that doesn't exist", func(t *testing.T) { + n := graph.Node(999) + + if n != nil { + t.Errorf("expected node to be nil") + } + }) + }) + + t.Run("Nodes", func(t *testing.T) { + nodes := graph.Nodes() + + if nodes.Len() != 4 { + t.Errorf("expected length to be 4, got %v", nodes.Len()) + } + }) + + t.Run("From", func(t *testing.T) { + nodes := graph.From(bID) + + if nodes.Len() != 2 { + t.Errorf("expected length to be 2, got %v", nodes.Len()) + } + }) + + t.Run("HasEdgeBetween", func(t *testing.T) { + t.Run("with a real edge", func(t *testing.T) { + ok := graph.HasEdgeBetween(aID, cID) + + if !ok { + t.Fatal("expected edge to be non-nil") + } + }) + + t.Run("with a non-existent edge", func(t *testing.T) { + ok := graph.HasEdgeBetween(aID, dID) + + if ok { + t.Errorf("expected edge to be nil") + } + }) + }) + + t.Run("Edge", func(t *testing.T) { + e := graph.Edge(aID, cID) + + if e == nil { + t.Fatal("expected edge to be non-nil") + } + }) + + t.Run("PageRank", func(t *testing.T) { + ranks := network.PageRank(graph, 0.85, 0.0001) + + if len(ranks) != 4 { + t.Errorf("expected length to be 4, got %v", len(ranks)) + } + }) + + t.Run("Undirected", func(t *testing.T) { + directed := NewSDPGraph(false) + undirected := NewSDPGraph(true) + + directed.AddItem(a, 1) + directed.AddItem(b, 1) + directed.AddItem(c, 1) + directed.AddItem(d, 1) + undirected.AddItem(a, 1) + undirected.AddItem(b, 1) + undirected.AddItem(c, 1) + undirected.AddItem(d, 1) + + if len(undirected.edges) == 4 { + t.Errorf("expected undirected graph to have > 4 edges, got %v", len(undirected.edges)) + } + }) +} diff --git a/go/sdp-go/handler_cancelquery.go b/go/sdp-go/handler_cancelquery.go new file mode 100644 index 00000000..4a3631fd --- /dev/null +++ b/go/sdp-go/handler_cancelquery.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler CancelQuery"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewCancelQueryHandler(spanName string, h func(ctx context.Context, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i CancelQuery + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawCancelQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i CancelQuery + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawCancelQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *CancelQuery), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i CancelQuery + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/handler_gatewayresponse.go b/go/sdp-go/handler_gatewayresponse.go new file mode 100644 index 00000000..a421a06e --- /dev/null +++ b/go/sdp-go/handler_gatewayresponse.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler GatewayResponse"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewGatewayResponseHandler(spanName string, h func(ctx context.Context, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i GatewayResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawGatewayResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i GatewayResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawGatewayResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *GatewayResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i GatewayResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/handler_natsgetlogrecordsrequest.go b/go/sdp-go/handler_natsgetlogrecordsrequest.go new file mode 100644 index 00000000..97628de6 --- /dev/null +++ b/go/sdp-go/handler_natsgetlogrecordsrequest.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler NATSGetLogRecordsRequest"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsRequest + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsRequest + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawNATSGetLogRecordsRequestHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsRequest), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsRequest + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/handler_natsgetlogrecordsresponse.go b/go/sdp-go/handler_natsgetlogrecordsresponse.go new file mode 100644 index 00000000..c2f24d4d --- /dev/null +++ b/go/sdp-go/handler_natsgetlogrecordsresponse.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler NATSGetLogRecordsResponse"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawNATSGetLogRecordsResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *NATSGetLogRecordsResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i NATSGetLogRecordsResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/handler_query.go b/go/sdp-go/handler_query.go new file mode 100644 index 00000000..44c78b02 --- /dev/null +++ b/go/sdp-go/handler_query.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler Query"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewQueryHandler(spanName string, h func(ctx context.Context, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i Query + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i Query + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawQueryHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *Query), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i Query + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/handler_queryresponse.go b/go/sdp-go/handler_queryresponse.go new file mode 100644 index 00000000..f629818e --- /dev/null +++ b/go/sdp-go/handler_queryresponse.go @@ -0,0 +1,56 @@ +// Code generated by "genhandler QueryResponse"; DO NOT EDIT + +package sdp + +import ( + "context" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel/trace" + "github.com/overmindtech/cli/go/tracing" +) + +func NewQueryResponseHandler(spanName string, h func(ctx context.Context, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i QueryResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, &i) + }, + tracing.Tracer(), + ) +} + +func NewRawQueryResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i QueryResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} + +func NewAsyncRawQueryResponseHandler(spanName string, h func(ctx context.Context, m *nats.Msg, i *QueryResponse), spanOpts ...trace.SpanStartOption) nats.MsgHandler { + return NewAsyncOtelExtractingHandler( + spanName, + func(ctx context.Context, m *nats.Msg) { + var i QueryResponse + err := Unmarshal(ctx, m.Data, &i) + if err != nil { + return + } + h(ctx, m, &i) + }, + tracing.Tracer(), + ) +} diff --git a/go/sdp-go/instance_detect.go b/go/sdp-go/instance_detect.go new file mode 100644 index 00000000..2da3ac9e --- /dev/null +++ b/go/sdp-go/instance_detect.go @@ -0,0 +1,91 @@ +package sdp + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/overmindtech/cli/go/tracing" +) + +// Information about a particular instance of Overmind. This is used to +// determine where to send requests, how to authenticate etc. +type OvermindInstance struct { + FrontendUrl *url.URL + ApiUrl *url.URL + NatsUrl *url.URL + Audience string + Auth0Domain string + CLIClientID string +} + +// GatewayUrl returns the URL for the gateway for this instance. +func (oi OvermindInstance) GatewayUrl() string { + return fmt.Sprintf("%v/api/gateway", oi.ApiUrl.String()) +} + +func (oi OvermindInstance) String() string { + return fmt.Sprintf("Frontend: %v, API: %v, Nats: %v, Audience: %v", oi.FrontendUrl, oi.ApiUrl, oi.NatsUrl, oi.Audience) +} + +type instanceData struct { + Api string `json:"api_url"` + Nats string `json:"nats_url"` + Aud string `json:"aud"` + Auth0Domain string `json:"auth0_domain"` + CLIClientID string `json:"auth0_cli_client_id"` +} + +// NewOvermindInstance creates a new OvermindInstance from the given app URL +// with all URLs filled in, or an error. The app URL should be the URL of the +// frontend of the Overmind instance. e.g. https://app.overmind.tech +func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, error) { + var instance OvermindInstance + var err error + + instance.FrontendUrl, err = url.Parse(app) + if err != nil { + return instance, fmt.Errorf("invalid app value '%v', error: %w", app, err) + } + + // Get the instance data + instanceDataUrl := fmt.Sprintf("%v/api/public/instance-data", instance.FrontendUrl) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, instanceDataUrl, nil) + if err != nil { + return OvermindInstance{}, fmt.Errorf("could not initialize instance-data fetch: %w", err) + } + + req = req.WithContext(ctx) + res, err := tracing.HTTPClient().Do(req) + if err != nil { + return OvermindInstance{}, fmt.Errorf("could not fetch instance-data: %w", err) + } + + if res.StatusCode != http.StatusOK { + return OvermindInstance{}, fmt.Errorf("instance-data fetch returned non-200 status: %v", res.StatusCode) + } + + defer res.Body.Close() + data := instanceData{} + err = json.NewDecoder(res.Body).Decode(&data) + if err != nil { + return OvermindInstance{}, fmt.Errorf("could not parse instance-data: %w", err) + } + + instance.ApiUrl, err = url.Parse(data.Api) + if err != nil { + return OvermindInstance{}, fmt.Errorf("invalid api_url value '%v' in instance-data, error: %w", data.Api, err) + } + instance.NatsUrl, err = url.Parse(data.Nats) + if err != nil { + return OvermindInstance{}, fmt.Errorf("invalid nats_url value '%v' in instance-data, error: %w", data.Nats, err) + } + + instance.Audience = data.Aud + instance.CLIClientID = data.CLIClientID + instance.Auth0Domain = data.Auth0Domain + + return instance, nil +} diff --git a/go/sdp-go/invites.pb.go b/go/sdp-go/invites.pb.go new file mode 100644 index 00000000..c7b63a1e --- /dev/null +++ b/go/sdp-go/invites.pb.go @@ -0,0 +1,546 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: invites.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/structpb" + _ "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Invite_InviteStatus int32 + +const ( + Invite_INVITE_STATUS_UNSPECIFIED Invite_InviteStatus = 0 + // The user has been invited but has not yet accepted + Invite_INVITE_STATUS_INVITED Invite_InviteStatus = 1 + // The user has accepted the invitation + Invite_INVITE_STATUS_ACCEPTED Invite_InviteStatus = 2 +) + +// Enum value maps for Invite_InviteStatus. +var ( + Invite_InviteStatus_name = map[int32]string{ + 0: "INVITE_STATUS_UNSPECIFIED", + 1: "INVITE_STATUS_INVITED", + 2: "INVITE_STATUS_ACCEPTED", + } + Invite_InviteStatus_value = map[string]int32{ + "INVITE_STATUS_UNSPECIFIED": 0, + "INVITE_STATUS_INVITED": 1, + "INVITE_STATUS_ACCEPTED": 2, + } +) + +func (x Invite_InviteStatus) Enum() *Invite_InviteStatus { + p := new(Invite_InviteStatus) + *p = x + return p +} + +func (x Invite_InviteStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Invite_InviteStatus) Descriptor() protoreflect.EnumDescriptor { + return file_invites_proto_enumTypes[0].Descriptor() +} + +func (Invite_InviteStatus) Type() protoreflect.EnumType { + return &file_invites_proto_enumTypes[0] +} + +func (x Invite_InviteStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Invite_InviteStatus.Descriptor instead. +func (Invite_InviteStatus) EnumDescriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{2, 0} +} + +type CreateInviteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Emails []string `protobuf:"bytes,1,rep,name=emails,proto3" json:"emails,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateInviteRequest) Reset() { + *x = CreateInviteRequest{} + mi := &file_invites_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateInviteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateInviteRequest) ProtoMessage() {} + +func (x *CreateInviteRequest) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[0] + 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 CreateInviteRequest.ProtoReflect.Descriptor instead. +func (*CreateInviteRequest) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateInviteRequest) GetEmails() []string { + if x != nil { + return x.Emails + } + return nil +} + +type CreateInviteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateInviteResponse) Reset() { + *x = CreateInviteResponse{} + mi := &file_invites_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateInviteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateInviteResponse) ProtoMessage() {} + +func (x *CreateInviteResponse) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[1] + 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 CreateInviteResponse.ProtoReflect.Descriptor instead. +func (*CreateInviteResponse) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{1} +} + +type Invite struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Status Invite_InviteStatus `protobuf:"varint,2,opt,name=status,proto3,enum=invites.Invite_InviteStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Invite) Reset() { + *x = Invite{} + mi := &file_invites_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Invite) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Invite) ProtoMessage() {} + +func (x *Invite) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[2] + 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 Invite.ProtoReflect.Descriptor instead. +func (*Invite) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{2} +} + +func (x *Invite) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *Invite) GetStatus() Invite_InviteStatus { + if x != nil { + return x.Status + } + return Invite_INVITE_STATUS_UNSPECIFIED +} + +type ListInvitesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInvitesRequest) Reset() { + *x = ListInvitesRequest{} + mi := &file_invites_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInvitesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInvitesRequest) ProtoMessage() {} + +func (x *ListInvitesRequest) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[3] + 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 ListInvitesRequest.ProtoReflect.Descriptor instead. +func (*ListInvitesRequest) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{3} +} + +type ListInvitesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Invites []*Invite `protobuf:"bytes,1,rep,name=invites,proto3" json:"invites,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInvitesResponse) Reset() { + *x = ListInvitesResponse{} + mi := &file_invites_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInvitesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInvitesResponse) ProtoMessage() {} + +func (x *ListInvitesResponse) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[4] + 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 ListInvitesResponse.ProtoReflect.Descriptor instead. +func (*ListInvitesResponse) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{4} +} + +func (x *ListInvitesResponse) GetInvites() []*Invite { + if x != nil { + return x.Invites + } + return nil +} + +type RevokeInviteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeInviteRequest) Reset() { + *x = RevokeInviteRequest{} + mi := &file_invites_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeInviteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeInviteRequest) ProtoMessage() {} + +func (x *RevokeInviteRequest) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[5] + 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 RevokeInviteRequest.ProtoReflect.Descriptor instead. +func (*RevokeInviteRequest) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{5} +} + +func (x *RevokeInviteRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type RevokeInviteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeInviteResponse) Reset() { + *x = RevokeInviteResponse{} + mi := &file_invites_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeInviteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeInviteResponse) ProtoMessage() {} + +func (x *RevokeInviteResponse) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[6] + 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 RevokeInviteResponse.ProtoReflect.Descriptor instead. +func (*RevokeInviteResponse) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{6} +} + +type ResendInviteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResendInviteRequest) Reset() { + *x = ResendInviteRequest{} + mi := &file_invites_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResendInviteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResendInviteRequest) ProtoMessage() {} + +func (x *ResendInviteRequest) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[7] + 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 ResendInviteRequest.ProtoReflect.Descriptor instead. +func (*ResendInviteRequest) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{7} +} + +func (x *ResendInviteRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type ResendInviteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResendInviteResponse) Reset() { + *x = ResendInviteResponse{} + mi := &file_invites_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResendInviteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResendInviteResponse) ProtoMessage() {} + +func (x *ResendInviteResponse) ProtoReflect() protoreflect.Message { + mi := &file_invites_proto_msgTypes[8] + 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 ResendInviteResponse.ProtoReflect.Descriptor instead. +func (*ResendInviteResponse) Descriptor() ([]byte, []int) { + return file_invites_proto_rawDescGZIP(), []int{8} +} + +var File_invites_proto protoreflect.FileDescriptor + +const file_invites_proto_rawDesc = "" + + "\n" + + "\rinvites.proto\x12\ainvites\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"-\n" + + "\x13CreateInviteRequest\x12\x16\n" + + "\x06emails\x18\x01 \x03(\tR\x06emails\"\x16\n" + + "\x14CreateInviteResponse\"\xba\x01\n" + + "\x06Invite\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\x124\n" + + "\x06status\x18\x02 \x01(\x0e2\x1c.invites.Invite.InviteStatusR\x06status\"d\n" + + "\fInviteStatus\x12\x1d\n" + + "\x19INVITE_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15INVITE_STATUS_INVITED\x10\x01\x12\x1a\n" + + "\x16INVITE_STATUS_ACCEPTED\x10\x02\"\x14\n" + + "\x12ListInvitesRequest\"@\n" + + "\x13ListInvitesResponse\x12)\n" + + "\ainvites\x18\x01 \x03(\v2\x0f.invites.InviteR\ainvites\"+\n" + + "\x13RevokeInviteRequest\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"\x16\n" + + "\x14RevokeInviteResponse\"+\n" + + "\x13ResendInviteRequest\x12\x14\n" + + "\x05email\x18\x01 \x01(\tR\x05email\"\x16\n" + + "\x14ResendInviteResponse2\xc0\x02\n" + + "\rInviteService\x12K\n" + + "\fCreateInvite\x12\x1c.invites.CreateInviteRequest\x1a\x1d.invites.CreateInviteResponse\x12H\n" + + "\vListInvites\x12\x1b.invites.ListInvitesRequest\x1a\x1c.invites.ListInvitesResponse\x12K\n" + + "\fRevokeInvite\x12\x1c.invites.RevokeInviteRequest\x1a\x1d.invites.RevokeInviteResponse\x12K\n" + + "\fResendInvite\x12\x1c.invites.ResendInviteRequest\x1a\x1d.invites.ResendInviteResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_invites_proto_rawDescOnce sync.Once + file_invites_proto_rawDescData []byte +) + +func file_invites_proto_rawDescGZIP() []byte { + file_invites_proto_rawDescOnce.Do(func() { + file_invites_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_invites_proto_rawDesc), len(file_invites_proto_rawDesc))) + }) + return file_invites_proto_rawDescData +} + +var file_invites_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_invites_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_invites_proto_goTypes = []any{ + (Invite_InviteStatus)(0), // 0: invites.Invite.InviteStatus + (*CreateInviteRequest)(nil), // 1: invites.CreateInviteRequest + (*CreateInviteResponse)(nil), // 2: invites.CreateInviteResponse + (*Invite)(nil), // 3: invites.Invite + (*ListInvitesRequest)(nil), // 4: invites.ListInvitesRequest + (*ListInvitesResponse)(nil), // 5: invites.ListInvitesResponse + (*RevokeInviteRequest)(nil), // 6: invites.RevokeInviteRequest + (*RevokeInviteResponse)(nil), // 7: invites.RevokeInviteResponse + (*ResendInviteRequest)(nil), // 8: invites.ResendInviteRequest + (*ResendInviteResponse)(nil), // 9: invites.ResendInviteResponse +} +var file_invites_proto_depIdxs = []int32{ + 0, // 0: invites.Invite.status:type_name -> invites.Invite.InviteStatus + 3, // 1: invites.ListInvitesResponse.invites:type_name -> invites.Invite + 1, // 2: invites.InviteService.CreateInvite:input_type -> invites.CreateInviteRequest + 4, // 3: invites.InviteService.ListInvites:input_type -> invites.ListInvitesRequest + 6, // 4: invites.InviteService.RevokeInvite:input_type -> invites.RevokeInviteRequest + 8, // 5: invites.InviteService.ResendInvite:input_type -> invites.ResendInviteRequest + 2, // 6: invites.InviteService.CreateInvite:output_type -> invites.CreateInviteResponse + 5, // 7: invites.InviteService.ListInvites:output_type -> invites.ListInvitesResponse + 7, // 8: invites.InviteService.RevokeInvite:output_type -> invites.RevokeInviteResponse + 9, // 9: invites.InviteService.ResendInvite:output_type -> invites.ResendInviteResponse + 6, // [6:10] is the sub-list for method output_type + 2, // [2:6] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_invites_proto_init() } +func file_invites_proto_init() { + if File_invites_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_invites_proto_rawDesc), len(file_invites_proto_rawDesc)), + NumEnums: 1, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_invites_proto_goTypes, + DependencyIndexes: file_invites_proto_depIdxs, + EnumInfos: file_invites_proto_enumTypes, + MessageInfos: file_invites_proto_msgTypes, + }.Build() + File_invites_proto = out.File + file_invites_proto_goTypes = nil + file_invites_proto_depIdxs = nil +} diff --git a/go/sdp-go/items.go b/go/sdp-go/items.go new file mode 100644 index 00000000..0f106653 --- /dev/null +++ b/go/sdp-go/items.go @@ -0,0 +1,679 @@ +package sdp + +import ( + "context" + "crypto/sha256" + "encoding/base32" + "encoding/json" + "errors" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/structpb" +) + +const WILDCARD = "*" + +// IsEqual compares two BlastPropagation settings for equality by checking +// both the In and Out propagation directions. +func (bp *BlastPropagation) IsEqual(other *BlastPropagation) bool { + return bp.GetIn() == other.GetIn() && bp.GetOut() == other.GetOut() +} + +// UniqueAttributeValue returns the value of whatever the Unique Attribute is +// for this item. This will then be converted to a string and returned +func (i *Item) UniqueAttributeValue() string { + var value interface{} + var err error + + value, err = i.GetAttributes().Get(i.GetUniqueAttribute()) + + if err == nil { + return fmt.Sprint(value) + } + + return "" +} + +// Reference returns an SDP reference for the item +func (i *Item) Reference() *Reference { + return &Reference{ + Scope: i.GetScope(), + Type: i.GetType(), + UniqueAttributeValue: i.UniqueAttributeValue(), + } +} + +// GloballyUniqueName Returns a string that defines the Item globally. This a +// combination of the following values: +// +// - scope +// - type +// - uniqueAttributeValue +// +// They are concatenated with dots (.) +func (i *Item) GloballyUniqueName() string { + return strings.Join([]string{ + i.GetScope(), + i.GetType(), + i.UniqueAttributeValue(), + }, + ".", + ) +} + +// Hash Returns a 12 character hash for the item. This is likely but not +// guaranteed to be unique. The hash is calculated using the GloballyUniqueName +func (i *Item) Hash() string { + return HashSum(([]byte(fmt.Sprint(i.GloballyUniqueName())))) +} + +// IsEqual compares two Edges for equality by checking the From reference, +// To reference, and BlastPropagation settings. +func (e *Edge) IsEqual(other *Edge) bool { + return e.GetFrom().IsEqual(other.GetFrom()) && + e.GetTo().IsEqual(other.GetTo()) && + e.GetBlastPropagation().IsEqual(other.GetBlastPropagation()) +} + +// Hash Returns a 12 character hash for the item. This is likely but not +// guaranteed to be unique. The hash is calculated using the GloballyUniqueName +func (r *Reference) Hash() string { + return HashSum(([]byte(fmt.Sprint(r.GloballyUniqueName())))) +} + +// GloballyUniqueName Returns a string that defines the Item globally. This a +// combination of the following values: +// +// - scope +// - type +// - uniqueAttributeValue +// +// They are concatenated with dots (.) +func (r *Reference) GloballyUniqueName() string { + if r == nil { + // in the llm templates nil references are processed, and after spending + // half an hour on trying to figure out what was happening in the + // reflect code, I decided to just return an empty string here. DS, + // 2025-02-26 + return "" + } + if r.GetIsQuery() { + if r.GetMethod() == QueryMethod_GET { + // GET queries are single items + return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetQuery()) + } + panic(fmt.Sprintf("cannot get globally unique name for query reference: %v", r)) + } + return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetUniqueAttributeValue()) +} + +// Key returns a globally unique string for this reference, even if it is a GET query +func (r *Reference) Key() string { + if r == nil { + panic("cannot get key for nil reference") + } + if r.GetIsQuery() { + if r.IsSingle() { + // GET queries without wildcards are single items + return fmt.Sprintf("%v.%v.%v", r.GetScope(), r.GetType(), r.GetQuery()) + } + return fmt.Sprintf("%v: %v.%v.%v", r.GetMethod(), r.GetScope(), r.GetType(), r.GetQuery()) + } + return r.GloballyUniqueName() +} + +// IsSingle returns true if this references a single item, false if it is a LIST +// or SEARCH query, or a GET query with scope and/or type wildcards. +func (r *Reference) IsSingle() bool { + // nil reference is never good + if r == nil { + return false + } + // if it is a query, then it is only a single item if it is a GET query with no wildcards + if r.GetIsQuery() { + return r.GetMethod() == QueryMethod_GET && r.GetScope() != "*" && r.GetType() != "*" + } + // if it is not a query, then it is always single item + return true +} + +// IsEqual compares two References for equality by checking all fields: +// Scope, Type, UniqueAttributeValue, IsQuery, Method, and Query. +func (r *Reference) IsEqual(other *Reference) bool { + return r.GetScope() == other.GetScope() && + r.GetType() == other.GetType() && + r.GetUniqueAttributeValue() == other.GetUniqueAttributeValue() && + r.GetIsQuery() == other.GetIsQuery() && + r.GetMethod() == other.GetMethod() && + r.GetQuery() == other.GetQuery() +} + +// ToQuery converts a Reference to a Query object. If the Reference is not +// already a query (IsQuery=false), it creates a GET query using the +// UniqueAttributeValue. Otherwise, it preserves the existing query parameters. +func (r *Reference) ToQuery() *Query { + if !r.GetIsQuery() { + return &Query{ + Scope: r.GetScope(), + Type: r.GetType(), + Method: QueryMethod_GET, + Query: r.GetUniqueAttributeValue(), + } + } + + return &Query{ + Scope: r.GetScope(), + Type: r.GetType(), + Method: r.GetMethod(), + Query: r.GetQuery(), + } +} + +// Get Returns the value of a given attribute by name. If the attribute is +// a nested hash, nested values can be referenced using dot notation e.g. +// location.country +func (a *ItemAttributes) Get(name string) (interface{}, error) { + var result interface{} + + // Start at the beginning of the map, we will then traverse down as required + result = a.GetAttrStruct().AsMap() + + for _, section := range strings.Split(name, ".") { + // Check that the data we're using is in the supported format + var m map[string]interface{} + + m, isMap := result.(map[string]interface{}) + + if !isMap { + return nil, fmt.Errorf("attribute %v not found", name) + } + + v, keyExists := m[section] + + if keyExists { + result = v + } else { + return nil, fmt.Errorf("attribute %v not found", name) + } + } + + return result, nil +} + +// Set sets an attribute. Values are converted to structpb versions and an error +// will be returned if this fails. Note that this does *not* yet support +// dot notation e.g. location.country +func (a *ItemAttributes) Set(name string, value interface{}) error { + // Check to make sure that the pointer is not nil + if a == nil { + return errors.New("Set called on nil pointer") + } + + // Ensure that this interface will be able to be converted to a struct value + sanitizedValue := sanitizeInterface(value, false, DefaultTransforms) + structValue, err := structpb.NewValue(sanitizedValue) + if err != nil { + return err + } + + fields := a.GetAttrStruct().GetFields() + + fields[name] = structValue + + return nil +} + +// IsSingle returns true if this query can only return a single item. +func (q *Query) IsSingle() bool { + return q.GetMethod() == QueryMethod_GET && q.GetScope() != "*" && q.GetType() != "*" +} + +// Reference returns an SDP reference equivalent to this Query +func (q *Query) Reference() *Reference { + if q.IsSingle() { + return &Reference{ + Scope: q.GetScope(), + Type: q.GetType(), + UniqueAttributeValue: q.GetQuery(), + } + } + return &Reference{ + Scope: q.GetScope(), + Type: q.GetType(), + IsQuery: true, + Query: q.GetQuery(), + Method: q.GetMethod(), + } +} + +// Subject returns a NATS subject for all traffic relating to this query +func (q *Query) Subject() string { + return fmt.Sprintf("query.%v", q.GetUUIDParsed()) +} + +// TimeoutContext returns a context and cancel function representing the timeout +// for this request +func (q *Query) TimeoutContext(ctx context.Context) (context.Context, context.CancelFunc) { + // If there is no deadline, treat that as infinite + if q == nil || !q.GetDeadline().IsValid() { + return context.WithCancel(ctx) + } + + return context.WithDeadline(ctx, q.GetDeadline().AsTime()) +} + +// GetUUIDParsed returns this request's UUID. If there's an error parsing it, +// generates and stores a fresh one +func (r *Query) GetUUIDParsed() uuid.UUID { + if r == nil { + return uuid.UUID{} + } + // Extract and parse the UUID + reqUUID, uuidErr := uuid.FromBytes(r.GetUUID()) + if uuidErr != nil { + reqUUID = uuid.New() + r.UUID = reqUUID[:] + } + return reqUUID +} + +// SetSpanAttributes sets OpenTelemetry span attributes for the query, +// including method, type, scope, query string, UUID, deadline, and cache settings. +// All attributes are prefixed with "ovm.sdp." for namespacing. +func (q *Query) SetSpanAttributes(span trace.Span) { + span.SetAttributes( + attribute.String("ovm.sdp.method", q.GetMethod().String()), + 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()), + ) +} + +// NewQueryResponseFromItem creates a QueryResponse wrapping a discovered Item. +func NewQueryResponseFromItem(item *Item) *QueryResponse { + return &QueryResponse{ + ResponseType: &QueryResponse_NewItem{ + NewItem: item, + }, + } +} + +// NewQueryResponseFromEdge creates a QueryResponse wrapping a discovered Edge. +func NewQueryResponseFromEdge(edge *Edge) *QueryResponse { + return &QueryResponse{ + ResponseType: &QueryResponse_Edge{ + Edge: edge, + }, + } +} + +// NewQueryResponseFromError creates a QueryResponse wrapping a QueryError. +func NewQueryResponseFromError(qe *QueryError) *QueryResponse { + return &QueryResponse{ + ResponseType: &QueryResponse_Error{ + Error: qe, + }, + } +} + +// NewQueryResponseFromResponse creates a QueryResponse wrapping a Response status update. +func NewQueryResponseFromResponse(r *Response) *QueryResponse { + return &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: r, + }, + } +} + +// ToGatewayResponse converts a QueryResponse to a GatewayResponse for sending +// to clients. Handles Item, Edge, Error, and Response status types. +func (qr *QueryResponse) ToGatewayResponse() *GatewayResponse { + switch qr.GetResponseType().(type) { + case *QueryResponse_NewItem: + return &GatewayResponse{ + ResponseType: &GatewayResponse_NewItem{ + NewItem: qr.GetNewItem(), + }, + } + case *QueryResponse_Edge: + return &GatewayResponse{ + ResponseType: &GatewayResponse_NewEdge{ + NewEdge: qr.GetEdge(), + }, + } + case *QueryResponse_Error: + return &GatewayResponse{ + ResponseType: &GatewayResponse_QueryError{ + QueryError: qr.GetError(), + }, + } + case *QueryResponse_Response: + return &GatewayResponse{ + ResponseType: &GatewayResponse_QueryStatus{ + QueryStatus: qr.GetResponse().ToQueryStatus(), + }, + } + default: + panic(fmt.Sprintf("encountered unknown QueryResponse type: %T", qr)) + } +} + +// GetUUIDParsed returns the parsed UUID from the CancelQuery, or nil if invalid. +func (x *CancelQuery) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// GetUUIDParsed returns the parsed UUID from the Expand request, or nil if invalid. +func (x *Expand) GetUUIDParsed() *uuid.UUID { + u, err := uuid.FromBytes(x.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// AddDefaultTransforms adds the default transforms to a TransformMap +func AddDefaultTransforms(customTransforms TransformMap) TransformMap { + for k, v := range DefaultTransforms { + if _, ok := customTransforms[k]; !ok { + customTransforms[k] = v + } + } + return customTransforms +} + +// Converts to attributes using an additional set of custom transformers. These +// can be used to change the transform behaviour of known types to do things +// like redaction of sensitive data or simplification of complex types. +// +// For example this could be used to completely remove anything of type +// `Secret`: +// +// ```go +// +// TransformMap{ +// reflect.TypeOf(Secret{}): func(i interface{}) interface{} { +// // Remove it +// return "REDACTED" +// }, +// } +// +// ``` +// +// Note that you need to use `AddDefaultTransforms(TransformMap) TransformMap` +// to get sensible default transformations. +func ToAttributesCustom(m map[string]interface{}, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { + return toAttributes(m, sort, customTransforms) +} + +// Converts a map[string]interface{} to an ItemAttributes object, sorting all +// slices alphabetically.This should be used when the item doesn't contain array +// attributes that are explicitly sorted, especially if these are sometimes +// returned in a different order +func ToAttributesSorted(m map[string]interface{}) (*ItemAttributes, error) { + return toAttributes(m, true, DefaultTransforms) +} + +// ToAttributes Converts a map[string]interface{} to an ItemAttributes object +func ToAttributes(m map[string]interface{}) (*ItemAttributes, error) { + return toAttributes(m, false, DefaultTransforms) +} + +func toAttributes(m map[string]interface{}, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { + if m == nil { + return nil, nil + } + + var s map[string]*structpb.Value + var err error + + s = make(map[string]*structpb.Value) + + // Loop over the map + for k, v := range m { + sanitizedValue := sanitizeInterface(v, sort, customTransforms) + structValue, err := structpb.NewValue(sanitizedValue) + if err != nil { + return nil, err + } + + s[k] = structValue + } + + return &ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: s, + }, + }, err +} + +// ToAttributesViaJson Converts any struct to a set of attributes by marshalling +// to JSON and then back again. This is less performant than ToAttributes() but +// does save work when copying large structs to attributes in their entirety +func ToAttributesViaJson(v interface{}) (*ItemAttributes, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + + var m map[string]interface{} + + err = json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return ToAttributes(m) +} + +// A function that transforms one data type into another that is compatible with +// protobuf. This is used to convert things like time.Time into a string +type TransformFunc func(interface{}) interface{} + +// A map of types to transform functions +type TransformMap map[reflect.Type]TransformFunc + +// The default transforms that are used when converting to attributes +var DefaultTransforms = TransformMap{ + // Time should be in RFC3339Nano format i.e. 2006-01-02T15:04:05.999999999Z07:00 + reflect.TypeOf(time.Time{}): func(i interface{}) interface{} { + return i.(time.Time).Format(time.RFC3339Nano) + }, + // Duration should be in string format + reflect.TypeOf(time.Duration(0)): func(i interface{}) interface{} { + return i.(time.Duration).String() + }, +} + +// sanitizeInterface Ensures that en interface is in a format that can be +// converted to a protobuf value. The structpb.ToValue() function expects things +// to be in one of the following formats: +// +// ╔════════════════════════╤════════════════════════════════════════════╗ +// ║ Go type │ Conversion ║ +// ╠════════════════════════╪════════════════════════════════════════════╣ +// ║ nil │ stored as NullValue ║ +// ║ bool │ stored as BoolValue ║ +// ║ int, int32, int64 │ stored as NumberValue ║ +// ║ uint, uint32, uint64 │ stored as NumberValue ║ +// ║ float32, float64 │ stored as NumberValue ║ +// ║ string │ stored as StringValue; must be valid UTF-8 ║ +// ║ []byte │ stored as StringValue; base64-encoded ║ +// ║ map[string]interface{} │ stored as StructValue ║ +// ║ []interface{} │ stored as ListValue ║ +// ╚════════════════════════╧════════════════════════════════════════════╝ +// +// However this means that a data type like []string won't work, despite the +// function being perfectly able to represent it in a protobuf struct. This +// function does its best to example the available data type to ensure that as +// long as the data can in theory be represented by a protobuf struct, the +// conversion will work. +func sanitizeInterface(i interface{}, sortArrays bool, customTransforms TransformMap) interface{} { + if i == nil { + return nil + } + + v := reflect.ValueOf(i) + t := v.Type() + + // Use the transform for this specific type if it exists + if tFunc, ok := customTransforms[t]; ok { + // Reset the value and type to the transformed value. This means that + // even if the function returns something bad, we will then transform it + i = tFunc(i) + + if i == nil { + return nil + } + + v = reflect.ValueOf(i) + t = v.Type() + } + + switch v.Kind() { //nolint:exhaustive // we fall through to the default case + case reflect.Bool: + return v.Bool() + case reflect.Int: + return v.Int() + case reflect.Int8: + return v.Int() + case reflect.Int16: + return v.Int() + case reflect.Int32: + return v.Int() + case reflect.Int64: + return v.Int() + case reflect.Uint: + return v.Uint() + case reflect.Uint8: + return v.Uint() + case reflect.Uint16: + return v.Uint() + case reflect.Uint32: + return v.Uint() + case reflect.Uint64: + return v.Uint() + case reflect.Float32: + return v.Float() + case reflect.Float64: + return v.Float() + case reflect.String: + return fmt.Sprint(v) + case reflect.Array, reflect.Slice: + // We need to check the type of each element in the array and do + // conversion on that + + // returnSlice Returns the array in the format that protobuf can deal with + var returnSlice []interface{} + + returnSlice = make([]interface{}, v.Len()) + + for i := range v.Len() { + returnSlice[i] = sanitizeInterface(v.Index(i).Interface(), sortArrays, customTransforms) + } + + if sortArrays { + sortInterfaceArray(returnSlice) + } + + return returnSlice + case reflect.Map: + var returnMap map[string]interface{} + + returnMap = make(map[string]interface{}) + + for _, mapKey := range v.MapKeys() { + // Convert the key to a string + stringKey := fmt.Sprint(mapKey.Interface()) + + // Convert the value to a compatible interface + value := sanitizeInterface(v.MapIndex(mapKey).Interface(), sortArrays, customTransforms) + returnMap[stringKey] = value + } + + return returnMap + case reflect.Struct: + // In the case of a struct we basically want to turn it into a + // map[string]interface{} + var returnMap map[string]interface{} + + returnMap = make(map[string]interface{}) + + // Range over fields + n := t.NumField() + for i := range n { + field := t.Field(i) + + if field.PkgPath != "" { + // If this has a PkgPath then it is an un-exported fiend and + // should be ignored + continue + } + + // Get the zero value for this field + zeroValue := reflect.Zero(field.Type).Interface() + fieldValue := v.Field(i).Interface() + + // Check if the field is it's nil value + // Check if there actually was a field with that name + if !reflect.DeepEqual(fieldValue, zeroValue) { + returnMap[field.Name] = fieldValue + } + } + + return sanitizeInterface(returnMap, sortArrays, customTransforms) + case reflect.Ptr: + // Get the zero value for this field + zero := reflect.Zero(t) + + // Check if the field is it's nil value + if reflect.DeepEqual(v, zero) { + return nil + } + + return sanitizeInterface(v.Elem().Interface(), sortArrays, customTransforms) + default: + // If we don't recognize the type then we need to see what the + // underlying type is and see if we can convert that + return i + } +} + +// Sorts an interface slice by converting each item to a string and sorting +// these strings +func sortInterfaceArray(input []interface{}) { + sort.Slice(input, func(i, j int) bool { + return fmt.Sprint(input[i]) < fmt.Sprint(input[j]) + }) +} + +// HashSum is a function that takes a byte array and returns a 12 character hash for use in neo4j +func HashSum(b []byte) string { + var paddedEncoding *base32.Encoding + var unpaddedEncoding *base32.Encoding + + shaSum := sha256.Sum256(b) + + // We need to specify a custom encoding here since dGraph has fairly strict + // requirements about what name a variable can have + paddedEncoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyzABCDEF") + + // We also can't have padding since "=" is not allowed in variable names + unpaddedEncoding = paddedEncoding.WithPadding(base32.NoPadding) + + return unpaddedEncoding.EncodeToString(shaSum[:11]) +} diff --git a/go/sdp-go/items.pb.go b/go/sdp-go/items.pb.go new file mode 100644 index 00000000..2f7bf864 --- /dev/null +++ b/go/sdp-go/items.pb.go @@ -0,0 +1,1840 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: items.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Represents the health of something, the meaning of each state may depend on +// the context in which it is used but should be reasonably obvious +type Health int32 + +const ( + Health_HEALTH_UNKNOWN Health = 0 // The health could not be determined + Health_HEALTH_OK Health = 1 // Functioning normally + Health_HEALTH_WARNING Health = 2 // Functioning, but degraded + Health_HEALTH_ERROR Health = 3 // Not functioning + Health_HEALTH_PENDING Health = 4 // Health state is transitioning, such as when something is first provisioned +) + +// Enum value maps for Health. +var ( + Health_name = map[int32]string{ + 0: "HEALTH_UNKNOWN", + 1: "HEALTH_OK", + 2: "HEALTH_WARNING", + 3: "HEALTH_ERROR", + 4: "HEALTH_PENDING", + } + Health_value = map[string]int32{ + "HEALTH_UNKNOWN": 0, + "HEALTH_OK": 1, + "HEALTH_WARNING": 2, + "HEALTH_ERROR": 3, + "HEALTH_PENDING": 4, + } +) + +func (x Health) Enum() *Health { + p := new(Health) + *p = x + return p +} + +func (x Health) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Health) Descriptor() protoreflect.EnumDescriptor { + return file_items_proto_enumTypes[0].Descriptor() +} + +func (Health) Type() protoreflect.EnumType { + return &file_items_proto_enumTypes[0] +} + +func (x Health) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Health.Descriptor instead. +func (Health) EnumDescriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{0} +} + +// QueryMethod represents the available query methods. The details of these +// methods are: +// +// GET: This takes a single unique query and should only return a single item. +// +// If an item matching the parameter passed doesn't exist the server should +// fail +// +// LIST: This takes no query (or ignores it) and should return all items that it +// +// can find +// +// SEARCH: This takes a non-unique query which is designed to be used as a +// +// search term. It should return some number of items (or zero) which +// match the query +type QueryMethod int32 + +const ( + QueryMethod_GET QueryMethod = 0 + QueryMethod_LIST QueryMethod = 1 + QueryMethod_SEARCH QueryMethod = 2 +) + +// Enum value maps for QueryMethod. +var ( + QueryMethod_name = map[int32]string{ + 0: "GET", + 1: "LIST", + 2: "SEARCH", + } + QueryMethod_value = map[string]int32{ + "GET": 0, + "LIST": 1, + "SEARCH": 2, + } +) + +func (x QueryMethod) Enum() *QueryMethod { + p := new(QueryMethod) + *p = x + return p +} + +func (x QueryMethod) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryMethod) Descriptor() protoreflect.EnumDescriptor { + return file_items_proto_enumTypes[1].Descriptor() +} + +func (QueryMethod) Type() protoreflect.EnumType { + return &file_items_proto_enumTypes[1] +} + +func (x QueryMethod) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryMethod.Descriptor instead. +func (QueryMethod) EnumDescriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{1} +} + +// The error type. Any types in here will be gracefully handled unless the +// type os "OTHER" +type QueryStatus_Status int32 + +const ( + // the status has not been specified + QueryStatus_UNSPECIFIED QueryStatus_Status = 0 + // the query has been started + QueryStatus_STARTED QueryStatus_Status = 1 + // the query has been cancelled. + // This is a final state. + QueryStatus_CANCELLED QueryStatus_Status = 3 + // the query has finished with an error status. expect a separate QueryError describing that. + // This is a final state. + // TODO: fold the error details into this message + QueryStatus_ERRORED QueryStatus_Status = 4 + // The query has finished and all results have been sent over the wire + // This is a final state. + QueryStatus_FINISHED QueryStatus_Status = 5 +) + +// Enum value maps for QueryStatus_Status. +var ( + QueryStatus_Status_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "STARTED", + 3: "CANCELLED", + 4: "ERRORED", + 5: "FINISHED", + } + QueryStatus_Status_value = map[string]int32{ + "UNSPECIFIED": 0, + "STARTED": 1, + "CANCELLED": 3, + "ERRORED": 4, + "FINISHED": 5, + } +) + +func (x QueryStatus_Status) Enum() *QueryStatus_Status { + p := new(QueryStatus_Status) + *p = x + return p +} + +func (x QueryStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_items_proto_enumTypes[2].Descriptor() +} + +func (QueryStatus_Status) Type() protoreflect.EnumType { + return &file_items_proto_enumTypes[2] +} + +func (x QueryStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryStatus_Status.Descriptor instead. +func (QueryStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{10, 0} +} + +// The error type. Any types in here will be gracefully handled unless the +// type os "OTHER" +type QueryError_ErrorType int32 + +const ( + // This should be used of all other failure modes, such as timeouts, + // unexpected failures when querying state, permissions errors etc. Errors + // that return this type should not be cached as the error may be transient. + QueryError_OTHER QueryError_ErrorType = 0 + // NOTFOUND means that the item was not found. This is only returned as the + // result of a GET query since all other queries would return an empty + // list instead + QueryError_NOTFOUND QueryError_ErrorType = 1 + // NOSCOPE means that the item was not found because we don't have + // access to the requested scope. This should not be interpreted as "The + // item doesn't exist" (as with a NOTFOUND error) but rather as "We can't + // tell you whether or not the item exists" + QueryError_NOSCOPE QueryError_ErrorType = 2 + // TIMEOUT means that the source times out when trying to query the item. + // The timeout is provided in the original query + QueryError_TIMEOUT QueryError_ErrorType = 3 +) + +// Enum value maps for QueryError_ErrorType. +var ( + QueryError_ErrorType_name = map[int32]string{ + 0: "OTHER", + 1: "NOTFOUND", + 2: "NOSCOPE", + 3: "TIMEOUT", + } + QueryError_ErrorType_value = map[string]int32{ + "OTHER": 0, + "NOTFOUND": 1, + "NOSCOPE": 2, + "TIMEOUT": 3, + } +) + +func (x QueryError_ErrorType) Enum() *QueryError_ErrorType { + p := new(QueryError_ErrorType) + *p = x + return p +} + +func (x QueryError_ErrorType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (QueryError_ErrorType) Descriptor() protoreflect.EnumDescriptor { + return file_items_proto_enumTypes[3].Descriptor() +} + +func (QueryError_ErrorType) Type() protoreflect.EnumType { + return &file_items_proto_enumTypes[3] +} + +func (x QueryError_ErrorType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use QueryError_ErrorType.Descriptor instead. +func (QueryError_ErrorType) EnumDescriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{11, 0} +} + +// This message stores additional information on Edges (and edge-like constructs) to determine how configuration changes can impact +// the linked items. +// +// Blast Propagation options: +// +// |-------|-------|---------------------- +// | in | out | result +// |-------|-------|---------------------- +// | false | false | no change in any item can affect the other +// | false | true | a change to this item can affect its linked items +// | | | example: a change to an EC2 instance can affect its DNS name (in the sense that other items depending on that DNS name will see the impact) +// | true | false | a change to linked items can affect this item +// | | | example: changing the KMS key used by a DynamoDB table can impact the table, but no change to the table can impact the key +// | true | true | changes on both sides of the link can affect the other +// | | | example: changes to both EC2 Instances and their volumes can affect the other side of the relation. +type BlastPropagation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // is true if changes on linked items can affect this item + In bool `protobuf:"varint,1,opt,name=in,proto3" json:"in,omitempty"` + // is true if changes on this item can affect linked items + Out bool `protobuf:"varint,2,opt,name=out,proto3" json:"out,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlastPropagation) Reset() { + *x = BlastPropagation{} + mi := &file_items_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlastPropagation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlastPropagation) ProtoMessage() {} + +func (x *BlastPropagation) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[0] + 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 BlastPropagation.ProtoReflect.Descriptor instead. +func (*BlastPropagation) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{0} +} + +func (x *BlastPropagation) GetIn() bool { + if x != nil { + return x.In + } + return false +} + +func (x *BlastPropagation) GetOut() bool { + if x != nil { + return x.Out + } + return false +} + +// An annotated query to indicate potential linked items. +type LinkedItemQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the query that would find linked items + Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + // how configuration changes (i.e. the "blast") propagates over this link + BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkedItemQuery) Reset() { + *x = LinkedItemQuery{} + mi := &file_items_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkedItemQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkedItemQuery) ProtoMessage() {} + +func (x *LinkedItemQuery) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[1] + 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 LinkedItemQuery.ProtoReflect.Descriptor instead. +func (*LinkedItemQuery) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{1} +} + +func (x *LinkedItemQuery) GetQuery() *Query { + if x != nil { + return x.Query + } + return nil +} + +func (x *LinkedItemQuery) GetBlastPropagation() *BlastPropagation { + if x != nil { + return x.BlastPropagation + } + return nil +} + +// An annotated reference to list linked items. +type LinkedItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the linked item + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // how configuration changes (i.e. the "blast") propagates over this link + BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkedItem) Reset() { + *x = LinkedItem{} + mi := &file_items_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkedItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkedItem) ProtoMessage() {} + +func (x *LinkedItem) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[2] + 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 LinkedItem.ProtoReflect.Descriptor instead. +func (*LinkedItem) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{2} +} + +func (x *LinkedItem) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *LinkedItem) GetBlastPropagation() *BlastPropagation { + if x != nil { + return x.BlastPropagation + } + return nil +} + +// This is the same as Item within the package with a couple of exceptions, no +// real reason why this whole thing couldn't be modelled in protobuf though if +// required. Just need to decide what if anything should remain private +type Item struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + UniqueAttribute string `protobuf:"bytes,2,opt,name=uniqueAttribute,proto3" json:"uniqueAttribute,omitempty"` + Attributes *ItemAttributes `protobuf:"bytes,3,opt,name=attributes,proto3" json:"attributes,omitempty"` + Metadata *Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + // The scope within which the item is unique. Item uniqueness is determined + // by the combination of type and uniqueAttribute value. However it is + // possible for the same item to exist in many scopes. There is not formal + // definition for what a scope should be other than the fact that it should + // be somewhat descriptive and should ensure item uniqueness + Scope string `protobuf:"bytes,5,opt,name=scope,proto3" json:"scope,omitempty"` + // Not all items will have relatedItems we are are using a two byte + // integer to save one byte integers for more common things + LinkedItemQueries []*LinkedItemQuery `protobuf:"bytes,16,rep,name=linkedItemQueries,proto3" json:"linkedItemQueries,omitempty"` + // Linked items + LinkedItems []*LinkedItem `protobuf:"bytes,17,rep,name=linkedItems,proto3" json:"linkedItems,omitempty"` + // (optional) Represents the health of the item. Only items that have a + // clearly relevant health attribute should return a value for health + Health *Health `protobuf:"varint,18,opt,name=health,proto3,enum=Health,oneof" json:"health,omitempty"` + // Arbitrary key-value pairs that can be used to store additional information. + // These tags are retrieved from the source and map to the target's definition + // of a tag (e.g. AWS tags, Kubernetes labels, etc.) + Tags map[string]string `protobuf:"bytes,19,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // The available log streams for this item, if any. Use the Logs service to + // access the actual contents. + LogStreams []*LogStreamDetails `protobuf:"bytes,20,rep,name=logStreams,proto3" json:"logStreams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Item) Reset() { + *x = Item{} + mi := &file_items_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Item) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Item) ProtoMessage() {} + +func (x *Item) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[3] + 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 Item.ProtoReflect.Descriptor instead. +func (*Item) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{3} +} + +func (x *Item) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Item) GetUniqueAttribute() string { + if x != nil { + return x.UniqueAttribute + } + return "" +} + +func (x *Item) GetAttributes() *ItemAttributes { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *Item) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Item) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *Item) GetLinkedItemQueries() []*LinkedItemQuery { + if x != nil { + return x.LinkedItemQueries + } + return nil +} + +func (x *Item) GetLinkedItems() []*LinkedItem { + if x != nil { + return x.LinkedItems + } + return nil +} + +func (x *Item) GetHealth() Health { + if x != nil && x.Health != nil { + return *x.Health + } + return Health_HEALTH_UNKNOWN +} + +func (x *Item) GetTags() map[string]string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *Item) GetLogStreams() []*LogStreamDetails { + if x != nil { + return x.LogStreams + } + return nil +} + +// ItemAttributes represents the known attributes for an item. These are likely +// to be common to a given type, but even this is not guaranteed. All items must +// have at least one attribute however as it needs something to uniquely +// identify it +type ItemAttributes struct { + state protoimpl.MessageState `protogen:"open.v1"` + AttrStruct *structpb.Struct `protobuf:"bytes,1,opt,name=attrStruct,proto3" json:"attrStruct,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItemAttributes) Reset() { + *x = ItemAttributes{} + mi := &file_items_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItemAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemAttributes) ProtoMessage() {} + +func (x *ItemAttributes) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[4] + 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 ItemAttributes.ProtoReflect.Descriptor instead. +func (*ItemAttributes) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{4} +} + +func (x *ItemAttributes) GetAttrStruct() *structpb.Struct { + if x != nil { + return x.AttrStruct + } + return nil +} + +// Metadata about the item. Where it came from, how long it took, etc. +type Metadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This is the name of the source that was used to find the item. + SourceName string `protobuf:"bytes,2,opt,name=sourceName,proto3" json:"sourceName,omitempty"` + // The query that caused this item to be found. This is for gateway-internal use and will not be exposed to the frontend. + SourceQuery *Query `protobuf:"bytes,3,opt,name=sourceQuery,proto3" json:"sourceQuery,omitempty"` + // The time that the item was found + Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // How long the source took to execute in total when processing the Query. + // + // (deprecated) This is no longer sent as streaming responses make this metric + // impossible to calculate on a per-item basis + // + // Deprecated: Marked as deprecated in items.proto. + SourceDuration *durationpb.Duration `protobuf:"bytes,5,opt,name=sourceDuration,proto3" json:"sourceDuration,omitempty"` + // How long the source took to execute per item when processing the + // Query + // + // (deprecated) This is no longer sent + // + // Deprecated: Marked as deprecated in items.proto. + SourceDurationPerItem *durationpb.Duration `protobuf:"bytes,6,opt,name=sourceDurationPerItem,proto3" json:"sourceDurationPerItem,omitempty"` + // Whether the item should be hidden/ignored by user-facing things such as + // GUIs and databases. + // + // Some types of items are only relevant in calculating higher-layer + // abstractions and are therefore always hidden. A good example of this would + // be the output of a command. This could be used by a remote source to gather + // information, but we don't actually want to show the user all the commands + // that were run, just the final item returned by the source + Hidden bool `protobuf:"varint,7,opt,name=hidden,proto3" json:"hidden,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Metadata) Reset() { + *x = Metadata{} + mi := &file_items_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metadata) ProtoMessage() {} + +func (x *Metadata) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[5] + 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 Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{5} +} + +func (x *Metadata) GetSourceName() string { + if x != nil { + return x.SourceName + } + return "" +} + +func (x *Metadata) GetSourceQuery() *Query { + if x != nil { + return x.SourceQuery + } + return nil +} + +func (x *Metadata) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +// Deprecated: Marked as deprecated in items.proto. +func (x *Metadata) GetSourceDuration() *durationpb.Duration { + if x != nil { + return x.SourceDuration + } + return nil +} + +// Deprecated: Marked as deprecated in items.proto. +func (x *Metadata) GetSourceDurationPerItem() *durationpb.Duration { + if x != nil { + return x.SourceDurationPerItem + } + return nil +} + +func (x *Metadata) GetHidden() bool { + if x != nil { + return x.Hidden + } + return false +} + +// This is a list of items, like a List() would return +type Items struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Items) Reset() { + *x = Items{} + mi := &file_items_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Items) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Items) ProtoMessage() {} + +func (x *Items) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[6] + 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 Items.ProtoReflect.Descriptor instead. +func (*Items) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{6} +} + +func (x *Items) GetItems() []*Item { + if x != nil { + return x.Items + } + return nil +} + +// describes the details of a Log Stream for an item +type LogStreamDetails struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The descriptive name for display purposes + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // the source scope for this log stream. Has to be a specific scope, not + // wildcarded. + Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` + // The query that should pe passed back to the upstream + // API to get log lines from this stream + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogStreamDetails) Reset() { + *x = LogStreamDetails{} + mi := &file_items_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogStreamDetails) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogStreamDetails) ProtoMessage() {} + +func (x *LogStreamDetails) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[7] + 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 LogStreamDetails.ProtoReflect.Descriptor instead. +func (*LogStreamDetails) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{7} +} + +func (x *LogStreamDetails) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LogStreamDetails) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *LogStreamDetails) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +// Query represents a query for an item or a list of items. +type Query struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of item to search for. "*" means all types + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + // Which method to use when looking for it + Method QueryMethod `protobuf:"varint,2,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` + // What query should be passed to that method + Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + // Defines how this query should behave when finding new items + RecursionBehaviour *Query_RecursionBehaviour `protobuf:"bytes,4,opt,name=recursionBehaviour,proto3" json:"recursionBehaviour,omitempty"` + // The scope for which we are requesting. To query all scopes use the the + // wildcard '*' + Scope string `protobuf:"bytes,5,opt,name=scope,proto3" json:"scope,omitempty"` + // Whether to ignore the cache and execute the query regardless. + // + // By default sources will implement some level of caching, this is + // particularly important for linked items as a single query with a large link + // depth may result in the same item being queried many times as links are + // resolved and more and more items link to each other. However if required + // this caching can be turned off using this parameter + IgnoreCache bool `protobuf:"varint,6,opt,name=ignoreCache,proto3" json:"ignoreCache,omitempty"` + // A UUID to uniquely identify the query. This should be stored by the + // requester as it will be needed later if the requester wants to cancel a + // query. It should be stored as 128 bytes, as opposed to the textual + // representation + UUID []byte `protobuf:"bytes,7,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The deadline for this query. When the deadline elapses, results become + // irrelevant for the sender and any processing can stop. The deadline gets + // propagated to all related queries (e.g. for linked items) and processes. + // Note: there is currently a migration going on from timeouts to durations, + // so depending on which service is hit, either one is evaluated. + Deadline *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=deadline,proto3" json:"deadline,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Query) Reset() { + *x = Query{} + mi := &file_items_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Query) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Query) ProtoMessage() {} + +func (x *Query) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[8] + 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 Query.ProtoReflect.Descriptor instead. +func (*Query) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{8} +} + +func (x *Query) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Query) GetMethod() QueryMethod { + if x != nil { + return x.Method + } + return QueryMethod_GET +} + +func (x *Query) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *Query) GetRecursionBehaviour() *Query_RecursionBehaviour { + if x != nil { + return x.RecursionBehaviour + } + return nil +} + +func (x *Query) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *Query) GetIgnoreCache() bool { + if x != nil { + return x.IgnoreCache + } + return false +} + +func (x *Query) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *Query) GetDeadline() *timestamppb.Timestamp { + if x != nil { + return x.Deadline + } + return nil +} + +type QueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to ResponseType: + // + // *QueryResponse_NewItem + // *QueryResponse_Response + // *QueryResponse_Error + // *QueryResponse_Edge + ResponseType isQueryResponse_ResponseType `protobuf_oneof:"response_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryResponse) Reset() { + *x = QueryResponse{} + mi := &file_items_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryResponse) ProtoMessage() {} + +func (x *QueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[9] + 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 QueryResponse.ProtoReflect.Descriptor instead. +func (*QueryResponse) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{9} +} + +func (x *QueryResponse) GetResponseType() isQueryResponse_ResponseType { + if x != nil { + return x.ResponseType + } + return nil +} + +func (x *QueryResponse) GetNewItem() *Item { + if x != nil { + if x, ok := x.ResponseType.(*QueryResponse_NewItem); ok { + return x.NewItem + } + } + return nil +} + +func (x *QueryResponse) GetResponse() *Response { + if x != nil { + if x, ok := x.ResponseType.(*QueryResponse_Response); ok { + return x.Response + } + } + return nil +} + +func (x *QueryResponse) GetError() *QueryError { + if x != nil { + if x, ok := x.ResponseType.(*QueryResponse_Error); ok { + return x.Error + } + } + return nil +} + +func (x *QueryResponse) GetEdge() *Edge { + if x != nil { + if x, ok := x.ResponseType.(*QueryResponse_Edge); ok { + return x.Edge + } + } + return nil +} + +type isQueryResponse_ResponseType interface { + isQueryResponse_ResponseType() +} + +type QueryResponse_NewItem struct { + NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered +} + +type QueryResponse_Response struct { + Response *Response `protobuf:"bytes,3,opt,name=response,proto3,oneof"` // Status update +} + +type QueryResponse_Error struct { + Error *QueryError `protobuf:"bytes,4,opt,name=error,proto3,oneof"` // An error has been encountered +} + +type QueryResponse_Edge struct { + Edge *Edge `protobuf:"bytes,5,opt,name=edge,proto3,oneof"` // a link between items/queries +} + +func (*QueryResponse_NewItem) isQueryResponse_ResponseType() {} + +func (*QueryResponse_Response) isQueryResponse_ResponseType() {} + +func (*QueryResponse_Error) isQueryResponse_ResponseType() {} + +func (*QueryResponse_Edge) isQueryResponse_ResponseType() {} + +// QueryStatus informs the client of status updates of all queries running in this session. +type QueryStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UUID of the query + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + Status QueryStatus_Status `protobuf:"varint,2,opt,name=status,proto3,enum=QueryStatus_Status" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryStatus) Reset() { + *x = QueryStatus{} + mi := &file_items_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryStatus) ProtoMessage() {} + +func (x *QueryStatus) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[10] + 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 QueryStatus.ProtoReflect.Descriptor instead. +func (*QueryStatus) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{10} +} + +func (x *QueryStatus) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *QueryStatus) GetStatus() QueryStatus_Status { + if x != nil { + return x.Status + } + return QueryStatus_UNSPECIFIED +} + +// QueryError is sent back when an item query fails +type QueryError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UUID of the item query that this response is in relation to (in binary + // format) + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + ErrorType QueryError_ErrorType `protobuf:"varint,2,opt,name=errorType,proto3,enum=QueryError_ErrorType" json:"errorType,omitempty"` + // The string contents of the error + ErrorString string `protobuf:"bytes,3,opt,name=errorString,proto3" json:"errorString,omitempty"` + // The scope from which the error was raised + Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"` + // The name of the source which raised the error (if relevant) + SourceName string `protobuf:"bytes,5,opt,name=sourceName,proto3" json:"sourceName,omitempty"` + // The type of item that we were looking for at the time of the error + ItemType string `protobuf:"bytes,6,opt,name=itemType,proto3" json:"itemType,omitempty"` + // The name of the responder that this error was raised from + ResponderName string `protobuf:"bytes,7,opt,name=responderName,proto3" json:"responderName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryError) Reset() { + *x = QueryError{} + mi := &file_items_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryError) ProtoMessage() {} + +func (x *QueryError) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[11] + 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 QueryError.ProtoReflect.Descriptor instead. +func (*QueryError) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{11} +} + +func (x *QueryError) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *QueryError) GetErrorType() QueryError_ErrorType { + if x != nil { + return x.ErrorType + } + return QueryError_OTHER +} + +func (x *QueryError) GetErrorString() string { + if x != nil { + return x.ErrorString + } + return "" +} + +func (x *QueryError) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *QueryError) GetSourceName() string { + if x != nil { + return x.SourceName + } + return "" +} + +func (x *QueryError) GetItemType() string { + if x != nil { + return x.ItemType + } + return "" +} + +func (x *QueryError) GetResponderName() string { + if x != nil { + return x.ResponderName + } + return "" +} + +// The message signals that the Query with the corresponding UUID should +// be cancelled. Work should stop immediately, and a final response should be +// sent with a state of CANCELLED to acknowledge that the query has ended due +// to a cancellation +type CancelQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UUID of the Query to cancel + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelQuery) Reset() { + *x = CancelQuery{} + mi := &file_items_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelQuery) ProtoMessage() {} + +func (x *CancelQuery) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[12] + 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 CancelQuery.ProtoReflect.Descriptor instead. +func (*CancelQuery) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{12} +} + +func (x *CancelQuery) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +// This requests that the gateway "expands" an item. This involves executing all +// linked item queries within the session and sending the results to the +// client. It is recommended that this be used rather than simply sending each +// linked item request. Using this request type allows the Gateway to save the +// session more intelligently so that it can be bookmarked and used later. +// "Expanding" an item will mean an item always acts the same, even if its +// linked item queries have changed +type Expand struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The item that should be expanded + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // How many levels of expansion should be run + LinkDepth uint32 `protobuf:"varint,2,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` + // A UUID to uniquely identify the request. This should be stored by the + // requester as it will be needed later if the requester wants to cancel a + // request. It should be stored as 128 bytes, as opposed to the textual + // representation + UUID []byte `protobuf:"bytes,3,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The time at which the gateway should stop processing the queries spawned by this request + Deadline *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=deadline,proto3" json:"deadline,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Expand) Reset() { + *x = Expand{} + mi := &file_items_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Expand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Expand) ProtoMessage() {} + +func (x *Expand) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Expand.ProtoReflect.Descriptor instead. +func (*Expand) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{13} +} + +func (x *Expand) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *Expand) GetLinkDepth() uint32 { + if x != nil { + return x.LinkDepth + } + return 0 +} + +func (x *Expand) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *Expand) GetDeadline() *timestamppb.Timestamp { + if x != nil { + return x.Deadline + } + return nil +} + +// Reference to an item +// +// The uniqueness of an item is determined by the combination of: +// +// - Type +// - UniqueAttributeValue +// - Scope +type Reference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + UniqueAttributeValue string `protobuf:"bytes,2,opt,name=uniqueAttributeValue,proto3" json:"uniqueAttributeValue,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + IsQuery bool `protobuf:"varint,4,opt,name=isQuery,proto3" json:"isQuery,omitempty"` + Query string `protobuf:"bytes,5,opt,name=query,proto3" json:"query,omitempty"` + Method QueryMethod `protobuf:"varint,6,opt,name=method,proto3,enum=QueryMethod" json:"method,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Reference) Reset() { + *x = Reference{} + mi := &file_items_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Reference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reference) ProtoMessage() {} + +func (x *Reference) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reference.ProtoReflect.Descriptor instead. +func (*Reference) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{14} +} + +func (x *Reference) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Reference) GetUniqueAttributeValue() string { + if x != nil { + return x.UniqueAttributeValue + } + return "" +} + +func (x *Reference) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *Reference) GetIsQuery() bool { + if x != nil { + return x.IsQuery + } + return false +} + +func (x *Reference) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *Reference) GetMethod() QueryMethod { + if x != nil { + return x.Method + } + return QueryMethod_GET +} + +// Edge represents a link between two items. The `to` Reference can be a query +// that will be unrolled by the gateway during query processing. Clients are +// guaranteed that edges are only sent after the referenced items. +type Edge struct { + state protoimpl.MessageState `protogen:"open.v1"` + From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + BlastPropagation *BlastPropagation `protobuf:"bytes,3,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Edge) Reset() { + *x = Edge{} + mi := &file_items_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Edge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Edge) ProtoMessage() {} + +func (x *Edge) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[15] + 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 Edge.ProtoReflect.Descriptor instead. +func (*Edge) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{15} +} + +func (x *Edge) GetFrom() *Reference { + if x != nil { + return x.From + } + return nil +} + +func (x *Edge) GetTo() *Reference { + if x != nil { + return x.To + } + return nil +} + +func (x *Edge) GetBlastPropagation() *BlastPropagation { + if x != nil { + return x.BlastPropagation + } + return nil +} + +// Defines how this query should behave when finding new items +type Query_RecursionBehaviour struct { + state protoimpl.MessageState `protogen:"open.v1"` + // How deeply to link items. A value of 0 will mean that items are not linked. + // To resolve linked items "infinitely" simply set this to a high number, with + // the highest being 4,294,967,295. While this isn't truly *infinite*, chances + // are that it is effectively the same, think six degrees of separation etc. + LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` + // set to true to only follow links that propagate configuration change impact + FollowOnlyBlastPropagation bool `protobuf:"varint,2,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Query_RecursionBehaviour) Reset() { + *x = Query_RecursionBehaviour{} + mi := &file_items_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Query_RecursionBehaviour) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Query_RecursionBehaviour) ProtoMessage() {} + +func (x *Query_RecursionBehaviour) ProtoReflect() protoreflect.Message { + mi := &file_items_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Query_RecursionBehaviour.ProtoReflect.Descriptor instead. +func (*Query_RecursionBehaviour) Descriptor() ([]byte, []int) { + return file_items_proto_rawDescGZIP(), []int{8, 0} +} + +func (x *Query_RecursionBehaviour) GetLinkDepth() uint32 { + if x != nil { + return x.LinkDepth + } + return 0 +} + +func (x *Query_RecursionBehaviour) GetFollowOnlyBlastPropagation() bool { + if x != nil { + return x.FollowOnlyBlastPropagation + } + return false +} + +var File_items_proto protoreflect.FileDescriptor + +const file_items_proto_rawDesc = "" + + "\n" + + "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"4\n" + + "\x10BlastPropagation\x12\x0e\n" + + "\x02in\x18\x01 \x01(\bR\x02in\x12\x10\n" + + "\x03out\x18\x02 \x01(\bR\x03out\"n\n" + + "\x0fLinkedItemQuery\x12\x1c\n" + + "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05query\x12=\n" + + "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"k\n" + + "\n" + + "LinkedItem\x12\x1e\n" + + "\x04item\x18\x01 \x01(\v2\n" + + ".ReferenceR\x04item\x12=\n" + + "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"\xe3\x03\n" + + "\x04Item\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12(\n" + + "\x0funiqueAttribute\x18\x02 \x01(\tR\x0funiqueAttribute\x12/\n" + + "\n" + + "attributes\x18\x03 \x01(\v2\x0f.ItemAttributesR\n" + + "attributes\x12%\n" + + "\bmetadata\x18\x04 \x01(\v2\t.MetadataR\bmetadata\x12\x14\n" + + "\x05scope\x18\x05 \x01(\tR\x05scope\x12>\n" + + "\x11linkedItemQueries\x18\x10 \x03(\v2\x10.LinkedItemQueryR\x11linkedItemQueries\x12-\n" + + "\vlinkedItems\x18\x11 \x03(\v2\v.LinkedItemR\vlinkedItems\x12$\n" + + "\x06health\x18\x12 \x01(\x0e2\a.HealthH\x00R\x06health\x88\x01\x01\x12#\n" + + "\x04tags\x18\x13 \x03(\v2\x0f.Item.TagsEntryR\x04tags\x121\n" + + "\n" + + "logStreams\x18\x14 \x03(\v2\x11.LogStreamDetailsR\n" + + "logStreams\x1a7\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + + "\a_health\"I\n" + + "\x0eItemAttributes\x127\n" + + "\n" + + "attrStruct\x18\x01 \x01(\v2\x17.google.protobuf.StructR\n" + + "attrStruct\"\xc2\x02\n" + + "\bMetadata\x12\x1e\n" + + "\n" + + "sourceName\x18\x02 \x01(\tR\n" + + "sourceName\x12(\n" + + "\vsourceQuery\x18\x03 \x01(\v2\x06.QueryR\vsourceQuery\x128\n" + + "\ttimestamp\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12E\n" + + "\x0esourceDuration\x18\x05 \x01(\v2\x19.google.protobuf.DurationB\x02\x18\x01R\x0esourceDuration\x12S\n" + + "\x15sourceDurationPerItem\x18\x06 \x01(\v2\x19.google.protobuf.DurationB\x02\x18\x01R\x15sourceDurationPerItem\x12\x16\n" + + "\x06hidden\x18\a \x01(\bR\x06hidden\"$\n" + + "\x05Items\x12\x1b\n" + + "\x05items\x18\x01 \x03(\v2\x05.ItemR\x05items\"R\n" + + "\x10LogStreamDetails\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05scope\x18\x02 \x01(\tR\x05scope\x12\x14\n" + + "\x05query\x18\x03 \x01(\tR\x05query\"\xa0\x03\n" + + "\x05Query\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + + "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + + "\x05query\x18\x03 \x01(\tR\x05query\x12I\n" + + "\x12recursionBehaviour\x18\x04 \x01(\v2\x19.Query.RecursionBehaviourR\x12recursionBehaviour\x12\x14\n" + + "\x05scope\x18\x05 \x01(\tR\x05scope\x12 \n" + + "\vignoreCache\x18\x06 \x01(\bR\vignoreCache\x12\x12\n" + + "\x04UUID\x18\a \x01(\fR\x04UUID\x126\n" + + "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1ar\n" + + "\x12RecursionBehaviour\x12\x1c\n" + + "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepth\x12>\n" + + "\x1afollowOnlyBlastPropagation\x18\x02 \x01(\bR\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + + "\rQueryResponse\x12!\n" + + "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12'\n" + + "\bresponse\x18\x03 \x01(\v2\t.ResponseH\x00R\bresponse\x12#\n" + + "\x05error\x18\x04 \x01(\v2\v.QueryErrorH\x00R\x05error\x12\x1b\n" + + "\x04edge\x18\x05 \x01(\v2\x05.EdgeH\x00R\x04edgeB\x0f\n" + + "\rresponse_type\"\xa6\x01\n" + + "\vQueryStatus\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12+\n" + + "\x06status\x18\x02 \x01(\x0e2\x13.QueryStatus.StatusR\x06status\"V\n" + + "\x06Status\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\v\n" + + "\aSTARTED\x10\x01\x12\r\n" + + "\tCANCELLED\x10\x03\x12\v\n" + + "\aERRORED\x10\x04\x12\f\n" + + "\bFINISHED\x10\x05\"\x04\b\x02\x10\x02\"\xaf\x02\n" + + "\n" + + "QueryError\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x123\n" + + "\terrorType\x18\x02 \x01(\x0e2\x15.QueryError.ErrorTypeR\terrorType\x12 \n" + + "\verrorString\x18\x03 \x01(\tR\verrorString\x12\x14\n" + + "\x05scope\x18\x04 \x01(\tR\x05scope\x12\x1e\n" + + "\n" + + "sourceName\x18\x05 \x01(\tR\n" + + "sourceName\x12\x1a\n" + + "\bitemType\x18\x06 \x01(\tR\bitemType\x12$\n" + + "\rresponderName\x18\a \x01(\tR\rresponderName\">\n" + + "\tErrorType\x12\t\n" + + "\x05OTHER\x10\x00\x12\f\n" + + "\bNOTFOUND\x10\x01\x12\v\n" + + "\aNOSCOPE\x10\x02\x12\v\n" + + "\aTIMEOUT\x10\x03\"!\n" + + "\vCancelQuery\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x92\x01\n" + + "\x06Expand\x12\x1e\n" + + "\x04item\x18\x01 \x01(\v2\n" + + ".ReferenceR\x04item\x12\x1c\n" + + "\tlinkDepth\x18\x02 \x01(\rR\tlinkDepth\x12\x12\n" + + "\x04UUID\x18\x03 \x01(\fR\x04UUID\x126\n" + + "\bdeadline\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\"\xbf\x01\n" + + "\tReference\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x122\n" + + "\x14uniqueAttributeValue\x18\x02 \x01(\tR\x14uniqueAttributeValue\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + + "\aisQuery\x18\x04 \x01(\bR\aisQuery\x12\x14\n" + + "\x05query\x18\x05 \x01(\tR\x05query\x12$\n" + + "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"\x81\x01\n" + + "\x04Edge\x12\x1e\n" + + "\x04from\x18\x01 \x01(\v2\n" + + ".ReferenceR\x04from\x12\x1a\n" + + "\x02to\x18\x02 \x01(\v2\n" + + ".ReferenceR\x02to\x12=\n" + + "\x10blastPropagation\x18\x03 \x01(\v2\x11.BlastPropagationR\x10blastPropagation*e\n" + + "\x06Health\x12\x12\n" + + "\x0eHEALTH_UNKNOWN\x10\x00\x12\r\n" + + "\tHEALTH_OK\x10\x01\x12\x12\n" + + "\x0eHEALTH_WARNING\x10\x02\x12\x10\n" + + "\fHEALTH_ERROR\x10\x03\x12\x12\n" + + "\x0eHEALTH_PENDING\x10\x04*,\n" + + "\vQueryMethod\x12\a\n" + + "\x03GET\x10\x00\x12\b\n" + + "\x04LIST\x10\x01\x12\n" + + "\n" + + "\x06SEARCH\x10\x02B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_items_proto_rawDescOnce sync.Once + file_items_proto_rawDescData []byte +) + +func file_items_proto_rawDescGZIP() []byte { + file_items_proto_rawDescOnce.Do(func() { + file_items_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_items_proto_rawDesc), len(file_items_proto_rawDesc))) + }) + return file_items_proto_rawDescData +} + +var file_items_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_items_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_items_proto_goTypes = []any{ + (Health)(0), // 0: Health + (QueryMethod)(0), // 1: QueryMethod + (QueryStatus_Status)(0), // 2: QueryStatus.Status + (QueryError_ErrorType)(0), // 3: QueryError.ErrorType + (*BlastPropagation)(nil), // 4: BlastPropagation + (*LinkedItemQuery)(nil), // 5: LinkedItemQuery + (*LinkedItem)(nil), // 6: LinkedItem + (*Item)(nil), // 7: Item + (*ItemAttributes)(nil), // 8: ItemAttributes + (*Metadata)(nil), // 9: Metadata + (*Items)(nil), // 10: Items + (*LogStreamDetails)(nil), // 11: LogStreamDetails + (*Query)(nil), // 12: Query + (*QueryResponse)(nil), // 13: QueryResponse + (*QueryStatus)(nil), // 14: QueryStatus + (*QueryError)(nil), // 15: QueryError + (*CancelQuery)(nil), // 16: CancelQuery + (*Expand)(nil), // 17: Expand + (*Reference)(nil), // 18: Reference + (*Edge)(nil), // 19: Edge + nil, // 20: Item.TagsEntry + (*Query_RecursionBehaviour)(nil), // 21: Query.RecursionBehaviour + (*structpb.Struct)(nil), // 22: google.protobuf.Struct + (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 24: google.protobuf.Duration + (*Response)(nil), // 25: Response +} +var file_items_proto_depIdxs = []int32{ + 12, // 0: LinkedItemQuery.query:type_name -> Query + 4, // 1: LinkedItemQuery.blastPropagation:type_name -> BlastPropagation + 18, // 2: LinkedItem.item:type_name -> Reference + 4, // 3: LinkedItem.blastPropagation:type_name -> BlastPropagation + 8, // 4: Item.attributes:type_name -> ItemAttributes + 9, // 5: Item.metadata:type_name -> Metadata + 5, // 6: Item.linkedItemQueries:type_name -> LinkedItemQuery + 6, // 7: Item.linkedItems:type_name -> LinkedItem + 0, // 8: Item.health:type_name -> Health + 20, // 9: Item.tags:type_name -> Item.TagsEntry + 11, // 10: Item.logStreams:type_name -> LogStreamDetails + 22, // 11: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct + 12, // 12: Metadata.sourceQuery:type_name -> Query + 23, // 13: Metadata.timestamp:type_name -> google.protobuf.Timestamp + 24, // 14: Metadata.sourceDuration:type_name -> google.protobuf.Duration + 24, // 15: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration + 7, // 16: Items.items:type_name -> Item + 1, // 17: Query.method:type_name -> QueryMethod + 21, // 18: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour + 23, // 19: Query.deadline:type_name -> google.protobuf.Timestamp + 7, // 20: QueryResponse.newItem:type_name -> Item + 25, // 21: QueryResponse.response:type_name -> Response + 15, // 22: QueryResponse.error:type_name -> QueryError + 19, // 23: QueryResponse.edge:type_name -> Edge + 2, // 24: QueryStatus.status:type_name -> QueryStatus.Status + 3, // 25: QueryError.errorType:type_name -> QueryError.ErrorType + 18, // 26: Expand.item:type_name -> Reference + 23, // 27: Expand.deadline:type_name -> google.protobuf.Timestamp + 1, // 28: Reference.method:type_name -> QueryMethod + 18, // 29: Edge.from:type_name -> Reference + 18, // 30: Edge.to:type_name -> Reference + 4, // 31: Edge.blastPropagation:type_name -> BlastPropagation + 32, // [32:32] is the sub-list for method output_type + 32, // [32:32] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name +} + +func init() { file_items_proto_init() } +func file_items_proto_init() { + if File_items_proto != nil { + return + } + file_responses_proto_init() + file_items_proto_msgTypes[3].OneofWrappers = []any{} + file_items_proto_msgTypes[9].OneofWrappers = []any{ + (*QueryResponse_NewItem)(nil), + (*QueryResponse_Response)(nil), + (*QueryResponse_Error)(nil), + (*QueryResponse_Edge)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_items_proto_rawDesc), len(file_items_proto_rawDesc)), + NumEnums: 4, + NumMessages: 18, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_items_proto_goTypes, + DependencyIndexes: file_items_proto_depIdxs, + EnumInfos: file_items_proto_enumTypes, + MessageInfos: file_items_proto_msgTypes, + }.Build() + File_items_proto = out.File + file_items_proto_goTypes = nil + file_items_proto_depIdxs = nil +} diff --git a/go/sdp-go/items_test.go b/go/sdp-go/items_test.go new file mode 100644 index 00000000..666f3c69 --- /dev/null +++ b/go/sdp-go/items_test.go @@ -0,0 +1,695 @@ +package sdp + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type ToAttributesTest struct { + Name string + Input map[string]interface{} +} + +type CustomString string + +var Dylan CustomString = "Dylan" + +type CustomBool bool + +var Bool1 CustomBool = false +var NilPointerBool *bool + +type CustomStruct struct { + Foo string `json:",omitempty"` + Bar string `json:",omitempty"` + Baz string `json:",omitempty"` + Time time.Time `json:",omitempty"` + Duration time.Duration `json:",omitempty"` +} + +var ToAttributesTests = []ToAttributesTest{ + { + Name: "Basic strings map", + Input: map[string]interface{}{ + "firstName": "Dylan", + "lastName": "Ratcliffe", + }, + }, + { + Name: "Arrays map", + Input: map[string]interface{}{ + "empty": []string{}, + "single-level": []string{ + "one", + "two", + }, + "multi-level": [][]string{ + { + "one-one", + "one-two", + }, + { + "two-one", + "two-two", + }, + }, + }, + }, + { + Name: "Nested strings maps", + Input: map[string]interface{}{ + "strings map": map[string]string{ + "foo": "bar", + }, + }, + }, + { + Name: "Nested integer map", + Input: map[string]interface{}{ + "numbers map": map[string]int{ + "one": 1, + "two": 2, + }, + }, + }, + { + Name: "Nested string-array map", + Input: map[string]interface{}{ + "arrays map": map[string][]string{ + "dogs": { + "pug", + "also pug", + }, + }, + }, + }, + { + Name: "Nested non-string keys map", + Input: map[string]interface{}{ + "non-string keys": map[int]string{ + 1: "one", + 2: "two", + 3: "three", + }, + }, + }, + { + Name: "Composite types", + Input: map[string]interface{}{ + "underlying string": Dylan, + "underlying bool": Bool1, + }, + }, + { + Name: "Pointers", + Input: map[string]interface{}{ + "pointer bool": &Bool1, + "pointer string": &Dylan, + }, + }, + { + Name: "structs", + Input: map[string]interface{}{ + "named struct": CustomStruct{ + Foo: "foo", + Bar: "bar", + Baz: "baz", + Time: time.Now(), + }, + "anon struct": struct { + Yes bool + }{ + Yes: true, + }, + }, + }, + { + Name: "Zero-value structs", + Input: map[string]interface{}{ + "something": CustomStruct{ + Foo: "yes", + Time: time.Now(), + }, + }, + }, +} + +func TestToAttributes(t *testing.T) { + for _, tat := range ToAttributesTests { + t.Run(tat.Name, func(t *testing.T) { + var inputBytes []byte + var attributesBytes []byte + var inputJSON string + var attributesJSON string + var attributes *ItemAttributes + var err error + + // Convert the input to Attributes + attributes, err = ToAttributes(tat.Input) + + if err != nil { + t.Fatal(err) + } + + // In order to compare these reliably I'm going to do the following: + // + // 1. Convert to JSON + // 2. Convert back again + // 3. Compare with reflect.DeepEqual() + + // Convert the input to JSON + inputBytes, err = json.MarshalIndent(tat.Input, "", " ") + + if err != nil { + t.Fatal(err) + } + + // Convert the attributes to JSON + attributesBytes, err = json.MarshalIndent(attributes.GetAttrStruct().AsMap(), "", " ") + + if err != nil { + t.Fatal(err) + } + + var input map[string]interface{} + var output map[string]interface{} + + err = json.Unmarshal(inputBytes, &input) + + if err != nil { + t.Fatal(err) + } + + err = json.Unmarshal(attributesBytes, &output) + + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(input, output) { + // Convert to strings for printing + inputJSON = string(inputBytes) + attributesJSON = string(attributesBytes) + + t.Errorf("JSON did not match (note that order of map keys doesn't matter)\nInput: %v\nAttributes: %v", inputJSON, attributesJSON) + } + }) + + } +} + +func TestDefaultTransformMap(t *testing.T) { + input := map[string]interface{}{ + // Use a duration + "hour": 1 * time.Hour, + } + + attributes, err := ToAttributes(input) + + if err != nil { + t.Fatal(err) + } + + hour, err := attributes.Get("hour") + + if err != nil { + t.Fatal(err) + } + + if hour != "1h0m0s" { + t.Errorf("Expected hour to be 1h0m0s, got %v", hour) + } +} + +func TestCustomTransforms(t *testing.T) { + t.Run("redaction", func(t *testing.T) { + type Secret struct { + Value string + } + + data := map[string]interface{}{ + "user": map[string]interface{}{ + "name": "Hunter", + "password": Secret{ + Value: "hunter2", + }, + }, + } + + attributes, err := ToAttributesCustom(data, true, TransformMap{ + reflect.TypeOf(Secret{}): func(i interface{}) interface{} { + // Remove it + return "REDACTED" + }, + }) + + if err != nil { + t.Fatal(err) + } + + user, err := attributes.Get("user") + + if err != nil { + t.Fatal(err) + } + + userMap, ok := user.(map[string]interface{}) + + if !ok { + t.Fatalf("Expected user to be a map, got %T", user) + } + + pass := userMap["password"] + if pass != "REDACTED" { + t.Errorf("Expected password to be REDACTED, got %v", pass) + } + }) + + t.Run("map response", func(t *testing.T) { + type Something struct { + Foo string + Bar string + } + + data := map[string]interface{}{ + "something": Something{ + Foo: "foo", + Bar: "bar", + }, + } + + attributes, err := ToAttributesCustom(data, true, TransformMap{ + reflect.TypeOf(Something{}): func(i interface{}) interface{} { + something := i.(Something) + + return map[string]string{ + "foo": something.Foo, + "bar": something.Bar, + } + }, + }) + + if err != nil { + t.Fatal(err) + } + + something, err := attributes.Get("something") + + if err != nil { + t.Fatal(err) + } + + somethingMap, ok := something.(map[string]interface{}) + + if !ok { + t.Fatalf("Expected something to be a map, got %T", something) + } + + if somethingMap["foo"] != "foo" { + t.Errorf("Expected foo to be foo, got %v", somethingMap["foo"]) + } + + if somethingMap["bar"] != "bar" { + t.Errorf("Expected bar to be bar, got %v", somethingMap["bar"]) + } + }) + t.Run("returns nil", func(t *testing.T) { + type Something struct { + Foo string + Bar string + } + + data := map[string]interface{}{ + "something": Something{ + Foo: "foo", + Bar: "bar", + }, + "else": nil, + } + + _, err := ToAttributesCustom(data, true, TransformMap{ + reflect.TypeOf(Something{}): func(i interface{}) interface{} { + return nil + }, + }) + + if err != nil { + t.Fatal(err) + } + }) +} + +func TestCopy(t *testing.T) { + exampleAttributes, err := ToAttributes(map[string]interface{}{ + "name": "Dylan", + "friend": "Mike", + "age": 27, + }) + + if err != nil { + t.Fatalf("Could not convert to attributes: %v", err) + } + + t.Run("With a complete item", func(t *testing.T) { + u := uuid.New() + + itemA := Item{ + Type: "user", + UniqueAttribute: "name", + Scope: "test", + Attributes: exampleAttributes, + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + LinkedItemQueries: []*LinkedItemQuery{ + { + Query: &Query{ + Type: "user", + Method: QueryMethod_GET, + Query: "Mike", + }, + }, + }, + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + LinkedItems: []*LinkedItem{}, + Metadata: &Metadata{ + SourceName: "test", + SourceQuery: &Query{ + Type: "user", + Method: QueryMethod_GET, + Query: "Dylan", + Scope: "testScope", + UUID: u[:], + }, + Timestamp: timestamppb.Now(), + SourceDuration: durationpb.New(100 * time.Millisecond), + SourceDurationPerItem: durationpb.New(10 * time.Millisecond), + }, + Health: Health_HEALTH_ERROR.Enum(), + Tags: map[string]string{ + "foo": "bar", + }, + } + + t.Run("Copying an item", func(t *testing.T) { + itemB := proto.Clone(&itemA).(*Item) + + AssertItemsEqual(&itemA, itemB, t) + }) + }) + + t.Run("With a party-filled item", func(t *testing.T) { + itemA := Item{ + Type: "user", + UniqueAttribute: "name", + Scope: "test", + Attributes: exampleAttributes, + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + LinkedItemQueries: []*LinkedItemQuery{ + { + Query: &Query{ + Type: "user", + Method: QueryMethod_GET, + Query: "Mike", + }, + }, + }, + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + LinkedItems: []*LinkedItem{}, + Metadata: &Metadata{ + Hidden: true, + SourceName: "test", + Timestamp: timestamppb.Now(), + SourceDuration: durationpb.New(100 * time.Millisecond), + SourceDurationPerItem: durationpb.New(10 * time.Millisecond), + }, + } + + t.Run("Copying an item", func(t *testing.T) { + itemB := proto.Clone(&itemA).(*Item) + + AssertItemsEqual(&itemA, itemB, t) + }) + }) + + t.Run("With a minimal item", func(t *testing.T) { + itemA := Item{ + Type: "user", + UniqueAttribute: "name", + Scope: "test", + Attributes: exampleAttributes, + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + LinkedItemQueries: []*LinkedItemQuery{}, + LinkedItems: []*LinkedItem{}, + } + + t.Run("Copying an item", func(t *testing.T) { + itemB := proto.Clone(&itemA).(*Item) + + AssertItemsEqual(&itemA, itemB, t) + }) + }) + +} + +func AssertItemsEqual(itemA *Item, itemB *Item, t *testing.T) { + if itemA.GetScope() != itemB.GetScope() { + t.Error("Scope did not match") + } + + if itemA.GetType() != itemB.GetType() { + t.Error("Type did not match") + } + + if itemA.GetUniqueAttribute() != itemB.GetUniqueAttribute() { + t.Error("UniqueAttribute did not match") + } + + var nameA interface{} + var nameB interface{} + var err error + + nameA, err = itemA.GetAttributes().Get("name") + + if err != nil { + t.Error(err) + } + + nameB, err = itemB.GetAttributes().Get("name") + + if err != nil { + t.Error(err) + } + + if nameA != nameB { + t.Error("Attributes.nam did not match") + + } + + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + if len(itemA.GetLinkedItemQueries()) != len(itemB.GetLinkedItemQueries()) { + t.Error("LinkedItemQueries length did not match") + } + + if len(itemA.GetLinkedItemQueries()) > 0 { + if itemA.GetLinkedItemQueries()[0].GetQuery().GetType() != itemB.GetLinkedItemQueries()[0].GetQuery().GetType() { + t.Error("LinkedItemQueries[0].Type did not match") + } + } + + // TODO(LIQs): delete this; it's not part of `(*sdp.Item).Copy()` anymore + if len(itemA.GetLinkedItems()) != len(itemB.GetLinkedItems()) { + t.Error("LinkedItems length did not match") + } + + if len(itemA.GetLinkedItems()) > 0 { + if itemA.GetLinkedItems()[0].GetItem().GetType() != itemB.GetLinkedItems()[0].GetItem().GetType() { + t.Error("LinkedItemQueries[0].Type did not match") + } + } + + for k, v := range itemA.GetTags() { + if itemB.GetTags()[k] != v { + t.Errorf("Tags[%v] did not match", k) + } + } + + if itemA.Health == nil { + if itemB.Health != nil { + t.Errorf("mismatched health nil and %v", itemB.GetHealth()) + } + } else { + if itemB.Health == nil { + t.Errorf("mismatched health %v and nil", itemA.GetHealth()) + + } else { + if itemA.GetHealth() != itemB.GetHealth() { + t.Errorf("mismatched health %v and %v", itemA.GetHealth(), itemB.GetHealth()) + } + } + } + + if itemA.GetMetadata() != nil { + if itemA.GetMetadata().GetSourceDuration().String() != itemB.GetMetadata().GetSourceDuration().String() { + t.Error("SourceDuration did not match") + } + + if itemA.GetMetadata().GetSourceDurationPerItem().String() != itemB.GetMetadata().GetSourceDurationPerItem().String() { + t.Error("SourceDurationPerItem did not match") + } + + if itemA.GetMetadata().GetSourceName() != itemB.GetMetadata().GetSourceName() { + t.Error("SourceName did not match") + } + + if itemA.GetMetadata().GetTimestamp().String() != itemB.GetMetadata().GetTimestamp().String() { + t.Error("Timestamp did not match") + } + + if itemA.GetMetadata().GetHidden() != itemB.GetMetadata().GetHidden() { + t.Error("Metadata.Hidden does not match") + } + + if itemA.GetMetadata().GetSourceQuery() != nil { + if itemA.GetMetadata().GetSourceQuery().GetScope() != itemB.GetMetadata().GetSourceQuery().GetScope() { + t.Error("Metadata.SourceQuery.Scope does not match") + } + + if itemA.GetMetadata().GetSourceQuery().GetMethod() != itemB.GetMetadata().GetSourceQuery().GetMethod() { + t.Error("Metadata.SourceQuery.Method does not match") + } + + if itemA.GetMetadata().GetSourceQuery().GetQuery() != itemB.GetMetadata().GetSourceQuery().GetQuery() { + t.Error("Metadata.SourceQuery.Query does not match") + } + + if itemA.GetMetadata().GetSourceQuery().GetType() != itemB.GetMetadata().GetSourceQuery().GetType() { + t.Error("Metadata.SourceQuery.Type does not match") + } + + if !bytes.Equal(itemA.GetMetadata().GetSourceQuery().GetUUID(), itemB.GetMetadata().GetSourceQuery().GetUUID()) { + t.Error("Metadata.SourceQuery.UUID does not match") + } + } + } +} + +func TestTimeoutContext(t *testing.T) { + q := Query{ + Type: "person", + Method: QueryMethod_GET, + Query: "foo", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 2, + }, + IgnoreCache: false, + Deadline: timestamppb.New(time.Now().Add(10 * time.Millisecond)), + } + + ctx, cancel := q.TimeoutContext(context.Background()) + defer cancel() + + select { + case <-time.After(20 * time.Millisecond): + t.Error("Context did not time out after 10ms") + case <-ctx.Done(): + // This is good + } +} + +func TestToAttributesViaJson(t *testing.T) { + // Create a random struct + test1 := struct { + Foo string + Bar bool + Blip []string + Baz struct { + Zap string + Bam int + } + }{ + Foo: "foo", + Bar: false, + Blip: []string{ + "yes", + "I", + "blip", + }, + Baz: struct { + Zap string + Bam int + }{ + Zap: "negative", + Bam: 42, + }, + } + + attributes, err := ToAttributesViaJson(test1) + + if err != nil { + t.Fatal(err) + } + + if foo, err := attributes.Get("Foo"); err != nil || foo != "foo" { + t.Errorf("Expected Foo to be 'foo', got %v, err: %v", foo, err) + } +} + +func TestAttributesGet(t *testing.T) { + mapData := map[string]interface{}{ + "foo": "bar", + "nest": map[string]interface{}{ + "nest2": map[string]string{ + "nest3": "nestValue", + }, + }, + } + + attr, err := ToAttributes(mapData) + + if err != nil { + t.Fatal(err) + } + + if v, err := attr.Get("foo"); err != nil || v != "bar" { + t.Errorf("expected Get(\"foo\") to be bar, got %v", v) + } + + if v, err := attr.Get("nest.nest2.nest3"); err != nil || v != "nestValue" { + t.Errorf("expected Get(\"nest.nest2.nest3\") to be nestValue, got %v", v) + } +} + +func TestAttributesSet(t *testing.T) { + mapData := map[string]interface{}{ + "foo": "bar", + "nest": map[string]interface{}{ + "nest2": map[string]string{ + "nest3": "nestValue", + }, + }, + } + + attr, err := ToAttributes(mapData) + + if err != nil { + t.Fatal(err) + } + + err = attr.Set("foo", "baz") + + if err != nil { + t.Error(err) + } + + if v, err := attr.Get("foo"); err != nil || v != "baz" { + t.Errorf("expected Get(\"foo\") to be baz, got %v", v) + } +} diff --git a/go/sdp-go/link_extract.go b/go/sdp-go/link_extract.go new file mode 100644 index 00000000..d0e6c383 --- /dev/null +++ b/go/sdp-go/link_extract.go @@ -0,0 +1,247 @@ +package sdp + +import ( + "net" + "net/url" + "regexp" + "strings" + + "google.golang.org/protobuf/types/known/structpb" +) + +// This function tries to extract linked item queries from the attributes of an +// item. It should be on items that we know are likely to contain references +// that we can discover, but are in an unstructured format which we can't +// construct the linked item queries from directly. A good example of this would +// be the env vars for a kubernetes pod, or a config map +// +// This supports extracting the following formats: +// +// - IP addresses +// - HTTP/HTTPS URLs +// - DNS names +func ExtractLinksFromAttributes(attributes *ItemAttributes) []*LinkedItemQuery { + return extractLinksFromStructValue(attributes.GetAttrStruct()) +} + +// The same as `ExtractLinksFromAttributes`, but takes any input format and +// converts it to a set of ItemAttributes via the `ToAttributes` function. This +// uses reflection. `ExtractLinksFromAttributes` is more efficient if you have +// the attributes already in the correct format. +func ExtractLinksFrom(anything interface{}) ([]*LinkedItemQuery, error) { + attributes, err := ToAttributes(map[string]interface{}{ + "": anything, + }) + if err != nil { + return nil, err + } + + return ExtractLinksFromAttributes(attributes), nil +} + +func extractLinksFromValue(value *structpb.Value) []*LinkedItemQuery { + switch value.GetKind().(type) { + case *structpb.Value_NullValue: + return nil + case *structpb.Value_NumberValue: + return nil + case *structpb.Value_StringValue: + return extractLinksFromStringValue(value.GetStringValue()) + case *structpb.Value_BoolValue: + return nil + case *structpb.Value_StructValue: + return extractLinksFromStructValue(value.GetStructValue()) + case *structpb.Value_ListValue: + return extractLinksFromListValue(value.GetListValue()) + } + + return nil +} + +func extractLinksFromStructValue(structValue *structpb.Struct) []*LinkedItemQuery { + queries := make([]*LinkedItemQuery, 0) + + for _, value := range structValue.GetFields() { + queries = append(queries, extractLinksFromValue(value)...) + } + + return queries +} + +func extractLinksFromListValue(list *structpb.ListValue) []*LinkedItemQuery { + queries := make([]*LinkedItemQuery, 0) + + for _, value := range list.GetValues() { + queries = append(queries, extractLinksFromValue(value)...) + } + + return queries +} + +// A regex that matches the ARN format and extracts the service, region, account +// id and resource. Uses a capture group for the full resource portion after +// the account-id (which may include slashes for resource types). +var awsARNRegex = regexp.MustCompile(`^arn:[\w-]+:([\w-]+):([\w-]*):([\w-]*):(.+)`) + +// This function does all the heavy lifting for extracting linked item queries +// from strings. It will be called once for every string value in the item so +// needs to be very performant +func extractLinksFromStringValue(val string) []*LinkedItemQuery { + if ip := net.ParseIP(val); ip != nil { + return []*LinkedItemQuery{ + { + Query: &Query{ + Type: "ip", + Method: QueryMethod_GET, + Query: ip.String(), + Scope: "global", + }, + BlastPropagation: &BlastPropagation{ + In: true, + Out: true, + }, + }, + } + } + + // This is pretty overzealous when it comes to what it considers a URL, so + // we need ot do out own validation to make sure that it has actually found + // what we expected + if parsed, err := url.Parse(val); err == nil && parsed.Scheme != "" && parsed.Host != "" { + // If it's a HTTP/HTTPS URL, we can use a HTTP query + if parsed.Scheme == "http" || parsed.Scheme == "https" { + return []*LinkedItemQuery{ + { + Query: &Query{ + Type: "http", + Method: QueryMethod_SEARCH, + Query: val, + Scope: "global", + }, + BlastPropagation: &BlastPropagation{ + // If we are referencing a HTTP URL, I think it's safe + // to assume that this is something that the current + // resource depends on and therefore that the blast + // radius should propagate inwards. This is a bit of a + // guess though... + In: true, + Out: false, + }, + }, + } + } + + // If it's not a HTTP/HTTPS URL, it'll be an IP or DNS name, so pass + // back to the main function + return extractLinksFromStringValue(parsed.Hostname()) + } + + if isLikelyDNSName(val) { + return []*LinkedItemQuery{ + { + Query: &Query{ + Type: "dns", + Method: QueryMethod_SEARCH, + Query: val, + Scope: "global", + }, + BlastPropagation: &BlastPropagation{ + In: true, + Out: false, + }, + }, + } + } + + // ARNs can't be shorter than 12 characters + if len(val) >= 12 { + if matches := awsARNRegex.FindStringSubmatch(val); matches != nil { + // If it looks like an ARN then we can construct a SEARCH query to try + // and find it. We can rely on the conventions in the AWS source here + + // Basic validation + if len(matches) != 5 || matches[1] == "" { + return nil + } + + // Parsed ARN parts + service := matches[1] // e.g. "ec2", "iam", "s3" + region := matches[2] // may be empty for global services (iam, cloudfront) + accountID := matches[3] // may be empty (e.g. s3, route53) + resource := matches[4] // full resource segment (may contain ":" or "/") + + // Extract resource type from the resource field (everything before first "/" or ":" if present) + resourceType := resource + if idx := strings.IndexAny(resource, "/:"); idx != -1 { + resourceType = resource[:idx] + } + + // Determine scope using a simple rule: + // - No account → wildcard scope + // - Account, no region → account-only + // - Account and region → account.region + var scope string + if accountID == "" { + scope = WILDCARD + } else if region == "" { + scope = accountID + } else { + scope = accountID + "." + region + } + + // Determine type using a consistent rule. Default to service-resourceType if available. + queryType := service + if resourceType != "" { + queryType = service + "-" + resourceType + } + // Special-case S3 ARNs that omit account and region → treat as bucket references + if service == "s3" && accountID == "" && region == "" { + queryType = "s3-bucket" + + // If this is an S3 object ARN (contains /), extract just the bucket + if strings.Contains(resource, "/") { + bucketName := strings.SplitN(resource, "/", 2)[0] + // Construct a bucket-only ARN for the query + val = "arn:aws:s3:::" + bucketName + } + } + + return []*LinkedItemQuery{ + { + Query: &Query{ + Type: queryType, + Method: QueryMethod_SEARCH, + Query: val, + Scope: scope, + }, + BlastPropagation: &BlastPropagation{ + In: true, + Out: false, + }, + }, + } + } + } + + return nil +} + +// Compile a regex pattern to match the general structure of a DNS name. Limits +// each label to 1-63 characters and matches only allowed characters and ensure +// that the name has at least three sections i.e. two dots. +var dnsNameRegex = regexp.MustCompile(`^(?i)([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\.){2,}[a-z]{2,}$`) + +// This function returns true if the given string is a valid DNS name with at +// least three labels (sections) +func isLikelyDNSName(name string) bool { + // Quick length check before the regex. The less than 6 is because we're + // only matching names that have three sections or more, and the shortest + // three section name is a.b.cd (6 characters, there are no single letter + // top-level domains) + if len(name) < 6 || len(name) > 253 { + return false + } + + // Check if the name matches the regex pattern. + return dnsNameRegex.MatchString(name) +} diff --git a/go/sdp-go/link_extract_test.go b/go/sdp-go/link_extract_test.go new file mode 100644 index 00000000..0314b0f4 --- /dev/null +++ b/go/sdp-go/link_extract_test.go @@ -0,0 +1,795 @@ +package sdp + +import ( + "testing" + + "gopkg.in/yaml.v3" +) + +// Create a very large set of attributes for the benchmark +func createTestData() (*ItemAttributes, interface{}) { + yamlString := `--- +creationTimestamp: 2024-07-09T11:16:31Z +data: + AUTH0_AUDIENCE: https://api.example.com + AUTH0_DOMAIN: domain.eu.auth0.com + AUTH_COOKIE_NAME: overmind_app_access_token + GATEWAY_CLIENT_ID: 1234567890 + GATEWAY_CORS_ALLOW_ORIGINS: https://app.example.com https://*.app.example.com + GATEWAY_OVERMIND_AUTH_URL: https://domain.eu.auth0.com/oauth/token + GATEWAY_OVERMIND_TOKEN_API: http://service:8080/api + GATEWAY_PGDBNAME: user + GATEWAY_PGHOST: name.cluster-id.eu-west-2.rds.amazonaws.com + GATEWAY_PGPORT: "5432" + GATEWAY_PGUSER: user + GATEWAY_RUN_MODE: release + GATEWAY_SERVICE_PORT: "8080" + LOG: info +immutable: false +name: foo-config +namespace: default +resourceVersion: "167230088" +uid: c1c1be5e-e11e-46da-8ef4-ce243fe7056e +generateName: 49731160-e407-4148-bd4d-e00b8eb56cd2-5b76f5987b- +labels: + app: test + config-hash: 2be88ca42 + pod-template-hash: 5b76f5987b + source: 49731160-e407-4148-bd4d-e00b8eb56cd2 +spec: + containers: + - env: + - name: NATS_SERVICE_HOST + value: fdb4:5627:96ee::bfa3 + - name: NATS_SERVICE_PORT + value: "4222" + - name: NATS_NAME_PREFIX + value: source.default + - name: SERVICE_PORT + value: "8080" + - name: NATS_JWT + valueFrom: + secretKeyRef: + key: jwt + name: 49731160-e407-4148-bd4d-e00b8eb56cd2-nats-auth + - name: NATS_NKEY_SEED + valueFrom: + secretKeyRef: + key: nkeySeed + name: 49731160-e407-4148-bd4d-e00b8eb56cd2-nats-auth + - name: NATS_CA_FILE + value: /etc/srcman/certs/ca.pem + - name: S3_BUCKET_ARN + value: arn:aws:s3:::example-bucket-name + - name: S3_OBJECT_ARN + value: arn:aws:s3:::my-test-bucket/data/key + - name: IAM_ROLE_ARN + value: arn:aws:iam::123456789012:role/example-role + - name: CLOUDFRONT_ARN + value: arn:aws:cloudfront::123456789012:distribution/EDFDVBDEXAMPLE + envFrom: + - secretRef: + name: prod-tracing-secrets + image: ghcr.io/example/example:main + imagePullPolicy: Always + name: 49731160-e407-4148-bd4d-e00b8eb56cd2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: healthz + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/srcman/config + name: source-config + readOnly: true + - mountPath: /etc/srcman/certs + name: nats-certs + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-vjgp7 + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + nodeName: ip-10-0-4-118.eu-west-2.compute.internal + preemptionPolicy: PreemptLowerPriority + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: default + serviceAccountName: default + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - configMap: + defaultMode: 420 + name: 49731160-e407-4148-bd4d-e00b8eb56cd2 + name: source-config + - configMap: + defaultMode: 420 + name: prod-ca + name: nats-certs + - name: kube-api-access-vjgp7 + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace +status: + conditions: + - lastTransitionTime: 2024-08-22T13:42:26Z + status: "True" + type: Initialized + - lastTransitionTime: 2024-08-22T13:43:17Z + status: "True" + type: Ready + - lastTransitionTime: 2024-08-22T13:43:17Z + status: "True" + type: ContainersReady + - lastTransitionTime: 2024-08-22T13:42:26Z + status: "True" + type: PodScheduled + containerStatuses: + - containerID: containerd://6274579a84ea3bee8cb9bd68092f4ccd6fff13852c1e5c09672c8b3489f3c082 + image: ghcr.io/example/example:main + imageID: ghcr.io/example/example@sha256:c3fd0767e82105e9127267bda3bdb77f51a9e6fbeb79d20c4d25ae0a71876719 + lastState: {} + name: 49731160-e407-4148-bd4d-e00b8eb56cd2 + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: 2024-08-22T13:42:32Z + hostIP: 2a05:d01c:40:7600::6c81 + phase: Running + podIP: 2a05:d01c:40:7600:fbac::4 + podIPs: + - ip: 2a05:d01c:40:7600:fbac::3 + qosClass: BestEffort + startTime: 2024-08-22T13:42:26Z +code: + location: https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/123456789/ingress_log-dd9f17f1-0694-4e79-9855-300a7f9b7300?versionId=sxmueRdM4S.HXwlebzlb7rOpD2ZHmS3Z&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMiJGMEQCIBWNIrhNoAqNUG%2BoZLmKNSxY9ncDogcyFTGeJFef0zVMAiBhkAW9JWxVna%2FoCXe4u9S3364dCavXEvZP%2FXcD6iwfISq7BQiR%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDQ3MjAzODg2NDE4OC + repositoryType: S3 +configuration: + architectures: + - x86_64 + codeSha256: JxWQc4FaGuW8503fcWt5S2Ua+HHpIX2z2SMhyo/gzBU= + codeSize: 7586073 + description: Parses LB access logs from S3, sending them to Honeycomb as structured events + environment: + variables: + APIHOST: https://api.honeycomb.io + DATASET: ingress + ENVIRONMENT: "" + FILTERFIELDS: "" + FORCEGUNZIP: "true" + HONEYCOMBWRITEKEY: foobar + KMSKEYID: "" + PARSERTYPE: alb + RENAMEFIELDS: "" + SAMPLERATE: "1" + SAMPLERATERULES: "[]" + ephemeralStorage: + size: 512 + functionArn: arn:aws:lambda:eu-west-2:123456789:function:ingress_log + functionName: ingress_log + handler: s3-handler + lastModified: 2024-05-10T14:33:45.279+0000 + lastUpdateStatus: Successful + lastUpdateStatusReasonCode: "" + loggingConfig: + applicationLogLevel: "" + logFormat: Text + logGroup: /aws/lambda/ingress_log + systemLogLevel: "" + memorySize: 192 + packageType: Zip + revisionId: 876d6948-2e4c-41e0-9a62-d9be8a6a59f5 + role: arn:aws:iam::123456789:role/ingress_log + runtime: provided.al2 + runtimeVersionConfig: + runtimeVersionArn: arn:aws:lambda:eu-west-2::runtime:f4d7a18770044f40f09a49471782a2a42431d746fcfb30bf1cadeda985858aa0 + snapStart: + applyOn: None + optimizationStatus: Off + state: Active + stateReasonCode: "" + timeout: 600 + tracingConfig: + mode: PassThrough + version: $LATEST +tags: + honeycombAgentless: "true" + terraform: "true" +capacityProviderStrategy: + - base: 0 + capacityProvider: FARGATE + weight: 100 +clusterArn: arn:aws:ecs:eu-west-2:123456789:cluster/example-tfc +createdAt: 2024-08-01T16:06:18.906Z +createdBy: arn:aws:iam::123456789:role/terraform-example +deploymentConfiguration: + deploymentCircuitBreaker: + enable: false + rollback: false + maximumPercent: 200 + minimumHealthyPercent: 100 +deploymentController: + type: ECS +deployments: + - capacityProviderStrategy: + - base: 0 + capacityProvider: FARGATE + weight: 100 + createdAt: 2024-08-01T16:42:08.6Z + desiredCount: 1 + failedTasks: 0 + id: ecs-svc/5699741454300708027 + launchType: "" + networkConfiguration: + awsvpcConfiguration: + assignPublicIp: DISABLED + securityGroups: + - sg-0826c8494b61cac1f + subnets: + - subnet-0a393cf4c844bf32d + - subnet-0fafe900a3dc4ba78 + pendingCount: 0 + platformFamily: Linux + platformVersion: 1.4.0 + rolloutState: COMPLETED + rolloutStateReason: ECS deployment ecs-svc/5699741454300708027 completed. + runningCount: 1 + status: PRIMARY + taskDefinition: arn:aws:ecs:eu-west-2:123456789:task-definition/facial-recognition-tfc:1 + updatedAt: 2024-08-01T17:20:11.853Z +desiredCount: 1 +enableECSManagedTags: false +enableExecuteCommand: false +events: + - createdAt: 2024-08-01T16:37:45.222Z + id: f8240f68-73d0-497f-bf8e-4cb5185bd76c + message: "(service facial-recognition) has started 1 tasks: (task + d0fd4b687ebf4c968482a9814e1de455)." + - createdAt: 2024-08-22T15:50:56.905Z + id: 769e21aa-7a70-4270-88b9-55f902ddb727 + message: (service facial-recognition) has reached a steady state. +healthCheckGracePeriodSeconds: 0 +launchType: "" +loadBalancers: + - containerName: facial-recognition + containerPort: 1234 + targetGroupArn: arn:aws:elasticloadbalancing:eu-west-2:123456789:targetgroup/facerec-tfc/0b6a17c7de07be40 +networkConfiguration: + awsvpcConfiguration: + assignPublicIp: DISABLED + securityGroups: + - sg-0826c8494b61cac1f + subnets: + - subnet-0a393cf4c844bf32d + - subnet-0fafe900a3dc4ba78 +pendingCount: 0 +placementConstraints: [] +placementStrategy: [] +platformFamily: Linux +platformVersion: LATEST +propagateTags: NONE +roleArn: arn:aws:iam::123456789:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS +runningCount: 1 +schedulingStrategy: REPLICA +serviceArn: arn:aws:ecs:eu-west-2:123456789:service/example-tfc/facial-recognition +serviceFullName: service/example-tfc/facial-recognition +serviceName: facial-recognition +serviceRegistries: [] +taskSets: [] +compatibilities: + - EC2 + - FARGATE +containerDefinitions: + - cpu: 1024 + environment: [] + essential: true + healthCheck: + command: + - CMD-SHELL + - wget -q --spider localhost:1234 + interval: 30 + retries: 3 + timeout: 5 + image: harshmanvar/face-detection-tensorjs:slim-amd + memory: 2048 + mountPoints: [] + name: facial-recognition + portMappings: + - appProtocol: http + containerPort: 1234 + hostPort: 1234 + protocol: tcp + systemControls: [] + volumesFrom: [] +cpu: "1024" +family: facial-recognition-tfc +ipcMode: "" +memory: "2048" +networkMode: awsvpc +pidMode: "" +registeredAt: 2024-08-01T15:27:30.781Z +registeredBy: arn:aws:sts::123456789:assumed-role/terraform-example/terraform-run-nKsGeEsVUcxfxEuY +requiresAttributes: + - name: com.amazonaws.ecs.capability.docker-remote-api.1.18 + targetType: "" + - name: com.amazonaws.ecs.capability.docker-remote-api.1.24 + targetType: "" + - name: ecs.capability.container-health-check + targetType: "" + - name: ecs.capability.task-eni + targetType: "" +requiresCompatibilities: + - FARGATE +revision: 1 +volumes: [] +attachments: + - details: + - name: macAddress + value: 0a:98:2a:a1:8c:cd + - name: networkInterfaceId + value: eni-0c99da7dff9025194 + - name: privateDnsName + value: ip-10-0-2-101.eu-west-2.compute.internal + - name: privateIPv4Address + value: 10.0.2.101 + - name: subnetId + value: subnet-0fafe900a3dc4ba78 + id: f2dc881c-d3c5-49ca-904e-30358f1675d8 + status: ATTACHED + type: ElasticNetworkInterface +attributes: + - name: ecs.cpu-architecture + targetType: "" + value: x86_64 +availabilityZone: eu-west-2b +capacityProviderName: FARGATE +connectivity: CONNECTED +connectivityAt: 2024-08-01T17:16:34.995Z +containers: + - containerArn: arn:aws:ecs:eu-west-2:123456789:container/example-tfc/ded4f8eebe4144ddb9a93a27b5661008/778778dd-3a31-44f0-a84f-42a2e75403d9 + cpu: "1024" + healthStatus: HEALTHY + image: harshmanvar/face-detection-tensorjs:slim-amd + imageDigest: sha256:a12d885a6d05efa01735e5dd60b2580eece2f21d962e38b9bbdf8cfeb81c6894 + lastStatus: RUNNING + memory: "2048" + name: facial-recognition + networkBindings: [] + networkInterfaces: + - attachmentId: f2dc881c-d3c5-49ca-904e-30358f1675d8 + runtimeId: ded4f8eebe4144ddb9a93a27b5661008-4091029319 +desiredStatus: RUNNING +ephemeralStorage: + sizeInGiB: 20 +fargateEphemeralStorage: + sizeInGiB: 20 +group: service:facial-recognition +healthStatus: HEALTHY +id: example-tfc/ded4f8eebe4144ddb9a93a27b5661008 +lastStatus: RUNNING +overrides: + containerOverrides: + - name: facial-recognition + inferenceAcceleratorOverrides: [] +pullStartedAt: 2024-08-01T17:18:22.901Z +pullStoppedAt: 2024-08-01T17:18:34.827Z +startedAt: 2024-08-01T17:18:48.139Z +startedBy: ecs-svc/5699741454300708027 +stopCode: "" +taskArn: arn:aws:ecs:eu-west-2:123456789:task/example-tfc/ded4f8eebe4144ddb9a93a27b5661008 +version: 5 +` + + mapData := make(map[string]interface{}) + _ = yaml.Unmarshal([]byte(yamlString), &mapData) + + attrs, _ := ToAttributes(mapData) + + return attrs, mapData +} + +// Current performance: +// BenchmarkExtractLinksFromAttributes-10 5676 193114 ns/op 58868 B/op 721 allocs/op +func BenchmarkExtractLinksFromAttributes(b *testing.B) { + attrs, _ := createTestData() + + for range b.N { + _ = ExtractLinksFromAttributes(attrs) + } +} + +// Current performance: +// BenchmarkExtractLinksFrom-10 2671 451209 ns/op 231509 B/op 4241 allocs/op +func BenchmarkExtractLinksFrom(b *testing.B) { + _, data := createTestData() + + for range b.N { + _, _ = ExtractLinksFrom(data) + } +} + +func TestExtractLinksFromAttributes(t *testing.T) { + attrs, _ := createTestData() + + queries := ExtractLinksFromAttributes(attrs) + + tests := []struct { + ExpectedType string + ExpectedQuery string + ExpectedScope string + }{ + // ARN edge cases - these should work after the fix + { + ExpectedType: "s3-bucket", + ExpectedQuery: "arn:aws:s3:::example-bucket-name", + ExpectedScope: "*", // S3 buckets don't have region/account in ARN, use wildcard + }, + { + ExpectedType: "s3-bucket", + ExpectedQuery: "arn:aws:s3:::my-test-bucket", + ExpectedScope: "*", + }, + { + ExpectedType: "iam-role", + ExpectedQuery: "arn:aws:iam::123456789012:role/example-role", + ExpectedScope: "123456789012", // IAM is account-scoped, no region + }, + { + ExpectedType: "cloudfront-distribution", + ExpectedQuery: "arn:aws:cloudfront::123456789012:distribution/EDFDVBDEXAMPLE", + ExpectedScope: "123456789012", // CloudFront is account-scoped, no region + }, + { + ExpectedType: "ip", + ExpectedQuery: "2a05:d01c:40:7600::6c81", + }, + { + ExpectedType: "ip", + ExpectedQuery: "2a05:d01c:40:7600:fbac::3", + }, + { + ExpectedType: "ip", + ExpectedQuery: "2a05:d01c:40:7600:fbac::4", + }, + { + ExpectedType: "ip", + ExpectedQuery: "10.0.2.101", + }, + { + ExpectedType: "ip", + ExpectedQuery: "fdb4:5627:96ee::bfa3", + }, + { + ExpectedType: "http", + ExpectedQuery: "https://api.example.com", + }, + { + ExpectedType: "http", + ExpectedQuery: "https://domain.eu.auth0.com/oauth/token", + }, + { + ExpectedType: "dns", + ExpectedQuery: "domain.eu.auth0.com", + }, + { + ExpectedType: "http", + ExpectedQuery: "http://service:8080/api", + }, + { + ExpectedType: "http", + ExpectedQuery: "https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/123456789/ingress_log-dd9f17f1-0694-4e79-9855-300a7f9b7300?versionId=sxmueRdM4S.HXwlebzlb7rOpD2ZHmS3Z&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMiJGMEQCIBWNIrhNoAqNUG%2BoZLmKNSxY9ncDogcyFTGeJFef0zVMAiBhkAW9JWxVna%2FoCXe4u9S3364dCavXEvZP%2FXcD6iwfISq7BQiR%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDQ3MjAzODg2NDE4OC", + }, + { + ExpectedType: "http", + ExpectedQuery: "https://api.honeycomb.io", + }, + { + ExpectedType: "dns", + ExpectedQuery: "ip-10-0-2-101.eu-west-2.compute.internal", + }, + { + ExpectedType: "dns", + ExpectedQuery: "ip-10-0-4-118.eu-west-2.compute.internal", + }, + { + ExpectedType: "dns", + ExpectedQuery: "name.cluster-id.eu-west-2.rds.amazonaws.com", + }, + { + ExpectedType: "lambda-function", + ExpectedQuery: "arn:aws:lambda:eu-west-2:123456789:function:ingress_log", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "ecs-cluster", + ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:cluster/example-tfc", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "ecs-container", + ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:container/example-tfc/ded4f8eebe4144ddb9a93a27b5661008/778778dd-3a31-44f0-a84f-42a2e75403d9", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "ecs-service", + ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:service/example-tfc/facial-recognition", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "ecs-task-definition", + ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:task-definition/facial-recognition-tfc:1", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "ecs-task", + ExpectedQuery: "arn:aws:ecs:eu-west-2:123456789:task/example-tfc/ded4f8eebe4144ddb9a93a27b5661008", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "elasticloadbalancing-targetgroup", + ExpectedQuery: "arn:aws:elasticloadbalancing:eu-west-2:123456789:targetgroup/facerec-tfc/0b6a17c7de07be40", + ExpectedScope: "123456789.eu-west-2", + }, + { + ExpectedType: "iam-role", + ExpectedQuery: "arn:aws:iam::123456789:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS", + ExpectedScope: "123456789", + }, + { + ExpectedType: "iam-role", + ExpectedQuery: "arn:aws:iam::123456789:role/ingress_log", + ExpectedScope: "123456789", + }, + { + ExpectedType: "iam-role", + ExpectedQuery: "arn:aws:iam::123456789:role/terraform-example", + ExpectedScope: "123456789", + }, + { + ExpectedType: "sts-assumed-role", + ExpectedQuery: "arn:aws:sts::123456789:assumed-role/terraform-example/terraform-run-nKsGeEsVUcxfxEuY", + ExpectedScope: "123456789", + }, + } + + // Note: We don't check length anymore since we added new test cases + // that may result in more extracted queries than we have tests for + + for _, test := range tests { + found := false + for _, query := range queries { + if query.GetQuery().GetQuery() == test.ExpectedQuery && query.GetQuery().GetType() == test.ExpectedType { + if test.ExpectedScope == "" { + // If we don't care about the scope then it's a match + found = true + break + } else { + // If we do care about the scope then check that it matches + if query.GetQuery().GetScope() == test.ExpectedScope { + found = true + break + } + } + } + } + + if !found { + t.Errorf("expected query not found: %s %s", test.ExpectedType, test.ExpectedQuery) + } + } +} + +func TestExtractLinksFrom(t *testing.T) { + tests := []struct { + Name string + Object interface{} + ExpectedQueries []string + }{ + { + Name: "Env var structure array", + Object: []struct { + Name string + Value string + }{ + { + Name: "example", + Value: "https://example.com", + }, + }, + ExpectedQueries: []string{"https://example.com"}, + }, + { + Name: "Just a raw string", + Object: "https://example.com", + ExpectedQueries: []string{"https://example.com"}, + }, + { + Name: "Nil", + Object: nil, + ExpectedQueries: []string{}, + }, + { + Name: "Struct", + Object: struct { + Name string + Value string + }{ + Name: "example", + Value: "https://example.com", + }, + ExpectedQueries: []string{"https://example.com"}, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + queries, err := ExtractLinksFrom(test.Object) + if err != nil { + t.Fatal(err) + } + + if len(queries) != len(test.ExpectedQueries) { + t.Errorf("expected %d queries, got %d", len(test.ExpectedQueries), len(queries)) + } + + for i, query := range queries { + if query.GetQuery().GetQuery() != test.ExpectedQueries[i] { + t.Errorf("expected query %s, got %s", test.ExpectedQueries[i], query.GetQuery().GetQuery()) + } + } + }) + } +} + +func TestExtractLinksFromConfigMapData(t *testing.T) { + // Test ConfigMap data with S3 bucket ARN + configMapData := map[string]interface{}{ + "data": map[string]interface{}{ + "S3_BUCKET_ARN": "arn:aws:s3:::example-bucket-name", + "S3_BUCKET_NAME": "example-bucket-name", + }, + } + + queries, err := ExtractLinksFrom(configMapData) + if err != nil { + t.Fatal(err) + } + + // Find the S3 bucket query + found := false + for _, query := range queries { + if query.GetQuery().GetType() == "s3-bucket" && + query.GetQuery().GetQuery() == "arn:aws:s3:::example-bucket-name" { + found = true + if query.GetQuery().GetScope() != WILDCARD { + t.Errorf("expected scope to be WILDCARD (%s), got %s", WILDCARD, query.GetQuery().GetScope()) + } + if query.GetQuery().GetMethod() != QueryMethod_SEARCH { + t.Errorf("expected method to be SEARCH, got %v", query.GetQuery().GetMethod()) + } + break + } + } + + if !found { + t.Errorf("expected to find s3-bucket query for ARN arn:aws:s3:::example-bucket-name") + t.Logf("Found %d queries:", len(queries)) + for _, q := range queries { + t.Logf(" Type: %s, Query: %s, Scope: %s", q.GetQuery().GetType(), q.GetQuery().GetQuery(), q.GetQuery().GetScope()) + } + } +} + +func TestS3BucketARNTypeDetection(t *testing.T) { + tests := []struct { + name string + arn string + expectedType string + expectedQuery string + expectedScope string + }{ + { + name: "S3 bucket ARN without account/region", + arn: "arn:aws:s3:::example-bucket-name", + expectedType: "s3-bucket", + expectedQuery: "arn:aws:s3:::example-bucket-name", + expectedScope: WILDCARD, + }, + { + name: "S3 bucket ARN with short name", + arn: "arn:aws:s3:::my-bucket", + expectedType: "s3-bucket", + expectedQuery: "arn:aws:s3:::my-bucket", + expectedScope: WILDCARD, + }, + { + name: "S3 object ARN (should extract bucket)", + arn: "arn:aws:s3:::my-bucket/path/to/object", + expectedType: "s3-bucket", + expectedQuery: "arn:aws:s3:::my-bucket", + expectedScope: WILDCARD, + }, + { + name: "S3 object ARN with nested path", + arn: "arn:aws:s3:::my-bucket/folder/subfolder/file.txt", + expectedType: "s3-bucket", + expectedQuery: "arn:aws:s3:::my-bucket", + expectedScope: WILDCARD, + }, + { + name: "S3 bucket ARN with hyphens in name", + arn: "arn:aws:s3:::my-test-bucket-name", + expectedType: "s3-bucket", + expectedQuery: "arn:aws:s3:::my-test-bucket-name", + expectedScope: WILDCARD, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + queries, err := ExtractLinksFrom(map[string]interface{}{ + "arn": tt.arn, + }) + if err != nil { + t.Fatal(err) + } + + found := false + for _, query := range queries { + if query.GetQuery().GetType() == tt.expectedType && + query.GetQuery().GetQuery() == tt.expectedQuery { + found = true + if query.GetQuery().GetScope() != tt.expectedScope { + t.Errorf("expected scope %s, got %s", tt.expectedScope, query.GetQuery().GetScope()) + } + if query.GetQuery().GetMethod() != QueryMethod_SEARCH { + t.Errorf("expected method SEARCH, got %v", query.GetQuery().GetMethod()) + } + break + } + } + + if !found { + t.Errorf("expected to find query with type %s and query %s", tt.expectedType, tt.expectedQuery) + t.Logf("Found %d queries:", len(queries)) + for _, q := range queries { + t.Logf(" Type: %s, Query: %s, Scope: %s", q.GetQuery().GetType(), q.GetQuery().GetQuery(), q.GetQuery().GetScope()) + } + } + }) + } +} diff --git a/go/sdp-go/logs.go b/go/sdp-go/logs.go new file mode 100644 index 00000000..aa012b4d --- /dev/null +++ b/go/sdp-go/logs.go @@ -0,0 +1,77 @@ +package sdp + +import ( + "errors" + "fmt" + "strings" + + "connectrpc.com/connect" +) + +// Validate ensures that GetLogRecordsRequest is valid +func (req *GetLogRecordsRequest) Validate() error { + if req == nil { + return errors.New("GetLogRecordsRequest is nil") + } + + // scope has to be non-nil, non-empty string + if strings.TrimSpace(req.GetScope()) == "" { + return errors.New("scope has to be non-empty") + } + + // query has to be non-nil, non-empty string + if strings.TrimSpace(req.GetQuery()) == "" { + return errors.New("query has to be non-empty") + } + + // from and to have to be valid timestamps + if req.GetFrom() == nil { + return errors.New("from timestamp is required") + } + + if req.GetTo() == nil { + return errors.New("to timestamp is required") + } + + // from has to be before or equal to to + fromTime := req.GetFrom().AsTime() + toTime := req.GetTo().AsTime() + if fromTime.After(toTime) { + return fmt.Errorf("from timestamp (%v) must be before or equal to to timestamp (%v)", fromTime, toTime) + } + + if req.GetMaxRecords() < 0 { + return errors.New("maxRecords must be greater than or equal to zero") + } + + return nil +} + +// NewUpstreamSourceError creates a new SourceError with the given message and error +func NewUpstreamSourceError(code connect.Code, message string) *SourceError { + return &SourceError{ + Code: SourceError_Code(code), //nolint:gosec + Message: message, + Upstream: true, + } +} + +// NewLocalSourceError creates a new SourceError with the given message and error, indicating a local (non-upstream) error +func NewLocalSourceError(code connect.Code, message string) *SourceError { + return &SourceError{ + Code: SourceError_Code(code), //nolint:gosec + Message: message, + Upstream: false, + } +} + +// assert interface implementation +var _ error = (*SourceError)(nil) + +// Error implements the error interface for SourceError +func (e *SourceError) Error() string { + if e.GetUpstream() { + return fmt.Sprintf("Upstream Error: %s", e.GetMessage()) + } + return fmt.Sprintf("Source Error: %s", e.GetMessage()) +} diff --git a/go/sdp-go/logs.pb.go b/go/sdp-go/logs.pb.go new file mode 100644 index 00000000..1d3a2d54 --- /dev/null +++ b/go/sdp-go/logs.pb.go @@ -0,0 +1,1083 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: logs.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// These mirror the OpenTelemetry log severity levels +// https://opentelemetry.io/docs/specs/otel/logs/data-model/#displaying-severity +// Refer to the OpenTelemetry documentation for information on how these should +// be mapped +type LogSeverity int32 + +const ( + LogSeverity_UNSPECIFIED LogSeverity = 0 + LogSeverity_TRACE LogSeverity = 1 + LogSeverity_TRACE2 LogSeverity = 2 + LogSeverity_TRACE3 LogSeverity = 3 + LogSeverity_TRACE4 LogSeverity = 4 + LogSeverity_DEBUG LogSeverity = 5 + LogSeverity_DEBUG2 LogSeverity = 6 + LogSeverity_DEBUG3 LogSeverity = 7 + LogSeverity_DEBUG4 LogSeverity = 8 + LogSeverity_INFO LogSeverity = 9 + LogSeverity_INFO2 LogSeverity = 10 + LogSeverity_INFO3 LogSeverity = 11 + LogSeverity_INFO4 LogSeverity = 12 + LogSeverity_WARN LogSeverity = 13 + LogSeverity_WARN2 LogSeverity = 14 + LogSeverity_WARN3 LogSeverity = 15 + LogSeverity_WARN4 LogSeverity = 16 + LogSeverity_ERROR LogSeverity = 17 + LogSeverity_ERROR2 LogSeverity = 18 + LogSeverity_ERROR3 LogSeverity = 19 + LogSeverity_ERROR4 LogSeverity = 20 + LogSeverity_FATAL LogSeverity = 21 + LogSeverity_FATAL2 LogSeverity = 22 + LogSeverity_FATAL3 LogSeverity = 23 + LogSeverity_FATAL4 LogSeverity = 24 +) + +// Enum value maps for LogSeverity. +var ( + LogSeverity_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "TRACE", + 2: "TRACE2", + 3: "TRACE3", + 4: "TRACE4", + 5: "DEBUG", + 6: "DEBUG2", + 7: "DEBUG3", + 8: "DEBUG4", + 9: "INFO", + 10: "INFO2", + 11: "INFO3", + 12: "INFO4", + 13: "WARN", + 14: "WARN2", + 15: "WARN3", + 16: "WARN4", + 17: "ERROR", + 18: "ERROR2", + 19: "ERROR3", + 20: "ERROR4", + 21: "FATAL", + 22: "FATAL2", + 23: "FATAL3", + 24: "FATAL4", + } + LogSeverity_value = map[string]int32{ + "UNSPECIFIED": 0, + "TRACE": 1, + "TRACE2": 2, + "TRACE3": 3, + "TRACE4": 4, + "DEBUG": 5, + "DEBUG2": 6, + "DEBUG3": 7, + "DEBUG4": 8, + "INFO": 9, + "INFO2": 10, + "INFO3": 11, + "INFO4": 12, + "WARN": 13, + "WARN2": 14, + "WARN3": 15, + "WARN4": 16, + "ERROR": 17, + "ERROR2": 18, + "ERROR3": 19, + "ERROR4": 20, + "FATAL": 21, + "FATAL2": 22, + "FATAL3": 23, + "FATAL4": 24, + } +) + +func (x LogSeverity) Enum() *LogSeverity { + p := new(LogSeverity) + *p = x + return p +} + +func (x LogSeverity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LogSeverity) Descriptor() protoreflect.EnumDescriptor { + return file_logs_proto_enumTypes[0].Descriptor() +} + +func (LogSeverity) Type() protoreflect.EnumType { + return &file_logs_proto_enumTypes[0] +} + +func (x LogSeverity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LogSeverity.Descriptor instead. +func (LogSeverity) EnumDescriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{0} +} + +type NATSGetLogRecordsResponseStatus_Status int32 + +const ( + NATSGetLogRecordsResponseStatus_UNSPECIFIED NATSGetLogRecordsResponseStatus_Status = 0 + // The source has started processing the request. + NATSGetLogRecordsResponseStatus_STARTED NATSGetLogRecordsResponseStatus_Status = 1 + // The source has finished processing the request. No further messages will + // be sent after this. + NATSGetLogRecordsResponseStatus_FINISHED NATSGetLogRecordsResponseStatus_Status = 2 + // The source encountered an error while processing the request. No further + // messages will be sent after this. + NATSGetLogRecordsResponseStatus_ERRORED NATSGetLogRecordsResponseStatus_Status = 3 +) + +// Enum value maps for NATSGetLogRecordsResponseStatus_Status. +var ( + NATSGetLogRecordsResponseStatus_Status_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "STARTED", + 2: "FINISHED", + 3: "ERRORED", + } + NATSGetLogRecordsResponseStatus_Status_value = map[string]int32{ + "UNSPECIFIED": 0, + "STARTED": 1, + "FINISHED": 2, + "ERRORED": 3, + } +) + +func (x NATSGetLogRecordsResponseStatus_Status) Enum() *NATSGetLogRecordsResponseStatus_Status { + p := new(NATSGetLogRecordsResponseStatus_Status) + *p = x + return p +} + +func (x NATSGetLogRecordsResponseStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NATSGetLogRecordsResponseStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_logs_proto_enumTypes[1].Descriptor() +} + +func (NATSGetLogRecordsResponseStatus_Status) Type() protoreflect.EnumType { + return &file_logs_proto_enumTypes[1] +} + +func (x NATSGetLogRecordsResponseStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NATSGetLogRecordsResponseStatus_Status.Descriptor instead. +func (NATSGetLogRecordsResponseStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{5, 0} +} + +// This directly mirrors the Connect RPC codes that can be found here: +// https://connectrpc.com/docs/protocol/#error-codes +type SourceError_Code int32 + +const ( + // This is the default value and should not be used. In connectrpc, this + // is "OK", but is not actually written out because _go_. + SourceError_UNSPECIFIED SourceError_Code = 0 + // CodeCanceled indicates that the operation was canceled, typically by the + // caller. + // + // HTTP Code: 499 Client Closed Request + SourceError_CANCELED SourceError_Code = 1 + // CodeUnknown indicates that the operation failed for an unknown reason. + // + // HTTP Code: 500 Internal Server Error + SourceError_UNKNOWN SourceError_Code = 2 + // CodeInvalidArgument indicates that client supplied an invalid argument. + // + // HTTP Code: 400 Bad Request + SourceError_INVALID_ARGUMENT SourceError_Code = 3 + // CodeDeadlineExceeded indicates that deadline expired before the operation + // could complete. + // + // HTTP Code: 504 Gateway Timeout + SourceError_DEADLINE_EXCEEDED SourceError_Code = 4 + // CodeNotFound indicates that some requested entity (for example, a file or + // directory) was not found. + // + // HTTP Code: 404 Not Found + SourceError_NOT_FOUND SourceError_Code = 5 + // CodeAlreadyExists indicates that client attempted to create an entity (for + // example, a file or directory) that already exists. + // + // HTTP Code: 409 Conflict + SourceError_ALREADY_EXISTS SourceError_Code = 6 + // CodePermissionDenied indicates that the caller doesn't have permission to + // execute the specified operation. + // + // HTTP Code: 403 Forbidden + SourceError_PERMISSION_DENIED SourceError_Code = 7 + // CodeResourceExhausted indicates that some resource has been exhausted. For + // example, a per-user quota may be exhausted or the entire file system may + // be full. + // + // HTTP Code: 429 Too Many Requests + SourceError_RESOURCE_EXHAUSTED SourceError_Code = 8 + // CodeFailedPrecondition indicates that the system is not in a state + // required for the operation's execution. + // + // HTTP Code: 400 Bad Request + SourceError_FAILED_PRECONDITION SourceError_Code = 9 + // CodeAborted indicates that operation was aborted by the system, usually + // because of a concurrency issue such as a sequencer check failure or + // transaction abort. + // + // HTTP Code: 409 Conflict + SourceError_ABORTED SourceError_Code = 10 + // CodeOutOfRange indicates that the operation was attempted past the valid + // range (for example, seeking past end-of-file). + // + // HTTP Code: 400 Bad Request + SourceError_OUT_OF_RANGE SourceError_Code = 11 + // CodeUnimplemented indicates that the operation isn't implemented, + // supported, or enabled in this service. + // + // HTTP Code: 501 Not Implemented + SourceError_UNIMPLEMENTED SourceError_Code = 12 + // CodeInternal indicates that some invariants expected by the underlying + // system have been broken. This code is reserved for serious errors. + // + // HTTP Code: 500 Internal Server Error + SourceError_INTERNAL SourceError_Code = 13 + // CodeUnavailable indicates that the service is currently unavailable. This + // is usually temporary, so clients can back off and retry idempotent + // operations. + // + // HTTP Code: 503 Service Unavailable + SourceError_UNAVAILABLE SourceError_Code = 14 + // CodeDataLoss indicates that the operation has resulted in unrecoverable + // data loss or corruption. + // + // HTTP Code: 500 Internal Server Error + SourceError_DATA_LOSS SourceError_Code = 15 + // CodeUnauthenticated indicates that the request does not have valid + // authentication credentials for the operation. + // + // HTTP Code: 401 Unauthorized + SourceError_UNAUTHENTICATED SourceError_Code = 16 +) + +// Enum value maps for SourceError_Code. +var ( + SourceError_Code_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "CANCELED", + 2: "UNKNOWN", + 3: "INVALID_ARGUMENT", + 4: "DEADLINE_EXCEEDED", + 5: "NOT_FOUND", + 6: "ALREADY_EXISTS", + 7: "PERMISSION_DENIED", + 8: "RESOURCE_EXHAUSTED", + 9: "FAILED_PRECONDITION", + 10: "ABORTED", + 11: "OUT_OF_RANGE", + 12: "UNIMPLEMENTED", + 13: "INTERNAL", + 14: "UNAVAILABLE", + 15: "DATA_LOSS", + 16: "UNAUTHENTICATED", + } + SourceError_Code_value = map[string]int32{ + "UNSPECIFIED": 0, + "CANCELED": 1, + "UNKNOWN": 2, + "INVALID_ARGUMENT": 3, + "DEADLINE_EXCEEDED": 4, + "NOT_FOUND": 5, + "ALREADY_EXISTS": 6, + "PERMISSION_DENIED": 7, + "RESOURCE_EXHAUSTED": 8, + "FAILED_PRECONDITION": 9, + "ABORTED": 10, + "OUT_OF_RANGE": 11, + "UNIMPLEMENTED": 12, + "INTERNAL": 13, + "UNAVAILABLE": 14, + "DATA_LOSS": 15, + "UNAUTHENTICATED": 16, + } +) + +func (x SourceError_Code) Enum() *SourceError_Code { + p := new(SourceError_Code) + *p = x + return p +} + +func (x SourceError_Code) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SourceError_Code) Descriptor() protoreflect.EnumDescriptor { + return file_logs_proto_enumTypes[2].Descriptor() +} + +func (SourceError_Code) Type() protoreflect.EnumType { + return &file_logs_proto_enumTypes[2] +} + +func (x SourceError_Code) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SourceError_Code.Descriptor instead. +func (SourceError_Code) EnumDescriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{6, 0} +} + +// The request to get log records from the upstream API. +type GetLogRecordsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The scope of the logs to get. This comes from the item that the LogStream + // was attached to and ensures that the `NATSGetLogRecordsRequest` is + // received by the same source that sent the item in the first place + Scope string `protobuf:"bytes,1,opt,name=scope,proto3" json:"scope,omitempty"` + // The query that was provided in the `LogStreamDetails` . The format of this + // is determined by the source, and will contain enough information for the + // source to successfully query the upstream API that contains the logs + Query string `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + // The start point for the logs + From *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"` + // The end point for the logs + To *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"` + // The maximum number of records to return. Set to zero (`0`) to return all. + MaxRecords int32 `protobuf:"varint,5,opt,name=maxRecords,proto3" json:"maxRecords,omitempty"` + // If the value is true, the earliest log events are returned first. If the + // value is false, the latest log events are returned first. The default + // value is false. + StartFromOldest bool `protobuf:"varint,6,opt,name=startFromOldest,proto3" json:"startFromOldest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLogRecordsRequest) Reset() { + *x = GetLogRecordsRequest{} + mi := &file_logs_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLogRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLogRecordsRequest) ProtoMessage() {} + +func (x *GetLogRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[0] + 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 GetLogRecordsRequest.ProtoReflect.Descriptor instead. +func (*GetLogRecordsRequest) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{0} +} + +func (x *GetLogRecordsRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *GetLogRecordsRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *GetLogRecordsRequest) GetFrom() *timestamppb.Timestamp { + if x != nil { + return x.From + } + return nil +} + +func (x *GetLogRecordsRequest) GetTo() *timestamppb.Timestamp { + if x != nil { + return x.To + } + return nil +} + +func (x *GetLogRecordsRequest) GetMaxRecords() int32 { + if x != nil { + return x.MaxRecords + } + return 0 +} + +func (x *GetLogRecordsRequest) GetStartFromOldest() bool { + if x != nil { + return x.StartFromOldest + } + return false +} + +// Each chunk is gonna be a page of the underlying APIs pagination. +// The source is expected to use sane defaults within the limits of the +// underlying API and SDP capabilities (message size, etc). +// +// This chunking can also be re-used for live streaming in the future. +// +// Note that the results are expected to be returned in ascending (oldest +// to newest) order. +type GetLogRecordsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Records []*LogRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLogRecordsResponse) Reset() { + *x = GetLogRecordsResponse{} + mi := &file_logs_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLogRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLogRecordsResponse) ProtoMessage() {} + +func (x *GetLogRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[1] + 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 GetLogRecordsResponse.ProtoReflect.Descriptor instead. +func (*GetLogRecordsResponse) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{1} +} + +func (x *GetLogRecordsResponse) GetRecords() []*LogRecord { + if x != nil { + return x.Records + } + return nil +} + +// Represents a single entry in a LogStream. Roughly a "line" in traditional +// terms, but nowadays often with more details, additional structure, etc. +// +// This is chiefly modelled on the OpenTelemetry log data model: +// https://opentelemetry.io/docs/specs/otel/logs/data-model/ +type LogRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + // "Time when the event occurred measured by the origin clock, i.e. the time + // at the source." + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-timestamp + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=createdAt,proto3,oneof" json:"createdAt,omitempty"` + // "Time when the event was observed by the collection system." + // This can be used if no `createdAt` timestamp is available. + // Client libraries are encouraged to provide a singular getter that returns + // our best guess for ease of use: createdAt if available, else observedAt. + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-observedtimestamp + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=observedAt,proto3,oneof" json:"observedAt,omitempty"` + // See the definitions in + // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber + // Each source should document its mapping to this standard, e.g. following + // the examples in + // https://opentelemetry.io/docs/specs/otel/logs/data-model-appendix/#appendix-b-severitynumber-example-mappings + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber + Severity LogSeverity `protobuf:"varint,3,opt,name=severity,proto3,enum=logs.LogSeverity" json:"severity,omitempty"` + // the string form of the `body`. Can be empty if the upstream API only + // provides structured records. A Source can decide in this case to render a + // default field here if available. + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-body + Body string `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` + // "Describes the source of the log", as provided by the upstream API. + // This is arbitrary metadata from the upstream API as interpreted by the + // source. May be empty. Should use standard OTel attribute names where + // applicable. + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-resource + Resource *structpb.Struct `protobuf:"bytes,5,opt,name=resource,proto3,oneof" json:"resource,omitempty"` + // "Additional information about the specific event occurrence." This is + // arbitrary metadata from the upstream API as interpreted by the source. + // May be empty, may contain error and exception attributes. + // + // See https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-attributes + Attributes *structpb.Struct `protobuf:"bytes,6,opt,name=attributes,proto3,oneof" json:"attributes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogRecord) Reset() { + *x = LogRecord{} + mi := &file_logs_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogRecord) ProtoMessage() {} + +func (x *LogRecord) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[2] + 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 LogRecord.ProtoReflect.Descriptor instead. +func (*LogRecord) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{2} +} + +func (x *LogRecord) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *LogRecord) GetObservedAt() *timestamppb.Timestamp { + if x != nil { + return x.ObservedAt + } + return nil +} + +func (x *LogRecord) GetSeverity() LogSeverity { + if x != nil { + return x.Severity + } + return LogSeverity_UNSPECIFIED +} + +func (x *LogRecord) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *LogRecord) GetResource() *structpb.Struct { + if x != nil { + return x.Resource + } + return nil +} + +func (x *LogRecord) GetAttributes() *structpb.Struct { + if x != nil { + return x.Attributes + } + return nil +} + +// A quick passthrough to keep the NATS message format consistent. +type NATSGetLogRecordsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Request *GetLogRecordsRequest `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NATSGetLogRecordsRequest) Reset() { + *x = NATSGetLogRecordsRequest{} + mi := &file_logs_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NATSGetLogRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NATSGetLogRecordsRequest) ProtoMessage() {} + +func (x *NATSGetLogRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[3] + 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 NATSGetLogRecordsRequest.ProtoReflect.Descriptor instead. +func (*NATSGetLogRecordsRequest) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{3} +} + +func (x *NATSGetLogRecordsRequest) GetRequest() *GetLogRecordsRequest { + if x != nil { + return x.Request + } + return nil +} + +type NATSGetLogRecordsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Content: + // + // *NATSGetLogRecordsResponse_Status + // *NATSGetLogRecordsResponse_Response + Content isNATSGetLogRecordsResponse_Content `protobuf_oneof:"content"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NATSGetLogRecordsResponse) Reset() { + *x = NATSGetLogRecordsResponse{} + mi := &file_logs_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NATSGetLogRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NATSGetLogRecordsResponse) ProtoMessage() {} + +func (x *NATSGetLogRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[4] + 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 NATSGetLogRecordsResponse.ProtoReflect.Descriptor instead. +func (*NATSGetLogRecordsResponse) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{4} +} + +func (x *NATSGetLogRecordsResponse) GetContent() isNATSGetLogRecordsResponse_Content { + if x != nil { + return x.Content + } + return nil +} + +func (x *NATSGetLogRecordsResponse) GetStatus() *NATSGetLogRecordsResponseStatus { + if x != nil { + if x, ok := x.Content.(*NATSGetLogRecordsResponse_Status); ok { + return x.Status + } + } + return nil +} + +func (x *NATSGetLogRecordsResponse) GetResponse() *GetLogRecordsResponse { + if x != nil { + if x, ok := x.Content.(*NATSGetLogRecordsResponse_Response); ok { + return x.Response + } + } + return nil +} + +type isNATSGetLogRecordsResponse_Content interface { + isNATSGetLogRecordsResponse_Content() +} + +type NATSGetLogRecordsResponse_Status struct { + // The status of the request. This is sent before any log records are + // sent, and then if an error occurs, or the request is finished. This + // provides signalling of the "method call" over NATS. + Status *NATSGetLogRecordsResponseStatus `protobuf:"bytes,1,opt,name=status,proto3,oneof"` +} + +type NATSGetLogRecordsResponse_Response struct { + // A set of log records (lines). These should be batched in whatever way + // that the upstream provider batches them. For example if the API that + // you are pulling the logs from returns them in pages of 50, then you + // should return 50 log records in each response, and send the response + // on before requesting the next page from the API. + Response *GetLogRecordsResponse `protobuf:"bytes,2,opt,name=response,proto3,oneof"` +} + +func (*NATSGetLogRecordsResponse_Status) isNATSGetLogRecordsResponse_Content() {} + +func (*NATSGetLogRecordsResponse_Response) isNATSGetLogRecordsResponse_Content() {} + +type NATSGetLogRecordsResponseStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status NATSGetLogRecordsResponseStatus_Status `protobuf:"varint,1,opt,name=status,proto3,enum=logs.NATSGetLogRecordsResponseStatus_Status" json:"status,omitempty"` + // Only populated when the status is ERRORED + Error *SourceError `protobuf:"bytes,2,opt,name=error,proto3,oneof" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NATSGetLogRecordsResponseStatus) Reset() { + *x = NATSGetLogRecordsResponseStatus{} + mi := &file_logs_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NATSGetLogRecordsResponseStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NATSGetLogRecordsResponseStatus) ProtoMessage() {} + +func (x *NATSGetLogRecordsResponseStatus) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[5] + 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 NATSGetLogRecordsResponseStatus.ProtoReflect.Descriptor instead. +func (*NATSGetLogRecordsResponseStatus) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{5} +} + +func (x *NATSGetLogRecordsResponseStatus) GetStatus() NATSGetLogRecordsResponseStatus_Status { + if x != nil { + return x.Status + } + return NATSGetLogRecordsResponseStatus_UNSPECIFIED +} + +func (x *NATSGetLogRecordsResponseStatus) GetError() *SourceError { + if x != nil { + return x.Error + } + return nil +} + +type SourceError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The error code + Code SourceError_Code `protobuf:"varint,1,opt,name=code,proto3,enum=logs.SourceError_Code" json:"code,omitempty"` + // The error message + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Whether this error comes from the upstream API or not. Errors that come + // from the upstream API will result in the user-facing RPC returning a + // `code.Aborted` error, with the `NatsError` embedded in the `Detail` + // field. This differentiates between errors that were part of Overmind + // (like the source panicking) and errors that come from the upstream (like + // Datadog having an outage) + Upstream bool `protobuf:"varint,3,opt,name=upstream,proto3" json:"upstream,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceError) Reset() { + *x = SourceError{} + mi := &file_logs_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceError) ProtoMessage() {} + +func (x *SourceError) ProtoReflect() protoreflect.Message { + mi := &file_logs_proto_msgTypes[6] + 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 SourceError.ProtoReflect.Descriptor instead. +func (*SourceError) Descriptor() ([]byte, []int) { + return file_logs_proto_rawDescGZIP(), []int{6} +} + +func (x *SourceError) GetCode() SourceError_Code { + if x != nil { + return x.Code + } + return SourceError_UNSPECIFIED +} + +func (x *SourceError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SourceError) GetUpstream() bool { + if x != nil { + return x.Upstream + } + return false +} + +var File_logs_proto protoreflect.FileDescriptor + +const file_logs_proto_rawDesc = "" + + "\n" + + "\n" + + "logs.proto\x12\x04logs\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe8\x01\n" + + "\x14GetLogRecordsRequest\x12\x14\n" + + "\x05scope\x18\x01 \x01(\tR\x05scope\x12\x14\n" + + "\x05query\x18\x02 \x01(\tR\x05query\x12.\n" + + "\x04from\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04from\x12*\n" + + "\x02to\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x02to\x12\x1e\n" + + "\n" + + "maxRecords\x18\x05 \x01(\x05R\n" + + "maxRecords\x12(\n" + + "\x0fstartFromOldest\x18\x06 \x01(\bR\x0fstartFromOldest\"B\n" + + "\x15GetLogRecordsResponse\x12)\n" + + "\arecords\x18\x01 \x03(\v2\x0f.logs.LogRecordR\arecords\"\xff\x02\n" + + "\tLogRecord\x12=\n" + + "\tcreatedAt\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampH\x00R\tcreatedAt\x88\x01\x01\x12?\n" + + "\n" + + "observedAt\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + + "observedAt\x88\x01\x01\x12-\n" + + "\bseverity\x18\x03 \x01(\x0e2\x11.logs.LogSeverityR\bseverity\x12\x12\n" + + "\x04body\x18\x04 \x01(\tR\x04body\x128\n" + + "\bresource\x18\x05 \x01(\v2\x17.google.protobuf.StructH\x02R\bresource\x88\x01\x01\x12<\n" + + "\n" + + "attributes\x18\x06 \x01(\v2\x17.google.protobuf.StructH\x03R\n" + + "attributes\x88\x01\x01B\f\n" + + "\n" + + "_createdAtB\r\n" + + "\v_observedAtB\v\n" + + "\t_resourceB\r\n" + + "\v_attributes\"P\n" + + "\x18NATSGetLogRecordsRequest\x124\n" + + "\arequest\x18\x01 \x01(\v2\x1a.logs.GetLogRecordsRequestR\arequest\"\xa2\x01\n" + + "\x19NATSGetLogRecordsResponse\x12?\n" + + "\x06status\x18\x01 \x01(\v2%.logs.NATSGetLogRecordsResponseStatusH\x00R\x06status\x129\n" + + "\bresponse\x18\x02 \x01(\v2\x1b.logs.GetLogRecordsResponseH\x00R\bresponseB\t\n" + + "\acontent\"\xe2\x01\n" + + "\x1fNATSGetLogRecordsResponseStatus\x12D\n" + + "\x06status\x18\x01 \x01(\x0e2,.logs.NATSGetLogRecordsResponseStatus.StatusR\x06status\x12,\n" + + "\x05error\x18\x02 \x01(\v2\x11.logs.SourceErrorH\x00R\x05error\x88\x01\x01\"A\n" + + "\x06Status\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\v\n" + + "\aSTARTED\x10\x01\x12\f\n" + + "\bFINISHED\x10\x02\x12\v\n" + + "\aERRORED\x10\x03B\b\n" + + "\x06_error\"\xb1\x03\n" + + "\vSourceError\x12*\n" + + "\x04code\x18\x01 \x01(\x0e2\x16.logs.SourceError.CodeR\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x1a\n" + + "\bupstream\x18\x03 \x01(\bR\bupstream\"\xbf\x02\n" + + "\x04Code\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\f\n" + + "\bCANCELED\x10\x01\x12\v\n" + + "\aUNKNOWN\x10\x02\x12\x14\n" + + "\x10INVALID_ARGUMENT\x10\x03\x12\x15\n" + + "\x11DEADLINE_EXCEEDED\x10\x04\x12\r\n" + + "\tNOT_FOUND\x10\x05\x12\x12\n" + + "\x0eALREADY_EXISTS\x10\x06\x12\x15\n" + + "\x11PERMISSION_DENIED\x10\a\x12\x16\n" + + "\x12RESOURCE_EXHAUSTED\x10\b\x12\x17\n" + + "\x13FAILED_PRECONDITION\x10\t\x12\v\n" + + "\aABORTED\x10\n" + + "\x12\x10\n" + + "\fOUT_OF_RANGE\x10\v\x12\x11\n" + + "\rUNIMPLEMENTED\x10\f\x12\f\n" + + "\bINTERNAL\x10\r\x12\x0f\n" + + "\vUNAVAILABLE\x10\x0e\x12\r\n" + + "\tDATA_LOSS\x10\x0f\x12\x13\n" + + "\x0fUNAUTHENTICATED\x10\x10*\xb0\x02\n" + + "\vLogSeverity\x12\x0f\n" + + "\vUNSPECIFIED\x10\x00\x12\t\n" + + "\x05TRACE\x10\x01\x12\n" + + "\n" + + "\x06TRACE2\x10\x02\x12\n" + + "\n" + + "\x06TRACE3\x10\x03\x12\n" + + "\n" + + "\x06TRACE4\x10\x04\x12\t\n" + + "\x05DEBUG\x10\x05\x12\n" + + "\n" + + "\x06DEBUG2\x10\x06\x12\n" + + "\n" + + "\x06DEBUG3\x10\a\x12\n" + + "\n" + + "\x06DEBUG4\x10\b\x12\b\n" + + "\x04INFO\x10\t\x12\t\n" + + "\x05INFO2\x10\n" + + "\x12\t\n" + + "\x05INFO3\x10\v\x12\t\n" + + "\x05INFO4\x10\f\x12\b\n" + + "\x04WARN\x10\r\x12\t\n" + + "\x05WARN2\x10\x0e\x12\t\n" + + "\x05WARN3\x10\x0f\x12\t\n" + + "\x05WARN4\x10\x10\x12\t\n" + + "\x05ERROR\x10\x11\x12\n" + + "\n" + + "\x06ERROR2\x10\x12\x12\n" + + "\n" + + "\x06ERROR3\x10\x13\x12\n" + + "\n" + + "\x06ERROR4\x10\x14\x12\t\n" + + "\x05FATAL\x10\x15\x12\n" + + "\n" + + "\x06FATAL2\x10\x16\x12\n" + + "\n" + + "\x06FATAL3\x10\x17\x12\n" + + "\n" + + "\x06FATAL4\x10\x182Y\n" + + "\vLogsService\x12J\n" + + "\rGetLogRecords\x12\x1a.logs.GetLogRecordsRequest\x1a\x1b.logs.GetLogRecordsResponse0\x01B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_logs_proto_rawDescOnce sync.Once + file_logs_proto_rawDescData []byte +) + +func file_logs_proto_rawDescGZIP() []byte { + file_logs_proto_rawDescOnce.Do(func() { + file_logs_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_logs_proto_rawDesc), len(file_logs_proto_rawDesc))) + }) + return file_logs_proto_rawDescData +} + +var file_logs_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_logs_proto_goTypes = []any{ + (LogSeverity)(0), // 0: logs.LogSeverity + (NATSGetLogRecordsResponseStatus_Status)(0), // 1: logs.NATSGetLogRecordsResponseStatus.Status + (SourceError_Code)(0), // 2: logs.SourceError.Code + (*GetLogRecordsRequest)(nil), // 3: logs.GetLogRecordsRequest + (*GetLogRecordsResponse)(nil), // 4: logs.GetLogRecordsResponse + (*LogRecord)(nil), // 5: logs.LogRecord + (*NATSGetLogRecordsRequest)(nil), // 6: logs.NATSGetLogRecordsRequest + (*NATSGetLogRecordsResponse)(nil), // 7: logs.NATSGetLogRecordsResponse + (*NATSGetLogRecordsResponseStatus)(nil), // 8: logs.NATSGetLogRecordsResponseStatus + (*SourceError)(nil), // 9: logs.SourceError + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 11: google.protobuf.Struct +} +var file_logs_proto_depIdxs = []int32{ + 10, // 0: logs.GetLogRecordsRequest.from:type_name -> google.protobuf.Timestamp + 10, // 1: logs.GetLogRecordsRequest.to:type_name -> google.protobuf.Timestamp + 5, // 2: logs.GetLogRecordsResponse.records:type_name -> logs.LogRecord + 10, // 3: logs.LogRecord.createdAt:type_name -> google.protobuf.Timestamp + 10, // 4: logs.LogRecord.observedAt:type_name -> google.protobuf.Timestamp + 0, // 5: logs.LogRecord.severity:type_name -> logs.LogSeverity + 11, // 6: logs.LogRecord.resource:type_name -> google.protobuf.Struct + 11, // 7: logs.LogRecord.attributes:type_name -> google.protobuf.Struct + 3, // 8: logs.NATSGetLogRecordsRequest.request:type_name -> logs.GetLogRecordsRequest + 8, // 9: logs.NATSGetLogRecordsResponse.status:type_name -> logs.NATSGetLogRecordsResponseStatus + 4, // 10: logs.NATSGetLogRecordsResponse.response:type_name -> logs.GetLogRecordsResponse + 1, // 11: logs.NATSGetLogRecordsResponseStatus.status:type_name -> logs.NATSGetLogRecordsResponseStatus.Status + 9, // 12: logs.NATSGetLogRecordsResponseStatus.error:type_name -> logs.SourceError + 2, // 13: logs.SourceError.code:type_name -> logs.SourceError.Code + 3, // 14: logs.LogsService.GetLogRecords:input_type -> logs.GetLogRecordsRequest + 4, // 15: logs.LogsService.GetLogRecords:output_type -> logs.GetLogRecordsResponse + 15, // [15:16] is the sub-list for method output_type + 14, // [14:15] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_logs_proto_init() } +func file_logs_proto_init() { + if File_logs_proto != nil { + return + } + file_logs_proto_msgTypes[2].OneofWrappers = []any{} + file_logs_proto_msgTypes[4].OneofWrappers = []any{ + (*NATSGetLogRecordsResponse_Status)(nil), + (*NATSGetLogRecordsResponse_Response)(nil), + } + file_logs_proto_msgTypes[5].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_logs_proto_rawDesc), len(file_logs_proto_rawDesc)), + NumEnums: 3, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_logs_proto_goTypes, + DependencyIndexes: file_logs_proto_depIdxs, + EnumInfos: file_logs_proto_enumTypes, + MessageInfos: file_logs_proto_msgTypes, + }.Build() + File_logs_proto = out.File + file_logs_proto_goTypes = nil + file_logs_proto_depIdxs = nil +} diff --git a/go/sdp-go/logs_test.go b/go/sdp-go/logs_test.go new file mode 100644 index 00000000..64994832 --- /dev/null +++ b/go/sdp-go/logs_test.go @@ -0,0 +1,155 @@ +package sdp + +import ( + "testing" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestGetLogRecordsRequest_Validate(t *testing.T) { + now := time.Now() + pastTime := now.Add(-1 * time.Hour) + futureTime := now.Add(1 * time.Hour) + + tests := []struct { + name string + req *GetLogRecordsRequest + wantErr bool + }{ + { + name: "Nil request", + req: nil, + wantErr: true, + }, + { + name: "Empty scope", + req: &GetLogRecordsRequest{ + Scope: "", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "Empty query", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "Missing from timestamp", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: nil, + To: timestamppb.New(now), + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "Missing to timestamp", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: nil, + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "From after to", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(futureTime), + To: timestamppb.New(pastTime), + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "MaxRecords zero", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: 0, + StartFromOldest: false, + }, + wantErr: false, + }, + { + name: "MaxRecords negative", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: -10, + StartFromOldest: false, + }, + wantErr: true, + }, + { + name: "Valid request with MaxRecords", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: 100, + StartFromOldest: false, + }, + wantErr: false, + }, + { + name: "Valid request without MaxRecords", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(pastTime), + To: timestamppb.New(now), + MaxRecords: 0, + StartFromOldest: false, + }, + wantErr: false, + }, + { + name: "Valid request with equal timestamps", + req: &GetLogRecordsRequest{ + Scope: "valid-scope", + Query: "valid-query", + From: timestamppb.New(now), + To: timestamppb.New(now), + MaxRecords: 100, + StartFromOldest: true, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("GetLogRecordsRequest.Validate() error = %v, wantErr %v\nrequest = %v", err, tt.wantErr, tt.req) + } + }) + } +} diff --git a/go/sdp-go/progress.go b/go/sdp-go/progress.go new file mode 100644 index 00000000..fe1f40c6 --- /dev/null +++ b/go/sdp-go/progress.go @@ -0,0 +1,734 @@ +package sdp + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/getsentry/sentry-go" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/durationpb" +) + +// DefaultResponseInterval is the default period of time within which responses +// are sent (5 seconds) +const DefaultResponseInterval = (5 * time.Second) + +// DefaultStartTimeout is the default period of time to wait for the first +// response on a query. If no response is received in this time, the query will +// be marked as complete. +const DefaultStartTimeout = 2000 * time.Millisecond + +// ResponseSender is a struct responsible for sending responses out on behalf of +// agents that are working on that request. Think of it as the agent side +// component of Responder +type ResponseSender struct { + // How often to send responses. The expected next update will be 230% of + // this value, allowing for one-and-a-bit missed responses before it is + // marked as stalled + ResponseInterval time.Duration + ResponseSubject string + monitorRunning sync.WaitGroup + monitorKill chan *Response // Sending to this channel will kill the response sender goroutine and publish the sent message as last msg on the subject + responderName string + responderId uuid.UUID + connection EncodedConnection + responseCtx context.Context +} + +// Start sends the first response on the given subject and connection to say +// that the request is being worked on. It also starts a go routine to continue +// sending responses. +// +// The user should make sure to call Done(), Error() or Cancel() once the query +// has finished to make sure this process stops sending responses. The sender +// will also be stopped if the context is cancelled +func (rs *ResponseSender) Start(ctx context.Context, ec EncodedConnection, responderName string, responderId uuid.UUID) { + rs.monitorKill = make(chan *Response, 1) + rs.responseCtx = ctx + + // Set the default if it's not set + if rs.ResponseInterval == 0 { + rs.ResponseInterval = DefaultResponseInterval + } + + // Tell it to expect the next update in 230% of the expected time. This + // allows for a response getting lost, plus some delay + nextUpdateIn := durationpb.New(time.Duration((float64(rs.ResponseInterval) * 2.3))) + + // Set struct values + rs.responderName = responderName + rs.responderId = responderId + rs.connection = ec + + // Create the response before starting the goroutine since it only needs to + // be done once + resp := Response{ + Responder: rs.responderName, + ResponderUUID: rs.responderId[:], + State: ResponderState_WORKING, + NextUpdateIn: nextUpdateIn, + } + + if rs.connection != nil { + // Send the initial response + err := rs.connection.Publish( + ctx, + rs.ResponseSubject, + &QueryResponse{ResponseType: &QueryResponse_Response{Response: &resp}}, + ) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Error publishing initial response") + } + } + + rs.monitorRunning.Add(1) + + // Start a goroutine to send further responses + go func() { + defer tracing.LogRecoverToReturn(ctx, "ResponseSender ticker") + // confirm closure on exit + defer rs.monitorRunning.Done() + + if ec == nil { + return + } + tick := time.NewTicker(rs.ResponseInterval) + defer tick.Stop() + + for { + var err error + + select { + case <-rs.monitorKill: + return + case <-ctx.Done(): + return + case <-tick.C: + err = rs.connection.Publish( + ctx, + rs.ResponseSubject, + &QueryResponse{ResponseType: &QueryResponse_Response{Response: &resp}}, + ) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Error publishing response") + } + } + } + }() +} + +// Kill Kills the response sender immediately. This should be used if something +// has failed and you don't want to send a completed response +// +// Deprecated: Use KillWithContext(ctx) instead +func (rs *ResponseSender) Kill() { + rs.killWithResponse(context.Background(), nil) +} + +// KillWithContext Kills the response sender immediately. This should be used if something +// has failed and you don't want to send a completed response +func (rs *ResponseSender) KillWithContext(ctx context.Context) { + rs.killWithResponse(ctx, nil) +} + +func (rs *ResponseSender) killWithResponse(ctx context.Context, r *Response) { + // close the channel to kill the sender + close(rs.monitorKill) + + // wait for the sender to be actually done + rs.monitorRunning.Wait() + + if rs.connection != nil { + if r != nil { + // Send the final response + err := rs.connection.Publish(ctx, rs.ResponseSubject, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: r, + }, + }) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Error publishing final response") + } + } + } +} + +// Done kills the responder but sends a final completion message +// +// Deprecated: Use DoneWithContext(ctx) instead +func (rs *ResponseSender) Done() { + rs.DoneWithContext(context.Background()) +} + +// DoneWithContext kills the responder but sends a final completion message +func (rs *ResponseSender) DoneWithContext(ctx context.Context) { + resp := Response{ + Responder: rs.responderName, + ResponderUUID: rs.responderId[:], + State: ResponderState_COMPLETE, + } + rs.killWithResponse(ctx, &resp) +} + +// Error marks the request and completed with error, and sends the final error +// response +// +// Deprecated: Use ErrorWithContext(ctx) instead +func (rs *ResponseSender) Error() { + rs.ErrorWithContext(context.Background()) +} + +// ErrorWithContext marks the request and completed with error, and sends the final error +// response +func (rs *ResponseSender) ErrorWithContext(ctx context.Context) { + resp := Response{ + Responder: rs.responderName, + ResponderUUID: rs.responderId[:], + State: ResponderState_ERROR, + } + rs.killWithResponse(ctx, &resp) +} + +// Cancel Marks the request as CANCELLED and sends the final response +// +// Deprecated: Use CancelWithContext(ctx) instead +func (rs *ResponseSender) Cancel() { + rs.CancelWithContext(context.Background()) +} + +// CancelWithContext Marks the request as CANCELLED and sends the final response +func (rs *ResponseSender) CancelWithContext(ctx context.Context) { + resp := Response{ + Responder: rs.responderName, + ResponderUUID: rs.responderId[:], + State: ResponderState_CANCELLED, + } + rs.killWithResponse(ctx, &resp) +} + +type lastResponse struct { + Response *Response + Timestamp time.Time +} + +// Checks to see if this responder is stalled. If it is, it will update the +// responder state to ResponderState_STALLED. Only runs if the responder is in +// the WORKING state, doesn't do anything otherwise. +func (l *lastResponse) checkStalled() { + if l.Response == nil || l.Response.GetState() != ResponderState_WORKING { + return + } + + // Calculate if it's stalled, but only if it has a `NextUpdateIn` value. + // Responders that do not provided a `NextUpdateIn` value are not considered + // for stalling + timeSinceLastUpdate := time.Since(l.Timestamp) + timeToNextUpdate := l.Response.GetNextUpdateIn().AsDuration() + if timeToNextUpdate > 0 && timeSinceLastUpdate > timeToNextUpdate { + l.Response.State = ResponderState_STALLED + } +} + +// SourceQuery tracks the progress of a query across multiple responders (Sources). +// It manages a state machine for each responder with the following states: +// +// WORKING → COMPLETE (normal completion) +// WORKING → ERROR (responder failed) +// WORKING → CANCELLED (query was cancelled) +// WORKING → STALLED (responder stopped sending updates) +// +// A query is considered finished when the start timeout has elapsed AND all +// responders are in a terminal state (COMPLETE, ERROR, CANCELLED, or STALLED). +type SourceQuery struct { + // A map of ResponderUUIDs to the last response we got from them + responders map[uuid.UUID]*lastResponse + respondersMu sync.Mutex + + // Channel storage for sending back to the user + responseChan chan<- *QueryResponse + + // Use to make sure a user doesn't try to start a request twice. This is an + // atomic to allow tests to directly inject messages using + // `handleQueryResponse` + startTimeoutElapsed atomic.Bool + + querySub *nats.Subscription + + cancel context.CancelFunc +} + +// SourceQueryProgress represents the current progress of a tracked query, +// aggregating the state of all responders. +type SourceQueryProgress struct { + // How many responders are currently working on this query. This means they + // are active sending updates + Working int + + // Stalled responders are ones that have sent updates in the past, but the + // latest update is overdue. This likely indicates a problem with the + // responder + Stalled int + + // Responders that are complete + Complete int + + // Responders that failed + Error int + + // Responders that were cancelled. When cancelling the SourceQueryProgress + // does not wait for responders to acknowledge the cancellation, it simply + // sends the message and marks all responders that are currently "working" + // as "cancelled". It is possible that a responder will self-report + // cancellation, but given the timings this is unlikely as it would need to + // be very fast + Cancelled int + + // The total number of tracked responders + Responders int +} + +// RunSourceQuery returns a pointer to a SourceQuery object with the various +// internal members initialized. A startTimeout must also be provided, feel free +// to use `DefaultStartTimeout` if you don't have a specific value in mind. +func RunSourceQuery(ctx context.Context, query *Query, startTimeout time.Duration, ec EncodedConnection, responseChan chan<- *QueryResponse) (*SourceQuery, error) { + if startTimeout == 0 { + return nil, errors.New("startTimeout must be greater than 0") + } + + if ec.Underlying() == nil { + return nil, errors.New("nil NATS connection") + } + + if responseChan == nil { + return nil, errors.New("nil response channel") + } + + if query.GetScope() == "" { + return nil, errors.New("cannot execute request with blank scope") + } + + // Generate a UUID if required + if len(query.GetUUID()) == 0 { + u := uuid.New() + query.UUID = u[:] + } + + // Calculate the correct subject to send the message on + var requestSubject string + if query.GetScope() == WILDCARD { + requestSubject = "request.all" + } else { + requestSubject = fmt.Sprintf("request.scope.%v", query.GetScope()) + } + + // Create the channel that NATS responses will come through + natsResponses := make(chan *QueryResponse) + + // Create a timer for the start timeout + startTimeoutTimer := time.NewTimer(startTimeout) + + // Subscribe to the query subject and wait for responses + querySub, err := ec.Subscribe(query.Subject(), NewQueryResponseHandler("", func(ctx context.Context, qr *QueryResponse) { //nolint:contextcheck // we pass the context in the func + natsResponses <- qr + })) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(ctx) + + sq := &SourceQuery{ + responders: make(map[uuid.UUID]*lastResponse), + startTimeoutElapsed: atomic.Bool{}, + querySub: querySub, + cancel: cancel, + responseChan: responseChan, + } + + // Main processing loop. This runs is the main goroutine that tracks this + // request + go func() { + // Initialise the stall check ticker + stallCheck := time.NewTicker(500 * time.Millisecond) + defer stallCheck.Stop() + ctx, span := tracing.Tracer().Start(ctx, "QueryProgress") + defer span.End() + + query.SetSpanAttributes(span) + + for { + select { + case <-ctx.Done(): + // If the connection is closed, we do not need to send a cancel message + if u := ec.Underlying(); u == nil || u.IsClosed() { + sq.markWorkingRespondersCancelled() + sq.cleanup(ctx) + return + } + // Since this context is done, we need a new context just to + // send the cancellation message + cancelCtx, cancelCtxCancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second) + defer cancelCtxCancel() + + // Send a cancel message to all responders + cancelRequest := CancelQuery{ + UUID: query.GetUUID(), + } + + var cancelSubject string + if query.GetScope() == WILDCARD { + cancelSubject = "cancel.all" + } else { + cancelSubject = fmt.Sprintf("cancel.scope.%v", query.GetScope()) + } + + err := ec.Publish(cancelCtx, cancelSubject, &cancelRequest) + if err != nil { + log.WithContext(ctx).WithError(err).Error("Error sending cancel message") + span.RecordError(err) + } + + sq.markWorkingRespondersCancelled() + sq.cleanup(ctx) + return + case <-startTimeoutTimer.C: + sq.startTimeoutElapsed.Store(true) + + if sq.finished() { + sq.cleanup(ctx) + return + } + case response := <-natsResponses: + // Handle the response + if sq.handleQueryResponse(ctx, response) { + // This means we are done + return + } + case <-stallCheck.C: + + // If we get here, it means that we haven't had a response + // in a while, so we should check to see if things have + // stalled + if sq.finished() { + sq.cleanup(ctx) + return + } + } + } + }() + + // Send the message to start the query + err = ec.Publish(ctx, requestSubject, query) + if err != nil { + return nil, err + } + + return sq, nil +} + +// Execute a given request and wait for it to finish, returns the items that +// were found and any errors. The third return error value will only be returned +// only if there is a problem making the request. Details of which responders +// have failed etc. should be determined using the typical methods like +// `NumError()`. +func RunSourceQuerySync(ctx context.Context, query *Query, startTimeout time.Duration, ec EncodedConnection) ([]*Item, []*Edge, []*QueryError, error) { + items := make([]*Item, 0) + edges := make([]*Edge, 0) + errs := make([]*QueryError, 0) + r := make(chan *QueryResponse, 128) + + if ec == nil { + return items, edges, errs, errors.New("nil NATS connection") + } + + _, err := RunSourceQuery(ctx, query, startTimeout, ec, r) + if err != nil { + return items, edges, errs, err + } + + // Read items and errors + for response := range r { + item := response.GetNewItem() + if item != nil { + items = append(items, item) + } + edge := response.GetEdge() + if edge != nil { + edges = append(edges, edge) + } + qErr := response.GetError() + if qErr != nil { + errs = append(errs, qErr) + } + // ignore status responses for now + // status := response.GetResponse() + // if status != nil { + // panic("qp: status not implemented yet") + // } + } + + // when the channel closes, we're done + return items, edges, errs, nil +} + +// Cancel cancels the query by sending a cancel message to all responders and +// closing the response channel. Alternatively, the query can be cancelled by +// cancelling the context passed to RunSourceQuery. +func (sq *SourceQuery) Cancel() { + sq.cancel() +} + +// This is split out into its own function so that it can be tested more easily +// with out having to worry about race conditions. This returns a boolean which +// indicates if the request is complete or not +func (sq *SourceQuery) handleQueryResponse(ctx context.Context, response *QueryResponse) bool { + switch r := response.GetResponseType().(type) { + case *QueryResponse_NewItem: + sq.handleItem(r.NewItem) + case *QueryResponse_Edge: + sq.handleEdge(r.Edge) + case *QueryResponse_Error: + sq.handleError(r.Error) + case *QueryResponse_Response: + sq.handleResponse(ctx, r.Response) + + if sq.finished() { + sq.cleanup(ctx) + return true + } + } + + return false +} + +// markWorkingRespondersCancelled marks all working responders as cancelled +// internally, there is no need to wait for them to confirm the cancellation, as +// we're not going to wait for any further responses. +func (sq *SourceQuery) markWorkingRespondersCancelled() { + sq.respondersMu.Lock() + defer sq.respondersMu.Unlock() + + for _, lastResponse := range sq.responders { + if lastResponse.Response.GetState() == ResponderState_WORKING { + lastResponse.Response.State = ResponderState_CANCELLED + } + } +} + +// Whether the query should be considered finished or not. This is based on +// whether the start timeout has elapsed and all responders are done +func (sq *SourceQuery) finished() bool { + return sq.startTimeoutElapsed.Load() && sq.allDone() +} + +// Cleans up the query, unsubscribing from the query subject and closing the +// response channel +func (sq *SourceQuery) cleanup(ctx context.Context) { + span := trace.SpanFromContext(ctx) + if sq.querySub != nil && sq.querySub.IsValid() { + err := sq.querySub.Unsubscribe() + if err != nil { + log.WithField("error", err).Error("Error unsubscribing from query subject") + span.RecordError(err) + } + } + + close(sq.responseChan) + sq.cancel() +} + +// Sends the item back to the response channel, also extracts and synthesises +// edges from `LinkedItems` and `LinkedItemQueries` and sends them back too +func (sq *SourceQuery) handleItem(item *Item) { + if item == nil { + return + } + + // Send the item back over the channel + // TODO(LIQs): translation is not necessary anymore; update code and method comment + item, edges := TranslateLinksToEdges(item) + sq.responseChan <- &QueryResponse{ + ResponseType: &QueryResponse_NewItem{NewItem: item}, + } + for _, e := range edges { + sq.responseChan <- &QueryResponse{ + ResponseType: &QueryResponse_Edge{Edge: e}, + } + } +} + +// Sends the edge back to the response channel +func (sq *SourceQuery) handleEdge(edge *Edge) { + if edge == nil { + return + } + + sq.responseChan <- &QueryResponse{ + ResponseType: &QueryResponse_Edge{Edge: edge}, + } +} + +// Send the error back to the response channel +func (sq *SourceQuery) handleError(err *QueryError) { + if err == nil { + return + } + + sq.responseChan <- &QueryResponse{ + ResponseType: &QueryResponse_Error{ + Error: err, + }, + } +} + +// Update the internal state with the response +func (sq *SourceQuery) handleResponse(ctx context.Context, response *Response) { + span := trace.SpanFromContext(ctx) + + // do not deal with responses that do not have a responder UUID + ru, err := uuid.FromBytes(response.GetResponderUUID()) + if err != nil { + span.RecordError(fmt.Errorf("error parsing responder UUID: %w", err)) + return + } + + sq.respondersMu.Lock() + defer sq.respondersMu.Unlock() + + // Protect against out-of order responses. Do not mark a responder as + // working if it has already finished. this should never happen, but we want + // to know if it does as it will indicate a bug in the responder itself + last, exists := sq.responders[ru] + if exists { + if last.Response != nil { + switch last.Response.GetState() { + case ResponderState_COMPLETE, ResponderState_ERROR, ResponderState_CANCELLED: + err = fmt.Errorf("out-of-order response. Responder was already in the state %v, skipping update to %v", last.Response.String(), response.GetState().String()) + span.RecordError(err) + sentry.CaptureException(err) + return + case ResponderState_WORKING, ResponderState_STALLED: + // This is fine, we can update the state + } + } + } + + // Update the stored data + sq.responders[ru] = &lastResponse{ + Response: response, + Timestamp: time.Now(), + } +} + +// Checks whether all responders are done or not. A "Done" responder is one that +// is either: Complete, Error, Cancelled or Stalled +// +// Note that this doesn't perform locking if the mutex, this needs to be done by +// the caller +func (sq *SourceQuery) allDone() bool { + sq.respondersMu.Lock() + defer sq.respondersMu.Unlock() + + for _, lastResponse := range sq.responders { + // Recalculate the stall status + lastResponse.checkStalled() + + if lastResponse.Response.GetState() == ResponderState_WORKING { + return false + } + } + + return true +} + +// TranslateLinksToEdges Translates linked items and queries into edges. This is +// a temporary stop gap measure to allow parallel processing of items and edges +// in the gateway while allowing other parts of the system to be updated +// independently. See https://github.com/overmindtech/workspace/issues/753 +func TranslateLinksToEdges(item *Item) (*Item, []*Edge) { + // TODO(LIQs): translation is not necessary anymore; delete this method and all callsites + lis := item.GetLinkedItems() + item.LinkedItems = nil + liqs := item.GetLinkedItemQueries() + item.LinkedItemQueries = nil + + edges := []*Edge{} + + for _, li := range lis { + edges = append(edges, &Edge{ + From: item.Reference(), + To: li.GetItem(), + BlastPropagation: li.GetBlastPropagation(), + }) + } + + for _, liq := range liqs { + edges = append(edges, &Edge{ + From: item.Reference(), + To: liq.GetQuery().Reference(), + BlastPropagation: liq.GetBlastPropagation(), + }) + } + + return item, edges +} + +// Progress returns the current progress statistics for the query, including +// counts of responders in each state (Working, Stalled, Complete, Error, Cancelled). +func (sq *SourceQuery) Progress() SourceQueryProgress { + sq.respondersMu.Lock() + defer sq.respondersMu.Unlock() + + var numWorking, numStalled, numComplete, numError, numCancelled int + + // Loop over all responders once and calculate the progress + for _, lastResponse := range sq.responders { + // Recalculate the stall status + lastResponse.checkStalled() + + switch lastResponse.Response.GetState() { + case ResponderState_WORKING: + numWorking++ + case ResponderState_STALLED: + numStalled++ + case ResponderState_COMPLETE: + numComplete++ + case ResponderState_ERROR: + numError++ + case ResponderState_CANCELLED: + numCancelled++ + } + } + + return SourceQueryProgress{ + Working: numWorking, + Stalled: numStalled, + Complete: numComplete, + Error: numError, + Cancelled: numCancelled, + Responders: len(sq.responders), + } +} + +// String returns a human-readable summary of the query progress. +func (sq *SourceQuery) String() string { + progress := sq.Progress() + + return fmt.Sprintf( + "Working: %v\nStalled: %v\nComplete: %v\nError: %v\nCancelled: %v\nResponders: %v\n", + progress.Working, + progress.Stalled, + progress.Complete, + progress.Error, + progress.Cancelled, + progress.Responders, + ) +} diff --git a/go/sdp-go/progress_test.go b/go/sdp-go/progress_test.go new file mode 100644 index 00000000..02c8adfd --- /dev/null +++ b/go/sdp-go/progress_test.go @@ -0,0 +1,1525 @@ +package sdp + +import ( + "context" + "errors" + "fmt" + "math/rand" + "os" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestRunSourceQueryParams(t *testing.T) { + u := uuid.New() + q := Query{ + Type: "person", + Method: QueryMethod_GET, + Query: "dylan", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "test", + IgnoreCache: false, + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(20 * time.Second)), + } + + t.Run("with no start timeout", func(t *testing.T) { + _, err := RunSourceQuery(t.Context(), &q, 0, nil, nil) + + if err == nil { + t.Error("expected an error when there is not startTimeout") + } + }) +} + +func TestResponseNilPublisher(t *testing.T) { + ctx := context.Background() + + rs := ResponseSender{ + ResponseInterval: (10 * time.Millisecond), + ResponseSubject: "responses", + } + + // Start sending responses with a nil connection, should not panic + rs.Start(ctx, nil, "test", uuid.New()) + + // Give it enough time for ~10 responses + time.Sleep(100 * time.Millisecond) + + // Stop + rs.DoneWithContext(ctx) +} + +func TestResponseSenderDone(t *testing.T) { + ctx := context.Background() + + rs := ResponseSender{ + ResponseInterval: (10 * time.Millisecond), + ResponseSubject: "responses", + } + + tc := TestConnection{IgnoreNoResponders: true} + + // Start sending responses + rs.Start(ctx, &tc, "test", uuid.New()) + + // Give it enough time for ~10 responses + time.Sleep(100 * time.Millisecond) + + // Stop + rs.DoneWithContext(ctx) + + // Let it drain down + time.Sleep(100 * time.Millisecond) + + // Inspect what was sent + tc.MessagesMu.Lock() + if len(tc.Messages) <= 10 { + t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) + } + + // Make sure that the final message was a completion one + finalMessage := tc.Messages[len(tc.Messages)-1] + tc.MessagesMu.Unlock() + + if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { + if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { + if finalResponse.Response.GetState() != ResponderState_COMPLETE { + t.Errorf("Expected final message state to be COMPLETE (1), found: %v", finalResponse.Response.GetState()) + } + } else { + t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) + } + } else { + t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) + } +} + +func TestResponseSenderError(t *testing.T) { + ctx := context.Background() + + rs := ResponseSender{ + ResponseInterval: (10 * time.Millisecond), + ResponseSubject: "responses", + } + + tc := TestConnection{IgnoreNoResponders: true} + + // Start sending responses + rs.Start(ctx, &tc, "test", uuid.New()) + + // Give it enough time for >10 responses + time.Sleep(120 * time.Millisecond) + + // Stop + rs.ErrorWithContext(ctx) + + // Let it drain down + time.Sleep(100 * time.Millisecond) + + // Inspect what was sent + tc.MessagesMu.Lock() + if len(tc.Messages) <= 10 { + t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) + } + + // Make sure that the final message was a completion one + finalMessage := tc.Messages[len(tc.Messages)-1] + tc.MessagesMu.Unlock() + + if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { + if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { + if finalResponse.Response.GetState() != ResponderState_ERROR { + t.Errorf("Expected final message state to be ERROR, found: %v", finalResponse.Response.GetState()) + } + } else { + t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) + } + } else { + t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) + } +} + +func TestResponseSenderCancel(t *testing.T) { + ctx := context.Background() + + rs := ResponseSender{ + ResponseInterval: (10 * time.Millisecond), + ResponseSubject: "responses", + } + + tc := TestConnection{IgnoreNoResponders: true} + + // Start sending responses + rs.Start(ctx, &tc, "test", uuid.New()) + + // Give it enough time for >10 responses + time.Sleep(120 * time.Millisecond) + + // Stop + rs.CancelWithContext(ctx) + + // Let it drain down + time.Sleep(100 * time.Millisecond) + + // Inspect what was sent + tc.MessagesMu.Lock() + if len(tc.Messages) <= 10 { + t.Errorf("Expected <= 10 responses to be sent, found %v", len(tc.Messages)) + } + + // Make sure that the final message was a completion one + finalMessage := tc.Messages[len(tc.Messages)-1] + tc.MessagesMu.Unlock() + + if queryResponse, ok := finalMessage.V.(*QueryResponse); ok { + if finalResponse, ok := queryResponse.GetResponseType().(*QueryResponse_Response); ok { + if finalResponse.Response.GetState() != ResponderState_CANCELLED { + t.Errorf("Expected final message state to be CANCELLED, found: %v", finalResponse.Response.GetState()) + } + } else { + t.Errorf("Final QueryResponse did not contain a valid Response object. Message content type %T", queryResponse.GetResponseType()) + } + } else { + t.Errorf("Final message did not contain a valid response object. Message content type %T", finalMessage.V) + } +} + +func TestDefaultResponseInterval(t *testing.T) { + ctx := context.Background() + + rs := ResponseSender{} + + rs.Start(ctx, &TestConnection{}, "", uuid.New()) + rs.KillWithContext(ctx) + + if rs.ResponseInterval != DefaultResponseInterval { + t.Fatal("Response sender interval failed to default") + } +} + +// ExpectToMatch Checks that metrics are as expected and returns an error if not +func (expected SourceQueryProgress) ExpectToMatch(qp *SourceQuery) error { + actual := qp.Progress() + + var err error + + if expected.Working != actual.Working { + err = errors.Join(err, fmt.Errorf("Expected Working to be %v, got %v", expected.Working, actual.Working)) + } + if expected.Stalled != actual.Stalled { + err = errors.Join(err, fmt.Errorf("Expected Stalled to be %v, got %v", expected.Stalled, actual.Stalled)) + } + if expected.Complete != actual.Complete { + err = errors.Join(err, fmt.Errorf("Expected Complete to be %v, got %v", expected.Complete, actual.Complete)) + } + if expected.Error != actual.Error { + err = errors.Join(err, fmt.Errorf("Expected Error to be %v, got %v", expected.Error, actual.Error)) + } + if expected.Responders != actual.Responders { + err = errors.Join(err, fmt.Errorf("Expected Responders to be %v, got %v", expected.Responders, actual.Responders)) + } + if expected.Cancelled != actual.Cancelled { + err = errors.Join(err, fmt.Errorf("Expected Cancelled to be %v, got %v", expected.Cancelled, actual.Cancelled)) + } + + return err +} + +// Create a channel that discards everything +func devNull() chan<- *QueryResponse { + c := make(chan *QueryResponse, 128) + go func() { + for range c { + } + }() + return c +} + +func TestQueryProgressNormal(t *testing.T) { + t.Parallel() + + ctx := t.Context() + tc := TestConnection{IgnoreNoResponders: true} + sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) + if err != nil { + t.Fatal(err) + } + + ru1 := uuid.New() + ru2 := uuid.New() + ru3 := uuid.New() + t.Logf("UUIDs: %v %v %v", ru1, ru2, ru3) + + // Make sure that the details are correct initially + var expected SourceQueryProgress + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 0, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + + t.Run("Processing initial response", func(t *testing.T) { + // Test the initial response + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 1, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("Processing when other scopes also responding", func(t *testing.T) { + // Then another scope starts working + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru2[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru3[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 3, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 3, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("When some are complete and some are not", func(t *testing.T) { + time.Sleep(5 * time.Millisecond) + + // test 1 still working + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + // Test 2 finishes + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru2[:], + State: ResponderState_COMPLETE, + }, + }, + }) + + // Test 3 still working + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru3[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 2, + Stalled: 0, + Complete: 1, + Error: 0, + Responders: 3, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("When one is cancelled", func(t *testing.T) { + time.Sleep(5 * time.Millisecond) + + // test 1 still working + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + // Test 3 cancelled + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru3[:], + State: ResponderState_CANCELLED, + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 1, + Stalled: 0, + Complete: 1, + Error: 0, + Cancelled: 1, + Responders: 3, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("When the final responder finishes", func(t *testing.T) { + time.Sleep(5 * time.Millisecond) + + // Test 1 finishes + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_COMPLETE, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 2, + Error: 0, + Cancelled: 1, + Responders: 3, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + if sq.allDone() == false { + t.Error("expected allDone() to be true") + } +} + +func TestQueryProgressParallel(t *testing.T) { + t.Parallel() + + ctx := t.Context() + tc := TestConnection{IgnoreNoResponders: true} + sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) + if err != nil { + t.Fatal(err) + } + + ru1 := uuid.New() + + // Make sure that the details are correct initially + var expected SourceQueryProgress + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 0, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + + t.Run("Processing many bunched responses", func(t *testing.T) { + var wg sync.WaitGroup + + for i := 0; i != 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + // Test the initial response + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + }() + } + + wg.Wait() + + expected = SourceQueryProgress{ + Working: 1, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) +} + +func TestQueryProgressStalled(t *testing.T) { + t.Parallel() + + ctx := t.Context() + tc := TestConnection{IgnoreNoResponders: true} + sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) + if err != nil { + t.Fatal(err) + } + + ru1 := uuid.New() + + // Make sure that the details are correct initially + var expected SourceQueryProgress + + t.Run("Processing the initial response", func(t *testing.T) { + // Test the initial response + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 1, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("After a responder has stalled", func(t *testing.T) { + // Wait long enough for the thing to be marked as stalled + time.Sleep(20 * time.Millisecond) + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 1, + Complete: 0, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + + sq.respondersMu.Lock() + defer sq.respondersMu.Unlock() + if _, ok := sq.responders[ru1]; !ok { + t.Error("Could not get responder for scope test1") + } + }) + + t.Run("After a responder recovers from a stall", func(t *testing.T) { + // See if it will un-stall itself + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_COMPLETE, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 1, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + if sq.allDone() == false { + t.Error("expected allDone() to be true") + } +} + +func TestRogueResponder(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + tc := TestConnection{IgnoreNoResponders: true} + sq, err := RunSourceQuery(ctx, &query, 100*time.Millisecond, &tc, devNull()) + if err != nil { + t.Fatal(err) + } + + rur := uuid.New() + + // Create our rogue responder that doesn't cancel when it should + ticker := time.NewTicker(5 * time.Second) + tickerCtx, tickerCancel := context.WithCancel(context.Background()) + defer tickerCancel() + defer ticker.Stop() + + go func() { + // Send an initial response + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: rur[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(5 * time.Second), + }, + }, + }) + + // Now start ticking + for { + select { + case <-ticker.C: + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: rur[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(5 * time.Second), + }, + }, + }) + case <-tickerCtx.Done(): + return + } + } + }() + + time.Sleep(300 * time.Millisecond) + + // Check that we've noticed the testRogue responder + if sq.allDone() == true { + t.Error("expected allDone() to be false") + } + + cancel() + + time.Sleep(100 * time.Millisecond) + + // We expect that it has been marked as cancelled, regardless of what the + // responder actually did + expected := SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 1, + Cancelled: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } +} + +func TestQueryProgressError(t *testing.T) { + t.Parallel() + + ctx := t.Context() + tc := TestConnection{IgnoreNoResponders: true} + sq, err := RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, devNull()) + if err != nil { + t.Fatal(err) + } + + ru1 := uuid.New() + + // Make sure that the details are correct initially + var expected SourceQueryProgress + + t.Run("Processing the initial response", func(t *testing.T) { + // Test the initial response + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(10 * time.Millisecond), + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 1, + Stalled: 0, + Complete: 0, + Error: 0, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("After a responder has failed", func(t *testing.T) { + sq.handleQueryResponse(ctx, &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_ERROR, + }, + }, + }) + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 0, + Error: 1, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + t.Run("Ensuring that a failed responder does not get marked as stalled", func(t *testing.T) { + time.Sleep(12 * time.Millisecond) + + expected = SourceQueryProgress{ + Working: 0, + Stalled: 0, + Complete: 0, + Error: 1, + Responders: 1, + } + + if err := expected.ExpectToMatch(sq); err != nil { + t.Error(err) + } + }) + + if sq.allDone() == false { + t.Error("expected allDone() to be true") + } +} + +func TestStart(t *testing.T) { + t.Parallel() + + ctx := t.Context() + tc := TestConnection{IgnoreNoResponders: true} + + responses := make(chan *QueryResponse, 128) + // this emulates a source + sourceHit := atomic.Bool{} + + _, err := tc.Subscribe(fmt.Sprintf("request.scope.%v", query.GetScope()), func(msg *nats.Msg) { + sourceHit.Store(true) + response := QueryResponse{ + ResponseType: &QueryResponse_NewItem{ + NewItem: &item, + }, + } + // Test that the handlers work + err := tc.Publish(ctx, query.Subject(), &response) + if err != nil { + t.Fatal(err) + } + }) + if err != nil { + t.Fatal(err) + } + + _, err = RunSourceQuery(ctx, &query, DefaultStartTimeout, &tc, responses) + if err != nil { + t.Fatal(err) + } + + response := <-responses + + tc.MessagesMu.Lock() + if len(tc.Messages) != 2 { + t.Errorf("expected 2 messages to be sent, got %v", len(tc.Messages)) + } + tc.MessagesMu.Unlock() + + returnedItem := response.GetNewItem() + if returnedItem == nil { + t.Fatal("expected item to be returned") + } + if returnedItem.Hash() != item.Hash() { + t.Error("item hash mismatch") + } + if !sourceHit.Load() { + t.Error("source was not hit") + } +} + +func TestExecute(t *testing.T) { + t.Parallel() + + t.Run("with no responders", func(t *testing.T) { + conn := TestConnection{} + _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) {}) + if err != nil { + t.Fatal(err) + } + u := uuid.New() + q := Query{ + Type: "user", + Method: QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "global", + IgnoreCache: false, + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), + } + + _, _, _, err = RunSourceQuerySync(t.Context(), &q, 100*time.Millisecond, &conn) + + if err != nil { + t.Fatal(err) + } + }) + + t.Run("with a full response set", func(t *testing.T) { + conn := TestConnection{} + _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) {}) + if err != nil { + t.Fatal(err) + } + u := uuid.New() + q := Query{ + Type: "user", + Method: QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 0, + }, + Scope: "global", + IgnoreCache: false, + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(10 * time.Second)), + } + + querySent := make(chan struct{}) + done := make(chan struct{}) + + go func() { + defer close(done) + // wait for the query to be sent + <-querySent + + ru1 := uuid.New() + + err := conn.Publish(context.Background(), q.Subject(), &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + UUID: q.GetUUID(), + NextUpdateIn: &durationpb.Duration{ + Seconds: 10, + Nanos: 0, + }, + }, + }, + }) + if err != nil { + t.Error(err) + } + + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ + ResponseType: &QueryResponse_NewItem{ + NewItem: &item, + }, + }) + if err != nil { + t.Error(err) + } + + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ + ResponseType: &QueryResponse_NewItem{ + NewItem: &item, + }, + }) + if err != nil { + t.Error(err) + } + + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ + ResponseType: &QueryResponse_Response{ + Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_COMPLETE, + UUID: q.GetUUID(), + }, + }, + }) + if err != nil { + t.Error(err) + } + }() + + responseChan := make(chan *QueryResponse) + // items, _, errs, err := RunSourceQuerySync(t.Context(), &q, DefaultStartTimeout, &conn) + _, err = RunSourceQuery(t.Context(), &q, DefaultStartTimeout, &conn, responseChan) + if err != nil { + t.Fatal(err) + } + + close(querySent) + + items := []*Item{} + errs := []*QueryError{} + + for r := range responseChan { + if r == nil { + t.Fatal("expected a response") + } + switch r.GetResponseType().(type) { + case *QueryResponse_NewItem: + items = append(items, r.GetNewItem()) + case *QueryResponse_Error: + errs = append(errs, r.GetError()) + default: + t.Errorf("unexpected response type: %T", r.GetResponseType()) + } + } + + <-done + + if len(items) != 2 { + t.Errorf("expected 2 items got %v: %v", len(items), items) + } + + if len(errs) != 0 { + t.Errorf("expected 0 errors got %v: %v", len(errs), errs) + } + }) +} + +func TestRealNats(t *testing.T) { + nc, err := nats.Connect("nats://localhost,nats://nats") + if err != nil { + t.Skip("No NATS connection") + } + + enc := EncodedConnectionImpl{Conn: nc} + + u := uuid.New() + q := Query{ + Type: "person", + Method: QueryMethod_GET, + Query: "dylan", + Scope: "global", + UUID: u[:], + } + + ru1 := uuid.New() + + ready := make(chan bool) + + go func() { + _, err := enc.Subscribe("request.scope.global", NewQueryHandler("test", func(ctx context.Context, handledQuery *Query) { + delay := 100 * time.Millisecond + + time.Sleep(delay) + + err := enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_WORKING, + UUID: q.GetUUID(), + NextUpdateIn: &durationpb.Duration{ + Seconds: 10, + Nanos: 0, + }, + }}}) + if err != nil { + t.Error(err) + } + + time.Sleep(delay) + + err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: &item}}) + if err != nil { + t.Error(err) + } + + err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: &item}}) + if err != nil { + t.Error(err) + } + + err = enc.Publish(ctx, q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: ru1[:], + State: ResponderState_COMPLETE, + UUID: q.GetUUID(), + }}}) + if err != nil { + t.Error(err) + } + })) + if err != nil { + t.Error(err) + } + ready <- true + }() + + <-ready + + slowChan := make(chan *QueryResponse) + + _, err = RunSourceQuery(t.Context(), &q, DefaultStartTimeout, &enc, slowChan) + if err != nil { + t.Fatal(err) + } + + for i := range slowChan { + time.Sleep(100 * time.Millisecond) + + t.Log(i) + } +} + +func TestFastFinisher(t *testing.T) { + t.Parallel() + + // Test for a situation where there is one responder that finishes really + // quickly and results in the other responders not getting a chance to start + conn := TestConnection{} + + fast := uuid.New() + slow := uuid.New() + + // Set up the fast responder, it should respond immediately and take only + // 100ms to complete its work + _, err := conn.Subscribe("request.scope.global", func(msg *nats.Msg) { + // Make sure this is the request + var q Query + + err := proto.Unmarshal(msg.Data, &q) + + if err != nil { + t.Error(err) + } + + // Respond immediately saying we're started + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: fast[:], + State: ResponderState_WORKING, + UUID: q.GetUUID(), + NextUpdateIn: &durationpb.Duration{ + Seconds: 1, + Nanos: 0, + }, + }}}) + if err != nil { + t.Fatal(err) + } + + time.Sleep(100 * time.Millisecond) + + // Send an item + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: newItem()}}) + if err != nil { + t.Fatal(err) + } + + // Send a complete message + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: fast[:], + State: ResponderState_COMPLETE, + UUID: q.GetUUID(), + }}}) + if err != nil { + t.Fatal(err) + } + }) + if err != nil { + t.Fatal(err) + } + + // Set up another responder that takes 250ms to start + _, err = conn.Subscribe("request.scope.global", func(msg *nats.Msg) { + // Unmarshal the query + var q Query + + err := proto.Unmarshal(msg.Data, &q) + + if err != nil { + t.Error(err) + } + + // Wait 250ms before starting + time.Sleep(250 * time.Millisecond) + + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: slow[:], + State: ResponderState_WORKING, + UUID: q.GetUUID(), + NextUpdateIn: &durationpb.Duration{ + Seconds: 1, + Nanos: 0, + }, + }}}) + if err != nil { + t.Fatal(err) + } + + // Send an item + item := newItem() + err = item.GetAttributes().Set("name", "baz") + if err != nil { + t.Fatal(err) + } + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: item}}) + if err != nil { + t.Fatal(err) + } + + // Send a complete message + err = conn.Publish(context.Background(), q.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: "test", + ResponderUUID: slow[:], + State: ResponderState_COMPLETE, + UUID: q.GetUUID(), + }}}) + if err != nil { + t.Fatal(err) + } + }) + if err != nil { + t.Fatal(err) + } + + items, _, errs, err := RunSourceQuerySync(t.Context(), newQuery(), 500*time.Millisecond, &conn) + + if err != nil { + t.Fatal(err) + } + + if len(items) != 2 { + t.Errorf("Expected 2 items, got %d: %v", len(items), items) + } + + if len(errs) != 0 { + t.Errorf("Expected 0 errors, got %d: %v", len(errs), errs) + } +} + +// This source will simply respond to any query that it sent with a configured +// number of items, and configurable delays. This is designed to replicate a +// real system at scale +type SimpleSource struct { + // How many items to return from the query + NumItemsReturn int + + // How long to wait before starting work on the query + StartDelay time.Duration + + // How long to wait before sending each item + PerItemDelay time.Duration + + // How long to wait before sending the completion message + CompletionDelay time.Duration + + // The connection to use + Conn *TestConnection + + // The probability of stalling where 0 is no stall and 1 is always stall + StallProbability float64 + + // The probability of failing where 0 is no fail and 1 is always fail + FailProbability float64 + + // The responder name to use + ResponderName string +} + +func (s *SimpleSource) Start(ctx context.Context, t *testing.T) { + // ignore errors from test connection + _, _ = s.Conn.Subscribe("request.>", func(msg *nats.Msg) { + // Run these in parallel + go func(msg *nats.Msg) { + query := &Query{} + + err := Unmarshal(ctx, msg.Data, query) + if err != nil { + panic(fmt.Errorf("Unmarshal(%v): %w", query, err)) + } + + // Create the number of items that were requested + items := make([]*Item, s.NumItemsReturn) + for i := range s.NumItemsReturn { + items[i] = newItem() + } + + // Make a UUID for yourself + responderUUID := uuid.New() + + // Wait for the start delay + time.Sleep(s.StartDelay) + + // Calculate the expected duration of the query + expectedQueryDuration := (s.PerItemDelay * time.Duration(s.NumItemsReturn)) + s.CompletionDelay + 500*time.Millisecond + + err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: s.ResponderName, + ResponderUUID: responderUUID[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(expectedQueryDuration), + UUID: query.GetUUID(), + }}}) + if err != nil { + t.Errorf("error publishing response: %v", err) + } + + for _, item := range items { + time.Sleep(s.PerItemDelay) + err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_NewItem{NewItem: item}}) + if err != nil { + t.Errorf("error publishing item: %v", err) + } + } + + // Stall with a certain probability + if rand.Float64() < s.StallProbability { + return + } + + // Fail with a certain probability + if rand.Float64() < s.FailProbability { + err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: s.ResponderName, + ResponderUUID: responderUUID[:], + State: ResponderState_ERROR, + UUID: query.GetUUID(), + }}}) + if err != nil { + t.Errorf("error publishing response: %v", err) + } + return + } + + time.Sleep(s.CompletionDelay) + err = s.Conn.Publish(ctx, query.Subject(), &QueryResponse{ResponseType: &QueryResponse_Response{Response: &Response{ + Responder: s.ResponderName, + ResponderUUID: responderUUID[:], + State: ResponderState_COMPLETE, + UUID: query.GetUUID(), + }}}) + if err != nil { + t.Errorf("error publishing response: %v", err) + } + }(msg) + }) +} + +func TestMassiveScale(t *testing.T) { + t.Parallel() + + if _, exists := os.LookupEnv("GITHUB_ACTIONS"); exists { + // Note that in these tests we can push things even further, to 10,000 + // sources for example. The problem is that once the CPU is context + // switching too heavily you end up in a position where the sources + // start getting marked as stalled as they don't have enough CPU to send + // their messages quickly enough and they blow through their expected + // timeout. + // + // They can also fail locally when using -race as this puts a lot more + // load on the CPU than there would normally be + t.Skip("These tests are too flaky due to reliance on wall clock time and fast timings") + } + + tests := []struct { + // The number of sources to create + NumSources int + // The maximum time to wait before starting + MaxStartDelayMilliseconds int + // The maximum time to wait between items + MaxPerItemDelayMilliseconds int + // The maximum time to wait before completion + MaxCompletionDelayMilliseconds int + // The maximum number of items to return + MaxItemsToReturn int + // The probability of a source stalling where 0 is no stall and 1 is + // always stall + StallProbability float64 + // The probability of a source failing where 0 is no fail and 1 is + // always fail + FailProbability float64 + // How long to give sources to start responding, over and above the + // maxStartDelayMilliseconds + StartDelayGracePeriodMilliseconds int + }{ + { + NumSources: 100, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.0, + FailProbability: 0.0, + StartDelayGracePeriodMilliseconds: 100, + }, + { + NumSources: 1_000, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.0, + FailProbability: 0.0, + StartDelayGracePeriodMilliseconds: 100, + }, + { + NumSources: 100, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.3, + FailProbability: 0.0, + StartDelayGracePeriodMilliseconds: 100, + }, + { + NumSources: 1_000, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.3, + FailProbability: 0.0, + StartDelayGracePeriodMilliseconds: 100, + }, + { + NumSources: 100, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.3, + FailProbability: 0.3, + StartDelayGracePeriodMilliseconds: 100, + }, + { + NumSources: 1_000, + MaxStartDelayMilliseconds: 100, + MaxPerItemDelayMilliseconds: 10, + MaxCompletionDelayMilliseconds: 100, + MaxItemsToReturn: 100, + StallProbability: 0.3, + FailProbability: 0.3, + StartDelayGracePeriodMilliseconds: 100, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("NumSources %v, MaxStartDelay %v, MaxPerItemDelay %v, MaxCompletionDelay %v, MaxItemsToReturn %v, StallProbability %v, FailProbability %v, StartDelayGracePeriod %v", + test.NumSources, + test.MaxStartDelayMilliseconds, + test.MaxPerItemDelayMilliseconds, + test.MaxCompletionDelayMilliseconds, + test.MaxItemsToReturn, + test.StallProbability, + test.FailProbability, + test.StartDelayGracePeriodMilliseconds, + ), func(t *testing.T) { + tConn := TestConnection{} + + // Generate a random duration between 0 and maxDuration + randomDuration := func(maxDuration int) time.Duration { + return time.Duration(rand.Intn(maxDuration)) * time.Millisecond + } + + expectedItems := 0 + + // Start all the sources + sources := make([]*SimpleSource, test.NumSources) + for i := range sources { + numItemsReturn := rand.Intn(test.MaxItemsToReturn) + expectedItems += numItemsReturn // Count how many items we expect to receive + startDelay := randomDuration(test.MaxStartDelayMilliseconds) + perItemDelay := randomDuration(test.MaxPerItemDelayMilliseconds) + completionDelay := randomDuration(test.MaxCompletionDelayMilliseconds) + + sources[i] = &SimpleSource{ + NumItemsReturn: numItemsReturn, + StartDelay: startDelay, + PerItemDelay: perItemDelay, + CompletionDelay: completionDelay, + StallProbability: test.StallProbability, + FailProbability: test.FailProbability, + Conn: &tConn, + ResponderName: fmt.Sprintf("NumItems %v, StartDelay %v, PerItemDelay %v CompletionDelay %v", + numItemsReturn, + startDelay.String(), + perItemDelay.String(), + completionDelay.String(), + ), + } + + sources[i].Start(context.Background(), t) + } + + // Create the query + u := uuid.New() + q := Query{ + Type: "massive-scale-test", + Method: QueryMethod_GET, + Query: "GO!!!!!", + Scope: "test", + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(60 * time.Second)), + } + + responseChan := make(chan *QueryResponse) + doneChan := make(chan struct{}) + + // Begin handling the responses + actualItems := 0 + go func() { + for { + select { + case <-t.Context().Done(): + return + case response, ok := <-responseChan: + if !ok { + // Channel closed + close(doneChan) + return + } + + switch response.GetResponseType().(type) { + case *QueryResponse_NewItem: + actualItems++ + } + } + } + }() + + // Start the query + startTimeout := time.Duration(test.MaxStartDelayMilliseconds+test.StartDelayGracePeriodMilliseconds) * time.Millisecond + qp, err := RunSourceQuery(t.Context(), &q, startTimeout, &tConn, responseChan) + if err != nil { + t.Fatal(err) + } + + // Wait for the query to finish + <-doneChan + + if actualItems != expectedItems { + t.Errorf("Expected %v items, got %v", expectedItems, actualItems) + } + + progress := qp.Progress() + + if progress.Responders != test.NumSources { + t.Errorf("Expected %v responders, got %v", test.NumSources, progress.Responders) + } + + fmt.Printf("Num Complete: %v\n", progress.Complete) + fmt.Printf("Num Working: %v\n", progress.Working) + fmt.Printf("Num Stalled: %v\n", progress.Stalled) + fmt.Printf("Num Error: %v\n", progress.Error) + fmt.Printf("Num Cancelled: %v\n", progress.Cancelled) + fmt.Printf("Num Responders: %v\n", progress.Responders) + fmt.Printf("Num Items: %v\n", actualItems) + }) + } +} diff --git a/go/sdp-go/proto_clone_test.go b/go/sdp-go/proto_clone_test.go new file mode 100644 index 00000000..0a73c4eb --- /dev/null +++ b/go/sdp-go/proto_clone_test.go @@ -0,0 +1,146 @@ +package sdp + +import ( + "testing" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// TestProtoCloneReplacesCustomCopy validates that proto.Clone works correctly +// for all SDP types and can replace the custom Copy methods +func TestProtoCloneReplacesCustomCopy(t *testing.T) { + t.Run("Reference with all fields", func(t *testing.T) { + original := &Reference{ + Type: "test", + UniqueAttributeValue: "value", + Scope: "scope", + IsQuery: true, + Method: QueryMethod_SEARCH, + Query: "search-term", + } + + cloned := proto.Clone(original).(*Reference) + + if !proto.Equal(original, cloned) { + t.Errorf("proto.Clone failed for Reference: %+v != %+v", original, cloned) + } + + // Specifically check the fields that Copy() was missing + if cloned.GetIsQuery() != original.GetIsQuery() { + t.Errorf("IsQuery field not cloned correctly: got %v, want %v", cloned.GetIsQuery(), original.GetIsQuery()) + } + if cloned.GetMethod() != original.GetMethod() { + t.Errorf("Method field not cloned correctly: got %v, want %v", cloned.GetMethod(), original.GetMethod()) + } + if cloned.GetQuery() != original.GetQuery() { + t.Errorf("Query field not cloned correctly: got %v, want %v", cloned.GetQuery(), original.GetQuery()) + } + }) + + t.Run("Query with all fields", func(t *testing.T) { + u := uuid.New() + original := &Query{ + Type: "test", + Method: QueryMethod_GET, + Query: "value", + Scope: "scope", + UUID: u[:], + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 5, + FollowOnlyBlastPropagation: true, + }, + IgnoreCache: true, + Deadline: timestamppb.Now(), + } + + cloned := proto.Clone(original).(*Query) + + if !proto.Equal(original, cloned) { + t.Errorf("proto.Clone failed for Query: %+v != %+v", original, cloned) + } + }) + + t.Run("Item with all fields", func(t *testing.T) { + original := &Item{ + Type: "test", + UniqueAttribute: "id", + Scope: "scope", + Metadata: &Metadata{ + SourceName: "test-source", + Hidden: true, + Timestamp: timestamppb.Now(), + }, + Health: Health_HEALTH_OK.Enum(), + Tags: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + + // Add attributes + attrs, err := ToAttributes(map[string]interface{}{ + "name": "test-item", + "port": 8080, + }) + if err != nil { + t.Fatal(err) + } + original.Attributes = attrs + + cloned := proto.Clone(original).(*Item) + + if !proto.Equal(original, cloned) { + t.Errorf("proto.Clone failed for Item: %+v != %+v", original, cloned) + } + }) + + t.Run("All other SDP types", func(t *testing.T) { + // BlastPropagation + bp := &BlastPropagation{In: true, Out: false} + bpClone := proto.Clone(bp).(*BlastPropagation) + if !proto.Equal(bp, bpClone) { + t.Errorf("proto.Clone failed for BlastPropagation") + } + + // LinkedItemQuery + liq := &LinkedItemQuery{ + Query: &Query{Type: "test", Method: QueryMethod_LIST}, + BlastPropagation: bp, + } + liqClone := proto.Clone(liq).(*LinkedItemQuery) + if !proto.Equal(liq, liqClone) { + t.Errorf("proto.Clone failed for LinkedItemQuery") + } + + // LinkedItem + li := &LinkedItem{ + Item: &Reference{Type: "test", Scope: "scope"}, + BlastPropagation: bp, + } + liClone := proto.Clone(li).(*LinkedItem) + if !proto.Equal(li, liClone) { + t.Errorf("proto.Clone failed for LinkedItem") + } + + // Metadata + metadata := &Metadata{ + SourceName: "test-source", + Hidden: true, + Timestamp: timestamppb.Now(), + } + metadataClone := proto.Clone(metadata).(*Metadata) + if !proto.Equal(metadata, metadataClone) { + t.Errorf("proto.Clone failed for Metadata") + } + + // CancelQuery + u := uuid.New() + cancelQuery := &CancelQuery{UUID: u[:]} + cancelQueryClone := proto.Clone(cancelQuery).(*CancelQuery) + if !proto.Equal(cancelQuery, cancelQueryClone) { + t.Errorf("proto.Clone failed for CancelQuery") + } + }) +} diff --git a/go/sdp-go/responses.go b/go/sdp-go/responses.go new file mode 100644 index 00000000..1d23b94e --- /dev/null +++ b/go/sdp-go/responses.go @@ -0,0 +1,27 @@ +package sdp + +// TODO: instead of translating, unify this +func (r *Response) ToQueryStatus() *QueryStatus { + return &QueryStatus{ + UUID: r.GetUUID(), + Status: r.GetState().ToQueryStatus(), + } +} + +// TODO: instead of translating, unify this +func (r ResponderState) ToQueryStatus() QueryStatus_Status { + switch r { + case ResponderState_WORKING: + return QueryStatus_STARTED + case ResponderState_COMPLETE: + return QueryStatus_FINISHED + case ResponderState_ERROR: + return QueryStatus_ERRORED + case ResponderState_CANCELLED: + return QueryStatus_CANCELLED + case ResponderState_STALLED: + return QueryStatus_ERRORED + default: + return QueryStatus_UNSPECIFIED + } +} diff --git a/go/sdp-go/responses.pb.go b/go/sdp-go/responses.pb.go new file mode 100644 index 00000000..6d1628a7 --- /dev/null +++ b/go/sdp-go/responses.pb.go @@ -0,0 +1,246 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: responses.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ResponderState represents the state of the responder, note that both +// COMPLETE and ERROR are completion states i.e. do not expect any more items +// to be returned from the query +type ResponderState int32 + +const ( + // The responder is still gathering data + ResponderState_WORKING ResponderState = 0 + // The query is complete + ResponderState_COMPLETE ResponderState = 1 + // All sources have returned errors + ResponderState_ERROR ResponderState = 2 + // Work has been cancelled while in progress + ResponderState_CANCELLED ResponderState = 3 + // The responder has not set a response in the expected interval + ResponderState_STALLED ResponderState = 4 +) + +// Enum value maps for ResponderState. +var ( + ResponderState_name = map[int32]string{ + 0: "WORKING", + 1: "COMPLETE", + 2: "ERROR", + 3: "CANCELLED", + 4: "STALLED", + } + ResponderState_value = map[string]int32{ + "WORKING": 0, + "COMPLETE": 1, + "ERROR": 2, + "CANCELLED": 3, + "STALLED": 4, + } +) + +func (x ResponderState) Enum() *ResponderState { + p := new(ResponderState) + *p = x + return p +} + +func (x ResponderState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResponderState) Descriptor() protoreflect.EnumDescriptor { + return file_responses_proto_enumTypes[0].Descriptor() +} + +func (ResponderState) Type() protoreflect.EnumType { + return &file_responses_proto_enumTypes[0] +} + +func (x ResponderState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResponderState.Descriptor instead. +func (ResponderState) EnumDescriptor() ([]byte, []int) { + return file_responses_proto_rawDescGZIP(), []int{0} +} + +// Response is returned when a query is made +type Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the responder that is working on a response. This is purely + // informational + Responder string `protobuf:"bytes,1,opt,name=responder,proto3" json:"responder,omitempty"` + // The state of the responder + State ResponderState `protobuf:"varint,2,opt,name=state,proto3,enum=ResponderState" json:"state,omitempty"` + // The timespan within which to expect the next update. (e.g. 10s) If no + // further interim responses are received within this time the connection + // can be considered stale and the requester may give up + NextUpdateIn *durationpb.Duration `protobuf:"bytes,3,opt,name=nextUpdateIn,proto3" json:"nextUpdateIn,omitempty"` + // UUID of the item query that this response is in relation to (in binary + // format) + UUID []byte `protobuf:"bytes,4,opt,name=UUID,proto3" json:"UUID,omitempty"` + // The ID of the responder that is working on a response. This is used for + // internal bookkeeping and should remain constant for the duration of a + // request, preferably over the lifetime of the source process. + ResponderUUID []byte `protobuf:"bytes,5,opt,name=responderUUID,proto3" json:"responderUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Response) Reset() { + *x = Response{} + mi := &file_responses_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_responses_proto_msgTypes[0] + 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 Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_responses_proto_rawDescGZIP(), []int{0} +} + +func (x *Response) GetResponder() string { + if x != nil { + return x.Responder + } + return "" +} + +func (x *Response) GetState() ResponderState { + if x != nil { + return x.State + } + return ResponderState_WORKING +} + +func (x *Response) GetNextUpdateIn() *durationpb.Duration { + if x != nil { + return x.NextUpdateIn + } + return nil +} + +func (x *Response) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *Response) GetResponderUUID() []byte { + if x != nil { + return x.ResponderUUID + } + return nil +} + +var File_responses_proto protoreflect.FileDescriptor + +const file_responses_proto_rawDesc = "" + + "\n" + + "\x0fresponses.proto\x1a\x1egoogle/protobuf/duration.proto\"\xc8\x01\n" + + "\bResponse\x12\x1c\n" + + "\tresponder\x18\x01 \x01(\tR\tresponder\x12%\n" + + "\x05state\x18\x02 \x01(\x0e2\x0f.ResponderStateR\x05state\x12=\n" + + "\fnextUpdateIn\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\fnextUpdateIn\x12\x12\n" + + "\x04UUID\x18\x04 \x01(\fR\x04UUID\x12$\n" + + "\rresponderUUID\x18\x05 \x01(\fR\rresponderUUID*R\n" + + "\x0eResponderState\x12\v\n" + + "\aWORKING\x10\x00\x12\f\n" + + "\bCOMPLETE\x10\x01\x12\t\n" + + "\x05ERROR\x10\x02\x12\r\n" + + "\tCANCELLED\x10\x03\x12\v\n" + + "\aSTALLED\x10\x04B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_responses_proto_rawDescOnce sync.Once + file_responses_proto_rawDescData []byte +) + +func file_responses_proto_rawDescGZIP() []byte { + file_responses_proto_rawDescOnce.Do(func() { + file_responses_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_responses_proto_rawDesc), len(file_responses_proto_rawDesc))) + }) + return file_responses_proto_rawDescData +} + +var file_responses_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_responses_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_responses_proto_goTypes = []any{ + (ResponderState)(0), // 0: ResponderState + (*Response)(nil), // 1: Response + (*durationpb.Duration)(nil), // 2: google.protobuf.Duration +} +var file_responses_proto_depIdxs = []int32{ + 0, // 0: Response.state:type_name -> ResponderState + 2, // 1: Response.nextUpdateIn:type_name -> google.protobuf.Duration + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_responses_proto_init() } +func file_responses_proto_init() { + if File_responses_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_responses_proto_rawDesc), len(file_responses_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_responses_proto_goTypes, + DependencyIndexes: file_responses_proto_depIdxs, + EnumInfos: file_responses_proto_enumTypes, + MessageInfos: file_responses_proto_msgTypes, + }.Build() + File_responses_proto = out.File + file_responses_proto_goTypes = nil + file_responses_proto_depIdxs = nil +} diff --git a/go/sdp-go/revlink.pb.go b/go/sdp-go/revlink.pb.go new file mode 100644 index 00000000..a1d2fa2b --- /dev/null +++ b/go/sdp-go/revlink.pb.go @@ -0,0 +1,447 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: revlink.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetReverseEdgesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The account that the item belongs to + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + // The item that you would like to find reverse edges for + Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + // set to true to only return edges that propagate configuration change impact + FollowOnlyBlastPropagation bool `protobuf:"varint,3,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetReverseEdgesRequest) Reset() { + *x = GetReverseEdgesRequest{} + mi := &file_revlink_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetReverseEdgesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetReverseEdgesRequest) ProtoMessage() {} + +func (x *GetReverseEdgesRequest) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[0] + 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 GetReverseEdgesRequest.ProtoReflect.Descriptor instead. +func (*GetReverseEdgesRequest) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{0} +} + +func (x *GetReverseEdgesRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *GetReverseEdgesRequest) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *GetReverseEdgesRequest) GetFollowOnlyBlastPropagation() bool { + if x != nil { + return x.FollowOnlyBlastPropagation + } + return false +} + +type GetReverseEdgesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The edges to the requested item + Edges []*Edge `protobuf:"bytes,1,rep,name=edges,proto3" json:"edges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetReverseEdgesResponse) Reset() { + *x = GetReverseEdgesResponse{} + mi := &file_revlink_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetReverseEdgesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetReverseEdgesResponse) ProtoMessage() {} + +func (x *GetReverseEdgesResponse) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[1] + 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 GetReverseEdgesResponse.ProtoReflect.Descriptor instead. +func (*GetReverseEdgesResponse) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{1} +} + +func (x *GetReverseEdgesResponse) GetEdges() []*Edge { + if x != nil { + return x.Edges + } + return nil +} + +type IngestGatewayResponseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The account that the response belongs to + Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` + // The response type to ingest + // + // Types that are valid to be assigned to ResponseType: + // + // *IngestGatewayResponseRequest_NewItem + // *IngestGatewayResponseRequest_NewEdge + ResponseType isIngestGatewayResponseRequest_ResponseType `protobuf_oneof:"response_type"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestGatewayResponseRequest) Reset() { + *x = IngestGatewayResponseRequest{} + mi := &file_revlink_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestGatewayResponseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestGatewayResponseRequest) ProtoMessage() {} + +func (x *IngestGatewayResponseRequest) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[2] + 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 IngestGatewayResponseRequest.ProtoReflect.Descriptor instead. +func (*IngestGatewayResponseRequest) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{2} +} + +func (x *IngestGatewayResponseRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *IngestGatewayResponseRequest) GetResponseType() isIngestGatewayResponseRequest_ResponseType { + if x != nil { + return x.ResponseType + } + return nil +} + +func (x *IngestGatewayResponseRequest) GetNewItem() *Item { + if x != nil { + if x, ok := x.ResponseType.(*IngestGatewayResponseRequest_NewItem); ok { + return x.NewItem + } + } + return nil +} + +func (x *IngestGatewayResponseRequest) GetNewEdge() *Edge { + if x != nil { + if x, ok := x.ResponseType.(*IngestGatewayResponseRequest_NewEdge); ok { + return x.NewEdge + } + } + return nil +} + +type isIngestGatewayResponseRequest_ResponseType interface { + isIngestGatewayResponseRequest_ResponseType() +} + +type IngestGatewayResponseRequest_NewItem struct { + NewItem *Item `protobuf:"bytes,2,opt,name=newItem,proto3,oneof"` // A new item that has been discovered +} + +type IngestGatewayResponseRequest_NewEdge struct { + NewEdge *Edge `protobuf:"bytes,3,opt,name=newEdge,proto3,oneof"` // A new edge between two items +} + +func (*IngestGatewayResponseRequest_NewItem) isIngestGatewayResponseRequest_ResponseType() {} + +func (*IngestGatewayResponseRequest_NewEdge) isIngestGatewayResponseRequest_ResponseType() {} + +type IngestGatewayResponsesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NumItemsReceived int32 `protobuf:"varint,1,opt,name=numItemsReceived,proto3" json:"numItemsReceived,omitempty"` + NumEdgesReceived int32 `protobuf:"varint,2,opt,name=numEdgesReceived,proto3" json:"numEdgesReceived,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IngestGatewayResponsesResponse) Reset() { + *x = IngestGatewayResponsesResponse{} + mi := &file_revlink_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IngestGatewayResponsesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IngestGatewayResponsesResponse) ProtoMessage() {} + +func (x *IngestGatewayResponsesResponse) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[3] + 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 IngestGatewayResponsesResponse.ProtoReflect.Descriptor instead. +func (*IngestGatewayResponsesResponse) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{3} +} + +func (x *IngestGatewayResponsesResponse) GetNumItemsReceived() int32 { + if x != nil { + return x.NumItemsReceived + } + return 0 +} + +func (x *IngestGatewayResponsesResponse) GetNumEdgesReceived() int32 { + if x != nil { + return x.NumEdgesReceived + } + return 0 +} + +type CheckpointRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckpointRequest) Reset() { + *x = CheckpointRequest{} + mi := &file_revlink_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckpointRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckpointRequest) ProtoMessage() {} + +func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[4] + 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 CheckpointRequest.ProtoReflect.Descriptor instead. +func (*CheckpointRequest) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{4} +} + +type CheckpointResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckpointResponse) Reset() { + *x = CheckpointResponse{} + mi := &file_revlink_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckpointResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckpointResponse) ProtoMessage() {} + +func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { + mi := &file_revlink_proto_msgTypes[5] + 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 CheckpointResponse.ProtoReflect.Descriptor instead. +func (*CheckpointResponse) Descriptor() ([]byte, []int) { + return file_revlink_proto_rawDescGZIP(), []int{5} +} + +var File_revlink_proto protoreflect.FileDescriptor + +const file_revlink_proto_rawDesc = "" + + "\n" + + "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x92\x01\n" + + "\x16GetReverseEdgesRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x12\x1e\n" + + "\x04item\x18\x02 \x01(\v2\n" + + ".ReferenceR\x04item\x12>\n" + + "\x1afollowOnlyBlastPropagation\x18\x03 \x01(\bR\x1afollowOnlyBlastPropagation\"6\n" + + "\x17GetReverseEdgesResponse\x12\x1b\n" + + "\x05edges\x18\x01 \x03(\v2\x05.EdgeR\x05edges\"\x8f\x01\n" + + "\x1cIngestGatewayResponseRequest\x12\x18\n" + + "\aaccount\x18\x01 \x01(\tR\aaccount\x12!\n" + + "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12!\n" + + "\anewEdge\x18\x03 \x01(\v2\x05.EdgeH\x00R\anewEdgeB\x0f\n" + + "\rresponse_type\"x\n" + + "\x1eIngestGatewayResponsesResponse\x12*\n" + + "\x10numItemsReceived\x18\x01 \x01(\x05R\x10numItemsReceived\x12*\n" + + "\x10numEdgesReceived\x18\x02 \x01(\x05R\x10numEdgesReceived\"\x13\n" + + "\x11CheckpointRequest\"\x14\n" + + "\x12CheckpointResponse2\x99\x02\n" + + "\x0eRevlinkService\x12T\n" + + "\x0fGetReverseEdges\x12\x1f.revlink.GetReverseEdgesRequest\x1a .revlink.GetReverseEdgesResponse\x12j\n" + + "\x16IngestGatewayResponses\x12%.revlink.IngestGatewayResponseRequest\x1a'.revlink.IngestGatewayResponsesResponse(\x01\x12E\n" + + "\n" + + "Checkpoint\x12\x1a.revlink.CheckpointRequest\x1a\x1b.revlink.CheckpointResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_revlink_proto_rawDescOnce sync.Once + file_revlink_proto_rawDescData []byte +) + +func file_revlink_proto_rawDescGZIP() []byte { + file_revlink_proto_rawDescOnce.Do(func() { + file_revlink_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_revlink_proto_rawDesc), len(file_revlink_proto_rawDesc))) + }) + return file_revlink_proto_rawDescData +} + +var file_revlink_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_revlink_proto_goTypes = []any{ + (*GetReverseEdgesRequest)(nil), // 0: revlink.GetReverseEdgesRequest + (*GetReverseEdgesResponse)(nil), // 1: revlink.GetReverseEdgesResponse + (*IngestGatewayResponseRequest)(nil), // 2: revlink.IngestGatewayResponseRequest + (*IngestGatewayResponsesResponse)(nil), // 3: revlink.IngestGatewayResponsesResponse + (*CheckpointRequest)(nil), // 4: revlink.CheckpointRequest + (*CheckpointResponse)(nil), // 5: revlink.CheckpointResponse + (*Reference)(nil), // 6: Reference + (*Edge)(nil), // 7: Edge + (*Item)(nil), // 8: Item +} +var file_revlink_proto_depIdxs = []int32{ + 6, // 0: revlink.GetReverseEdgesRequest.item:type_name -> Reference + 7, // 1: revlink.GetReverseEdgesResponse.edges:type_name -> Edge + 8, // 2: revlink.IngestGatewayResponseRequest.newItem:type_name -> Item + 7, // 3: revlink.IngestGatewayResponseRequest.newEdge:type_name -> Edge + 0, // 4: revlink.RevlinkService.GetReverseEdges:input_type -> revlink.GetReverseEdgesRequest + 2, // 5: revlink.RevlinkService.IngestGatewayResponses:input_type -> revlink.IngestGatewayResponseRequest + 4, // 6: revlink.RevlinkService.Checkpoint:input_type -> revlink.CheckpointRequest + 1, // 7: revlink.RevlinkService.GetReverseEdges:output_type -> revlink.GetReverseEdgesResponse + 3, // 8: revlink.RevlinkService.IngestGatewayResponses:output_type -> revlink.IngestGatewayResponsesResponse + 5, // 9: revlink.RevlinkService.Checkpoint:output_type -> revlink.CheckpointResponse + 7, // [7:10] is the sub-list for method output_type + 4, // [4:7] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_revlink_proto_init() } +func file_revlink_proto_init() { + if File_revlink_proto != nil { + return + } + file_items_proto_init() + file_revlink_proto_msgTypes[2].OneofWrappers = []any{ + (*IngestGatewayResponseRequest_NewItem)(nil), + (*IngestGatewayResponseRequest_NewEdge)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_revlink_proto_rawDesc), len(file_revlink_proto_rawDesc)), + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_revlink_proto_goTypes, + DependencyIndexes: file_revlink_proto_depIdxs, + MessageInfos: file_revlink_proto_msgTypes, + }.Build() + File_revlink_proto = out.File + file_revlink_proto_goTypes = nil + file_revlink_proto_depIdxs = nil +} diff --git a/go/sdp-go/sdpconnect/account.connect.go b/go/sdp-go/sdpconnect/account.connect.go new file mode 100644 index 00000000..092d05d3 --- /dev/null +++ b/go/sdp-go/sdpconnect/account.connect.go @@ -0,0 +1,1187 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: account.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // AdminServiceName is the fully-qualified name of the AdminService service. + AdminServiceName = "account.AdminService" + // ManagementServiceName is the fully-qualified name of the ManagementService service. + ManagementServiceName = "account.ManagementService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // AdminServiceListAccountsProcedure is the fully-qualified name of the AdminService's ListAccounts + // RPC. + AdminServiceListAccountsProcedure = "/account.AdminService/ListAccounts" + // AdminServiceCreateAccountProcedure is the fully-qualified name of the AdminService's + // CreateAccount RPC. + AdminServiceCreateAccountProcedure = "/account.AdminService/CreateAccount" + // AdminServiceUpdateAccountProcedure is the fully-qualified name of the AdminService's + // UpdateAccount RPC. + AdminServiceUpdateAccountProcedure = "/account.AdminService/UpdateAccount" + // AdminServiceGetAccountProcedure is the fully-qualified name of the AdminService's GetAccount RPC. + AdminServiceGetAccountProcedure = "/account.AdminService/GetAccount" + // AdminServiceDeleteAccountProcedure is the fully-qualified name of the AdminService's + // DeleteAccount RPC. + AdminServiceDeleteAccountProcedure = "/account.AdminService/DeleteAccount" + // AdminServiceListSourcesProcedure is the fully-qualified name of the AdminService's ListSources + // RPC. + AdminServiceListSourcesProcedure = "/account.AdminService/ListSources" + // AdminServiceCreateSourceProcedure is the fully-qualified name of the AdminService's CreateSource + // RPC. + AdminServiceCreateSourceProcedure = "/account.AdminService/CreateSource" + // AdminServiceGetSourceProcedure is the fully-qualified name of the AdminService's GetSource RPC. + AdminServiceGetSourceProcedure = "/account.AdminService/GetSource" + // AdminServiceUpdateSourceProcedure is the fully-qualified name of the AdminService's UpdateSource + // RPC. + AdminServiceUpdateSourceProcedure = "/account.AdminService/UpdateSource" + // AdminServiceDeleteSourceProcedure is the fully-qualified name of the AdminService's DeleteSource + // RPC. + AdminServiceDeleteSourceProcedure = "/account.AdminService/DeleteSource" + // AdminServiceKeepaliveSourcesProcedure is the fully-qualified name of the AdminService's + // KeepaliveSources RPC. + AdminServiceKeepaliveSourcesProcedure = "/account.AdminService/KeepaliveSources" + // AdminServiceCreateTokenProcedure is the fully-qualified name of the AdminService's CreateToken + // RPC. + AdminServiceCreateTokenProcedure = "/account.AdminService/CreateToken" + // ManagementServiceGetAccountProcedure is the fully-qualified name of the ManagementService's + // GetAccount RPC. + ManagementServiceGetAccountProcedure = "/account.ManagementService/GetAccount" + // ManagementServiceDeleteAccountProcedure is the fully-qualified name of the ManagementService's + // DeleteAccount RPC. + ManagementServiceDeleteAccountProcedure = "/account.ManagementService/DeleteAccount" + // ManagementServiceListSourcesProcedure is the fully-qualified name of the ManagementService's + // ListSources RPC. + ManagementServiceListSourcesProcedure = "/account.ManagementService/ListSources" + // ManagementServiceCreateSourceProcedure is the fully-qualified name of the ManagementService's + // CreateSource RPC. + ManagementServiceCreateSourceProcedure = "/account.ManagementService/CreateSource" + // ManagementServiceGetSourceProcedure is the fully-qualified name of the ManagementService's + // GetSource RPC. + ManagementServiceGetSourceProcedure = "/account.ManagementService/GetSource" + // ManagementServiceUpdateSourceProcedure is the fully-qualified name of the ManagementService's + // UpdateSource RPC. + ManagementServiceUpdateSourceProcedure = "/account.ManagementService/UpdateSource" + // ManagementServiceDeleteSourceProcedure is the fully-qualified name of the ManagementService's + // DeleteSource RPC. + ManagementServiceDeleteSourceProcedure = "/account.ManagementService/DeleteSource" + // ManagementServiceListAllSourcesStatusProcedure is the fully-qualified name of the + // ManagementService's ListAllSourcesStatus RPC. + ManagementServiceListAllSourcesStatusProcedure = "/account.ManagementService/ListAllSourcesStatus" + // ManagementServiceListActiveSourcesStatusProcedure is the fully-qualified name of the + // ManagementService's ListActiveSourcesStatus RPC. + ManagementServiceListActiveSourcesStatusProcedure = "/account.ManagementService/ListActiveSourcesStatus" + // ManagementServiceSubmitSourceHeartbeatProcedure is the fully-qualified name of the + // ManagementService's SubmitSourceHeartbeat RPC. + ManagementServiceSubmitSourceHeartbeatProcedure = "/account.ManagementService/SubmitSourceHeartbeat" + // ManagementServiceKeepaliveSourcesProcedure is the fully-qualified name of the ManagementService's + // KeepaliveSources RPC. + ManagementServiceKeepaliveSourcesProcedure = "/account.ManagementService/KeepaliveSources" + // ManagementServiceCreateTokenProcedure is the fully-qualified name of the ManagementService's + // CreateToken RPC. + ManagementServiceCreateTokenProcedure = "/account.ManagementService/CreateToken" + // ManagementServiceRevlinkWarmupProcedure is the fully-qualified name of the ManagementService's + // RevlinkWarmup RPC. + ManagementServiceRevlinkWarmupProcedure = "/account.ManagementService/RevlinkWarmup" + // ManagementServiceListAvailableItemTypesProcedure is the fully-qualified name of the + // ManagementService's ListAvailableItemTypes RPC. + ManagementServiceListAvailableItemTypesProcedure = "/account.ManagementService/ListAvailableItemTypes" + // ManagementServiceGetSourceStatusProcedure is the fully-qualified name of the ManagementService's + // GetSourceStatus RPC. + ManagementServiceGetSourceStatusProcedure = "/account.ManagementService/GetSourceStatus" + // ManagementServiceGetUserOnboardingStatusProcedure is the fully-qualified name of the + // ManagementService's GetUserOnboardingStatus RPC. + ManagementServiceGetUserOnboardingStatusProcedure = "/account.ManagementService/GetUserOnboardingStatus" + // ManagementServiceSetUserOnboardingStatusProcedure is the fully-qualified name of the + // ManagementService's SetUserOnboardingStatus RPC. + ManagementServiceSetUserOnboardingStatusProcedure = "/account.ManagementService/SetUserOnboardingStatus" + // ManagementServiceListTeamMembersProcedure is the fully-qualified name of the ManagementService's + // ListTeamMembers RPC. + ManagementServiceListTeamMembersProcedure = "/account.ManagementService/ListTeamMembers" + // ManagementServiceGetWelcomeScreenInformationProcedure is the fully-qualified name of the + // ManagementService's GetWelcomeScreenInformation RPC. + ManagementServiceGetWelcomeScreenInformationProcedure = "/account.ManagementService/GetWelcomeScreenInformation" + // ManagementServiceSetGithubInstallationIDProcedure is the fully-qualified name of the + // ManagementService's SetGithubInstallationID RPC. + ManagementServiceSetGithubInstallationIDProcedure = "/account.ManagementService/SetGithubInstallationID" + // ManagementServiceUnsetGithubInstallationIDProcedure is the fully-qualified name of the + // ManagementService's UnsetGithubInstallationID RPC. + ManagementServiceUnsetGithubInstallationIDProcedure = "/account.ManagementService/UnsetGithubInstallationID" +) + +// AdminServiceClient is a client for the account.AdminService service. +type AdminServiceClient interface { + // Lists the details of all NATS Accounts + ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) + // Creates a new account, public_nkey will be autogenerated + CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) + // Updates account details, returns the account + UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) + // Get the details of a given account + GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) + // Completely deletes an account. This includes all of the data in that + // account, bookmarks, changes etc. It also deletes all users from Auth0 + // that are associated with this account + DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) + // Lists all sources within the chosen account + ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) + // Creates a new source within the chosen account + CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) + // Get the details of a source within the chosen account + GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) + // Update the details of a source within the chosen account + UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) + // Deletes a source from a chosen account + DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) + // Updates sources to keep them running in the background. This can be used + // to add explicit action, when the built-in keepalives are not sufficient. + KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) + // Create a new NATS token for a given public NKey. The user requesting must + // control the associated private key also in order to connect to NATS as + // the token is not enough on its own + CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) +} + +// NewAdminServiceClient constructs a client for the account.AdminService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewAdminServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AdminServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + adminServiceMethods := sdp_go.File_account_proto.Services().ByName("AdminService").Methods() + return &adminServiceClient{ + listAccounts: connect.NewClient[sdp_go.ListAccountsRequest, sdp_go.ListAccountsResponse]( + httpClient, + baseURL+AdminServiceListAccountsProcedure, + connect.WithSchema(adminServiceMethods.ByName("ListAccounts")), + connect.WithClientOptions(opts...), + ), + createAccount: connect.NewClient[sdp_go.CreateAccountRequest, sdp_go.CreateAccountResponse]( + httpClient, + baseURL+AdminServiceCreateAccountProcedure, + connect.WithSchema(adminServiceMethods.ByName("CreateAccount")), + connect.WithClientOptions(opts...), + ), + updateAccount: connect.NewClient[sdp_go.AdminUpdateAccountRequest, sdp_go.UpdateAccountResponse]( + httpClient, + baseURL+AdminServiceUpdateAccountProcedure, + connect.WithSchema(adminServiceMethods.ByName("UpdateAccount")), + connect.WithClientOptions(opts...), + ), + getAccount: connect.NewClient[sdp_go.AdminGetAccountRequest, sdp_go.GetAccountResponse]( + httpClient, + baseURL+AdminServiceGetAccountProcedure, + connect.WithSchema(adminServiceMethods.ByName("GetAccount")), + connect.WithClientOptions(opts...), + ), + deleteAccount: connect.NewClient[sdp_go.AdminDeleteAccountRequest, sdp_go.AdminDeleteAccountResponse]( + httpClient, + baseURL+AdminServiceDeleteAccountProcedure, + connect.WithSchema(adminServiceMethods.ByName("DeleteAccount")), + connect.WithClientOptions(opts...), + ), + listSources: connect.NewClient[sdp_go.AdminListSourcesRequest, sdp_go.ListSourcesResponse]( + httpClient, + baseURL+AdminServiceListSourcesProcedure, + connect.WithSchema(adminServiceMethods.ByName("ListSources")), + connect.WithClientOptions(opts...), + ), + createSource: connect.NewClient[sdp_go.AdminCreateSourceRequest, sdp_go.CreateSourceResponse]( + httpClient, + baseURL+AdminServiceCreateSourceProcedure, + connect.WithSchema(adminServiceMethods.ByName("CreateSource")), + connect.WithClientOptions(opts...), + ), + getSource: connect.NewClient[sdp_go.AdminGetSourceRequest, sdp_go.GetSourceResponse]( + httpClient, + baseURL+AdminServiceGetSourceProcedure, + connect.WithSchema(adminServiceMethods.ByName("GetSource")), + connect.WithClientOptions(opts...), + ), + updateSource: connect.NewClient[sdp_go.AdminUpdateSourceRequest, sdp_go.UpdateSourceResponse]( + httpClient, + baseURL+AdminServiceUpdateSourceProcedure, + connect.WithSchema(adminServiceMethods.ByName("UpdateSource")), + connect.WithClientOptions(opts...), + ), + deleteSource: connect.NewClient[sdp_go.AdminDeleteSourceRequest, sdp_go.DeleteSourceResponse]( + httpClient, + baseURL+AdminServiceDeleteSourceProcedure, + connect.WithSchema(adminServiceMethods.ByName("DeleteSource")), + connect.WithClientOptions(opts...), + ), + keepaliveSources: connect.NewClient[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( + httpClient, + baseURL+AdminServiceKeepaliveSourcesProcedure, + connect.WithSchema(adminServiceMethods.ByName("KeepaliveSources")), + connect.WithClientOptions(opts...), + ), + createToken: connect.NewClient[sdp_go.AdminCreateTokenRequest, sdp_go.CreateTokenResponse]( + httpClient, + baseURL+AdminServiceCreateTokenProcedure, + connect.WithSchema(adminServiceMethods.ByName("CreateToken")), + connect.WithClientOptions(opts...), + ), + } +} + +// adminServiceClient implements AdminServiceClient. +type adminServiceClient struct { + listAccounts *connect.Client[sdp_go.ListAccountsRequest, sdp_go.ListAccountsResponse] + createAccount *connect.Client[sdp_go.CreateAccountRequest, sdp_go.CreateAccountResponse] + updateAccount *connect.Client[sdp_go.AdminUpdateAccountRequest, sdp_go.UpdateAccountResponse] + getAccount *connect.Client[sdp_go.AdminGetAccountRequest, sdp_go.GetAccountResponse] + deleteAccount *connect.Client[sdp_go.AdminDeleteAccountRequest, sdp_go.AdminDeleteAccountResponse] + listSources *connect.Client[sdp_go.AdminListSourcesRequest, sdp_go.ListSourcesResponse] + createSource *connect.Client[sdp_go.AdminCreateSourceRequest, sdp_go.CreateSourceResponse] + getSource *connect.Client[sdp_go.AdminGetSourceRequest, sdp_go.GetSourceResponse] + updateSource *connect.Client[sdp_go.AdminUpdateSourceRequest, sdp_go.UpdateSourceResponse] + deleteSource *connect.Client[sdp_go.AdminDeleteSourceRequest, sdp_go.DeleteSourceResponse] + keepaliveSources *connect.Client[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] + createToken *connect.Client[sdp_go.AdminCreateTokenRequest, sdp_go.CreateTokenResponse] +} + +// ListAccounts calls account.AdminService.ListAccounts. +func (c *adminServiceClient) ListAccounts(ctx context.Context, req *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) { + return c.listAccounts.CallUnary(ctx, req) +} + +// CreateAccount calls account.AdminService.CreateAccount. +func (c *adminServiceClient) CreateAccount(ctx context.Context, req *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) { + return c.createAccount.CallUnary(ctx, req) +} + +// UpdateAccount calls account.AdminService.UpdateAccount. +func (c *adminServiceClient) UpdateAccount(ctx context.Context, req *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) { + return c.updateAccount.CallUnary(ctx, req) +} + +// GetAccount calls account.AdminService.GetAccount. +func (c *adminServiceClient) GetAccount(ctx context.Context, req *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { + return c.getAccount.CallUnary(ctx, req) +} + +// DeleteAccount calls account.AdminService.DeleteAccount. +func (c *adminServiceClient) DeleteAccount(ctx context.Context, req *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) { + return c.deleteAccount.CallUnary(ctx, req) +} + +// ListSources calls account.AdminService.ListSources. +func (c *adminServiceClient) ListSources(ctx context.Context, req *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { + return c.listSources.CallUnary(ctx, req) +} + +// CreateSource calls account.AdminService.CreateSource. +func (c *adminServiceClient) CreateSource(ctx context.Context, req *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { + return c.createSource.CallUnary(ctx, req) +} + +// GetSource calls account.AdminService.GetSource. +func (c *adminServiceClient) GetSource(ctx context.Context, req *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { + return c.getSource.CallUnary(ctx, req) +} + +// UpdateSource calls account.AdminService.UpdateSource. +func (c *adminServiceClient) UpdateSource(ctx context.Context, req *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { + return c.updateSource.CallUnary(ctx, req) +} + +// DeleteSource calls account.AdminService.DeleteSource. +func (c *adminServiceClient) DeleteSource(ctx context.Context, req *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { + return c.deleteSource.CallUnary(ctx, req) +} + +// KeepaliveSources calls account.AdminService.KeepaliveSources. +func (c *adminServiceClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return c.keepaliveSources.CallUnary(ctx, req) +} + +// CreateToken calls account.AdminService.CreateToken. +func (c *adminServiceClient) CreateToken(ctx context.Context, req *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { + return c.createToken.CallUnary(ctx, req) +} + +// AdminServiceHandler is an implementation of the account.AdminService service. +type AdminServiceHandler interface { + // Lists the details of all NATS Accounts + ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) + // Creates a new account, public_nkey will be autogenerated + CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) + // Updates account details, returns the account + UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) + // Get the details of a given account + GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) + // Completely deletes an account. This includes all of the data in that + // account, bookmarks, changes etc. It also deletes all users from Auth0 + // that are associated with this account + DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) + // Lists all sources within the chosen account + ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) + // Creates a new source within the chosen account + CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) + // Get the details of a source within the chosen account + GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) + // Update the details of a source within the chosen account + UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) + // Deletes a source from a chosen account + DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) + // Updates sources to keep them running in the background. This can be used + // to add explicit action, when the built-in keepalives are not sufficient. + KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) + // Create a new NATS token for a given public NKey. The user requesting must + // control the associated private key also in order to connect to NATS as + // the token is not enough on its own + CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) +} + +// NewAdminServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewAdminServiceHandler(svc AdminServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + adminServiceMethods := sdp_go.File_account_proto.Services().ByName("AdminService").Methods() + adminServiceListAccountsHandler := connect.NewUnaryHandler( + AdminServiceListAccountsProcedure, + svc.ListAccounts, + connect.WithSchema(adminServiceMethods.ByName("ListAccounts")), + connect.WithHandlerOptions(opts...), + ) + adminServiceCreateAccountHandler := connect.NewUnaryHandler( + AdminServiceCreateAccountProcedure, + svc.CreateAccount, + connect.WithSchema(adminServiceMethods.ByName("CreateAccount")), + connect.WithHandlerOptions(opts...), + ) + adminServiceUpdateAccountHandler := connect.NewUnaryHandler( + AdminServiceUpdateAccountProcedure, + svc.UpdateAccount, + connect.WithSchema(adminServiceMethods.ByName("UpdateAccount")), + connect.WithHandlerOptions(opts...), + ) + adminServiceGetAccountHandler := connect.NewUnaryHandler( + AdminServiceGetAccountProcedure, + svc.GetAccount, + connect.WithSchema(adminServiceMethods.ByName("GetAccount")), + connect.WithHandlerOptions(opts...), + ) + adminServiceDeleteAccountHandler := connect.NewUnaryHandler( + AdminServiceDeleteAccountProcedure, + svc.DeleteAccount, + connect.WithSchema(adminServiceMethods.ByName("DeleteAccount")), + connect.WithHandlerOptions(opts...), + ) + adminServiceListSourcesHandler := connect.NewUnaryHandler( + AdminServiceListSourcesProcedure, + svc.ListSources, + connect.WithSchema(adminServiceMethods.ByName("ListSources")), + connect.WithHandlerOptions(opts...), + ) + adminServiceCreateSourceHandler := connect.NewUnaryHandler( + AdminServiceCreateSourceProcedure, + svc.CreateSource, + connect.WithSchema(adminServiceMethods.ByName("CreateSource")), + connect.WithHandlerOptions(opts...), + ) + adminServiceGetSourceHandler := connect.NewUnaryHandler( + AdminServiceGetSourceProcedure, + svc.GetSource, + connect.WithSchema(adminServiceMethods.ByName("GetSource")), + connect.WithHandlerOptions(opts...), + ) + adminServiceUpdateSourceHandler := connect.NewUnaryHandler( + AdminServiceUpdateSourceProcedure, + svc.UpdateSource, + connect.WithSchema(adminServiceMethods.ByName("UpdateSource")), + connect.WithHandlerOptions(opts...), + ) + adminServiceDeleteSourceHandler := connect.NewUnaryHandler( + AdminServiceDeleteSourceProcedure, + svc.DeleteSource, + connect.WithSchema(adminServiceMethods.ByName("DeleteSource")), + connect.WithHandlerOptions(opts...), + ) + adminServiceKeepaliveSourcesHandler := connect.NewUnaryHandler( + AdminServiceKeepaliveSourcesProcedure, + svc.KeepaliveSources, + connect.WithSchema(adminServiceMethods.ByName("KeepaliveSources")), + connect.WithHandlerOptions(opts...), + ) + adminServiceCreateTokenHandler := connect.NewUnaryHandler( + AdminServiceCreateTokenProcedure, + svc.CreateToken, + connect.WithSchema(adminServiceMethods.ByName("CreateToken")), + connect.WithHandlerOptions(opts...), + ) + return "/account.AdminService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case AdminServiceListAccountsProcedure: + adminServiceListAccountsHandler.ServeHTTP(w, r) + case AdminServiceCreateAccountProcedure: + adminServiceCreateAccountHandler.ServeHTTP(w, r) + case AdminServiceUpdateAccountProcedure: + adminServiceUpdateAccountHandler.ServeHTTP(w, r) + case AdminServiceGetAccountProcedure: + adminServiceGetAccountHandler.ServeHTTP(w, r) + case AdminServiceDeleteAccountProcedure: + adminServiceDeleteAccountHandler.ServeHTTP(w, r) + case AdminServiceListSourcesProcedure: + adminServiceListSourcesHandler.ServeHTTP(w, r) + case AdminServiceCreateSourceProcedure: + adminServiceCreateSourceHandler.ServeHTTP(w, r) + case AdminServiceGetSourceProcedure: + adminServiceGetSourceHandler.ServeHTTP(w, r) + case AdminServiceUpdateSourceProcedure: + adminServiceUpdateSourceHandler.ServeHTTP(w, r) + case AdminServiceDeleteSourceProcedure: + adminServiceDeleteSourceHandler.ServeHTTP(w, r) + case AdminServiceKeepaliveSourcesProcedure: + adminServiceKeepaliveSourcesHandler.ServeHTTP(w, r) + case AdminServiceCreateTokenProcedure: + adminServiceCreateTokenHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedAdminServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedAdminServiceHandler struct{} + +func (UnimplementedAdminServiceHandler) ListAccounts(context.Context, *connect.Request[sdp_go.ListAccountsRequest]) (*connect.Response[sdp_go.ListAccountsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.ListAccounts is not implemented")) +} + +func (UnimplementedAdminServiceHandler) CreateAccount(context.Context, *connect.Request[sdp_go.CreateAccountRequest]) (*connect.Response[sdp_go.CreateAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateAccount is not implemented")) +} + +func (UnimplementedAdminServiceHandler) UpdateAccount(context.Context, *connect.Request[sdp_go.AdminUpdateAccountRequest]) (*connect.Response[sdp_go.UpdateAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.UpdateAccount is not implemented")) +} + +func (UnimplementedAdminServiceHandler) GetAccount(context.Context, *connect.Request[sdp_go.AdminGetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.GetAccount is not implemented")) +} + +func (UnimplementedAdminServiceHandler) DeleteAccount(context.Context, *connect.Request[sdp_go.AdminDeleteAccountRequest]) (*connect.Response[sdp_go.AdminDeleteAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.DeleteAccount is not implemented")) +} + +func (UnimplementedAdminServiceHandler) ListSources(context.Context, *connect.Request[sdp_go.AdminListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.ListSources is not implemented")) +} + +func (UnimplementedAdminServiceHandler) CreateSource(context.Context, *connect.Request[sdp_go.AdminCreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateSource is not implemented")) +} + +func (UnimplementedAdminServiceHandler) GetSource(context.Context, *connect.Request[sdp_go.AdminGetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.GetSource is not implemented")) +} + +func (UnimplementedAdminServiceHandler) UpdateSource(context.Context, *connect.Request[sdp_go.AdminUpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.UpdateSource is not implemented")) +} + +func (UnimplementedAdminServiceHandler) DeleteSource(context.Context, *connect.Request[sdp_go.AdminDeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.DeleteSource is not implemented")) +} + +func (UnimplementedAdminServiceHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.KeepaliveSources is not implemented")) +} + +func (UnimplementedAdminServiceHandler) CreateToken(context.Context, *connect.Request[sdp_go.AdminCreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.AdminService.CreateToken is not implemented")) +} + +// ManagementServiceClient is a client for the account.ManagementService service. +type ManagementServiceClient interface { + // Get the details of the account that this user belongs to + GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) + // Completely deletes the user's account. This includes all of the data in + // that account, bookmarks, changes etc. It also deletes the current user, + // and all other users in that account from Auth0 + DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) + // Lists all sources within the user's account + ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) + // Creates a new source within the user's account + CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) + // Get the details of a source + GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) + // Update the details of a source + UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) + // Deletes a source from a user's account + DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) + // Sources heartbeat and health + // List of all recently active sources and their health, includes information from srcman + // meaning that it can show the status of managed sources that have not started and + // connected yet + ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) + // Lists all active sources and their health. This should be used to determine + // what types, scopes etc are available rather than `ListAllSourcesStatus` since + // this endpoint only include running, available sources + ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) + // Heartbeat from a source to keep it registered and healthy + SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) + // Updates sources to keep them running in the background. This can be used + // to add explicit action, when the built-in keepalives are not sufficient. + // A user can specify how long they are willing to wait and will get a + // response either when all sources start, or when the timeout is reached. + // If the timeout is reached the response will contain the current state of + // all sources at that moment + KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) + // Create a new NATS token for a given public NKey. The user requesting must + // control the associated private key also in order to connect to NATS as + // the token is not enough on its own + CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) + // Ensure that all reverse links are populated. This does internal debouncing + // so the actual logic does only run when required. + RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest]) (*connect.ServerStreamForClient[sdp_go.RevlinkWarmupResponse], error) + // Lists all the available item types that can be discovered by sources that are running and healthy + ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) + // Get status of a single source by UUID + GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) + // Get and set onboarding status for users + GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) + SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) + // List team members in the current user's account (excludes the active user) + ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) + // Get welcome information for the current user + GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) + // Set github installation ID for the account + SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) + // this will unset the github installation ID for the account, allowing the user to install the github app again + // it will also remove the organisation profile, so we no longer generate signals for that org + UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) +} + +// NewManagementServiceClient constructs a client for the account.ManagementService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewManagementServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ManagementServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + managementServiceMethods := sdp_go.File_account_proto.Services().ByName("ManagementService").Methods() + return &managementServiceClient{ + getAccount: connect.NewClient[sdp_go.GetAccountRequest, sdp_go.GetAccountResponse]( + httpClient, + baseURL+ManagementServiceGetAccountProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetAccount")), + connect.WithClientOptions(opts...), + ), + deleteAccount: connect.NewClient[sdp_go.DeleteAccountRequest, sdp_go.DeleteAccountResponse]( + httpClient, + baseURL+ManagementServiceDeleteAccountProcedure, + connect.WithSchema(managementServiceMethods.ByName("DeleteAccount")), + connect.WithClientOptions(opts...), + ), + listSources: connect.NewClient[sdp_go.ListSourcesRequest, sdp_go.ListSourcesResponse]( + httpClient, + baseURL+ManagementServiceListSourcesProcedure, + connect.WithSchema(managementServiceMethods.ByName("ListSources")), + connect.WithClientOptions(opts...), + ), + createSource: connect.NewClient[sdp_go.CreateSourceRequest, sdp_go.CreateSourceResponse]( + httpClient, + baseURL+ManagementServiceCreateSourceProcedure, + connect.WithSchema(managementServiceMethods.ByName("CreateSource")), + connect.WithClientOptions(opts...), + ), + getSource: connect.NewClient[sdp_go.GetSourceRequest, sdp_go.GetSourceResponse]( + httpClient, + baseURL+ManagementServiceGetSourceProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetSource")), + connect.WithClientOptions(opts...), + ), + updateSource: connect.NewClient[sdp_go.UpdateSourceRequest, sdp_go.UpdateSourceResponse]( + httpClient, + baseURL+ManagementServiceUpdateSourceProcedure, + connect.WithSchema(managementServiceMethods.ByName("UpdateSource")), + connect.WithClientOptions(opts...), + ), + deleteSource: connect.NewClient[sdp_go.DeleteSourceRequest, sdp_go.DeleteSourceResponse]( + httpClient, + baseURL+ManagementServiceDeleteSourceProcedure, + connect.WithSchema(managementServiceMethods.ByName("DeleteSource")), + connect.WithClientOptions(opts...), + ), + listAllSourcesStatus: connect.NewClient[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse]( + httpClient, + baseURL+ManagementServiceListAllSourcesStatusProcedure, + connect.WithSchema(managementServiceMethods.ByName("ListAllSourcesStatus")), + connect.WithClientOptions(opts...), + ), + listActiveSourcesStatus: connect.NewClient[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse]( + httpClient, + baseURL+ManagementServiceListActiveSourcesStatusProcedure, + connect.WithSchema(managementServiceMethods.ByName("ListActiveSourcesStatus")), + connect.WithClientOptions(opts...), + ), + submitSourceHeartbeat: connect.NewClient[sdp_go.SubmitSourceHeartbeatRequest, sdp_go.SubmitSourceHeartbeatResponse]( + httpClient, + baseURL+ManagementServiceSubmitSourceHeartbeatProcedure, + connect.WithSchema(managementServiceMethods.ByName("SubmitSourceHeartbeat")), + connect.WithClientOptions(opts...), + ), + keepaliveSources: connect.NewClient[sdp_go.KeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( + httpClient, + baseURL+ManagementServiceKeepaliveSourcesProcedure, + connect.WithSchema(managementServiceMethods.ByName("KeepaliveSources")), + connect.WithClientOptions(opts...), + ), + createToken: connect.NewClient[sdp_go.CreateTokenRequest, sdp_go.CreateTokenResponse]( + httpClient, + baseURL+ManagementServiceCreateTokenProcedure, + connect.WithSchema(managementServiceMethods.ByName("CreateToken")), + connect.WithClientOptions(opts...), + ), + revlinkWarmup: connect.NewClient[sdp_go.RevlinkWarmupRequest, sdp_go.RevlinkWarmupResponse]( + httpClient, + baseURL+ManagementServiceRevlinkWarmupProcedure, + connect.WithSchema(managementServiceMethods.ByName("RevlinkWarmup")), + connect.WithClientOptions(opts...), + ), + listAvailableItemTypes: connect.NewClient[sdp_go.ListAvailableItemTypesRequest, sdp_go.ListAvailableItemTypesResponse]( + httpClient, + baseURL+ManagementServiceListAvailableItemTypesProcedure, + connect.WithSchema(managementServiceMethods.ByName("ListAvailableItemTypes")), + connect.WithClientOptions(opts...), + ), + getSourceStatus: connect.NewClient[sdp_go.GetSourceStatusRequest, sdp_go.GetSourceStatusResponse]( + httpClient, + baseURL+ManagementServiceGetSourceStatusProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetSourceStatus")), + connect.WithClientOptions(opts...), + ), + getUserOnboardingStatus: connect.NewClient[sdp_go.GetUserOnboardingStatusRequest, sdp_go.GetUserOnboardingStatusResponse]( + httpClient, + baseURL+ManagementServiceGetUserOnboardingStatusProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetUserOnboardingStatus")), + connect.WithClientOptions(opts...), + ), + setUserOnboardingStatus: connect.NewClient[sdp_go.SetUserOnboardingStatusRequest, sdp_go.SetUserOnboardingStatusResponse]( + httpClient, + baseURL+ManagementServiceSetUserOnboardingStatusProcedure, + connect.WithSchema(managementServiceMethods.ByName("SetUserOnboardingStatus")), + connect.WithClientOptions(opts...), + ), + listTeamMembers: connect.NewClient[sdp_go.ListTeamMembersRequest, sdp_go.ListTeamMembersResponse]( + httpClient, + baseURL+ManagementServiceListTeamMembersProcedure, + connect.WithSchema(managementServiceMethods.ByName("ListTeamMembers")), + connect.WithClientOptions(opts...), + ), + getWelcomeScreenInformation: connect.NewClient[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse]( + httpClient, + baseURL+ManagementServiceGetWelcomeScreenInformationProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetWelcomeScreenInformation")), + connect.WithClientOptions(opts...), + ), + setGithubInstallationID: connect.NewClient[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse]( + httpClient, + baseURL+ManagementServiceSetGithubInstallationIDProcedure, + connect.WithSchema(managementServiceMethods.ByName("SetGithubInstallationID")), + connect.WithClientOptions(opts...), + ), + unsetGithubInstallationID: connect.NewClient[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse]( + httpClient, + baseURL+ManagementServiceUnsetGithubInstallationIDProcedure, + connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), + connect.WithClientOptions(opts...), + ), + } +} + +// managementServiceClient implements ManagementServiceClient. +type managementServiceClient struct { + getAccount *connect.Client[sdp_go.GetAccountRequest, sdp_go.GetAccountResponse] + deleteAccount *connect.Client[sdp_go.DeleteAccountRequest, sdp_go.DeleteAccountResponse] + listSources *connect.Client[sdp_go.ListSourcesRequest, sdp_go.ListSourcesResponse] + createSource *connect.Client[sdp_go.CreateSourceRequest, sdp_go.CreateSourceResponse] + getSource *connect.Client[sdp_go.GetSourceRequest, sdp_go.GetSourceResponse] + updateSource *connect.Client[sdp_go.UpdateSourceRequest, sdp_go.UpdateSourceResponse] + deleteSource *connect.Client[sdp_go.DeleteSourceRequest, sdp_go.DeleteSourceResponse] + listAllSourcesStatus *connect.Client[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse] + listActiveSourcesStatus *connect.Client[sdp_go.ListAllSourcesStatusRequest, sdp_go.ListAllSourcesStatusResponse] + submitSourceHeartbeat *connect.Client[sdp_go.SubmitSourceHeartbeatRequest, sdp_go.SubmitSourceHeartbeatResponse] + keepaliveSources *connect.Client[sdp_go.KeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] + createToken *connect.Client[sdp_go.CreateTokenRequest, sdp_go.CreateTokenResponse] + revlinkWarmup *connect.Client[sdp_go.RevlinkWarmupRequest, sdp_go.RevlinkWarmupResponse] + listAvailableItemTypes *connect.Client[sdp_go.ListAvailableItemTypesRequest, sdp_go.ListAvailableItemTypesResponse] + getSourceStatus *connect.Client[sdp_go.GetSourceStatusRequest, sdp_go.GetSourceStatusResponse] + getUserOnboardingStatus *connect.Client[sdp_go.GetUserOnboardingStatusRequest, sdp_go.GetUserOnboardingStatusResponse] + setUserOnboardingStatus *connect.Client[sdp_go.SetUserOnboardingStatusRequest, sdp_go.SetUserOnboardingStatusResponse] + listTeamMembers *connect.Client[sdp_go.ListTeamMembersRequest, sdp_go.ListTeamMembersResponse] + getWelcomeScreenInformation *connect.Client[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse] + setGithubInstallationID *connect.Client[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse] + unsetGithubInstallationID *connect.Client[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse] +} + +// GetAccount calls account.ManagementService.GetAccount. +func (c *managementServiceClient) GetAccount(ctx context.Context, req *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { + return c.getAccount.CallUnary(ctx, req) +} + +// DeleteAccount calls account.ManagementService.DeleteAccount. +func (c *managementServiceClient) DeleteAccount(ctx context.Context, req *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) { + return c.deleteAccount.CallUnary(ctx, req) +} + +// ListSources calls account.ManagementService.ListSources. +func (c *managementServiceClient) ListSources(ctx context.Context, req *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { + return c.listSources.CallUnary(ctx, req) +} + +// CreateSource calls account.ManagementService.CreateSource. +func (c *managementServiceClient) CreateSource(ctx context.Context, req *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { + return c.createSource.CallUnary(ctx, req) +} + +// GetSource calls account.ManagementService.GetSource. +func (c *managementServiceClient) GetSource(ctx context.Context, req *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { + return c.getSource.CallUnary(ctx, req) +} + +// UpdateSource calls account.ManagementService.UpdateSource. +func (c *managementServiceClient) UpdateSource(ctx context.Context, req *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { + return c.updateSource.CallUnary(ctx, req) +} + +// DeleteSource calls account.ManagementService.DeleteSource. +func (c *managementServiceClient) DeleteSource(ctx context.Context, req *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { + return c.deleteSource.CallUnary(ctx, req) +} + +// ListAllSourcesStatus calls account.ManagementService.ListAllSourcesStatus. +func (c *managementServiceClient) ListAllSourcesStatus(ctx context.Context, req *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { + return c.listAllSourcesStatus.CallUnary(ctx, req) +} + +// ListActiveSourcesStatus calls account.ManagementService.ListActiveSourcesStatus. +func (c *managementServiceClient) ListActiveSourcesStatus(ctx context.Context, req *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { + return c.listActiveSourcesStatus.CallUnary(ctx, req) +} + +// SubmitSourceHeartbeat calls account.ManagementService.SubmitSourceHeartbeat. +func (c *managementServiceClient) SubmitSourceHeartbeat(ctx context.Context, req *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) { + return c.submitSourceHeartbeat.CallUnary(ctx, req) +} + +// KeepaliveSources calls account.ManagementService.KeepaliveSources. +func (c *managementServiceClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return c.keepaliveSources.CallUnary(ctx, req) +} + +// CreateToken calls account.ManagementService.CreateToken. +func (c *managementServiceClient) CreateToken(ctx context.Context, req *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { + return c.createToken.CallUnary(ctx, req) +} + +// RevlinkWarmup calls account.ManagementService.RevlinkWarmup. +func (c *managementServiceClient) RevlinkWarmup(ctx context.Context, req *connect.Request[sdp_go.RevlinkWarmupRequest]) (*connect.ServerStreamForClient[sdp_go.RevlinkWarmupResponse], error) { + return c.revlinkWarmup.CallServerStream(ctx, req) +} + +// ListAvailableItemTypes calls account.ManagementService.ListAvailableItemTypes. +func (c *managementServiceClient) ListAvailableItemTypes(ctx context.Context, req *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) { + return c.listAvailableItemTypes.CallUnary(ctx, req) +} + +// GetSourceStatus calls account.ManagementService.GetSourceStatus. +func (c *managementServiceClient) GetSourceStatus(ctx context.Context, req *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) { + return c.getSourceStatus.CallUnary(ctx, req) +} + +// GetUserOnboardingStatus calls account.ManagementService.GetUserOnboardingStatus. +func (c *managementServiceClient) GetUserOnboardingStatus(ctx context.Context, req *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) { + return c.getUserOnboardingStatus.CallUnary(ctx, req) +} + +// SetUserOnboardingStatus calls account.ManagementService.SetUserOnboardingStatus. +func (c *managementServiceClient) SetUserOnboardingStatus(ctx context.Context, req *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) { + return c.setUserOnboardingStatus.CallUnary(ctx, req) +} + +// ListTeamMembers calls account.ManagementService.ListTeamMembers. +func (c *managementServiceClient) ListTeamMembers(ctx context.Context, req *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) { + return c.listTeamMembers.CallUnary(ctx, req) +} + +// GetWelcomeScreenInformation calls account.ManagementService.GetWelcomeScreenInformation. +func (c *managementServiceClient) GetWelcomeScreenInformation(ctx context.Context, req *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) { + return c.getWelcomeScreenInformation.CallUnary(ctx, req) +} + +// SetGithubInstallationID calls account.ManagementService.SetGithubInstallationID. +func (c *managementServiceClient) SetGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) { + return c.setGithubInstallationID.CallUnary(ctx, req) +} + +// UnsetGithubInstallationID calls account.ManagementService.UnsetGithubInstallationID. +func (c *managementServiceClient) UnsetGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { + return c.unsetGithubInstallationID.CallUnary(ctx, req) +} + +// ManagementServiceHandler is an implementation of the account.ManagementService service. +type ManagementServiceHandler interface { + // Get the details of the account that this user belongs to + GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) + // Completely deletes the user's account. This includes all of the data in + // that account, bookmarks, changes etc. It also deletes the current user, + // and all other users in that account from Auth0 + DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) + // Lists all sources within the user's account + ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) + // Creates a new source within the user's account + CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) + // Get the details of a source + GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) + // Update the details of a source + UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) + // Deletes a source from a user's account + DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) + // Sources heartbeat and health + // List of all recently active sources and their health, includes information from srcman + // meaning that it can show the status of managed sources that have not started and + // connected yet + ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) + // Lists all active sources and their health. This should be used to determine + // what types, scopes etc are available rather than `ListAllSourcesStatus` since + // this endpoint only include running, available sources + ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) + // Heartbeat from a source to keep it registered and healthy + SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) + // Updates sources to keep them running in the background. This can be used + // to add explicit action, when the built-in keepalives are not sufficient. + // A user can specify how long they are willing to wait and will get a + // response either when all sources start, or when the timeout is reached. + // If the timeout is reached the response will contain the current state of + // all sources at that moment + KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) + // Create a new NATS token for a given public NKey. The user requesting must + // control the associated private key also in order to connect to NATS as + // the token is not enough on its own + CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) + // Ensure that all reverse links are populated. This does internal debouncing + // so the actual logic does only run when required. + RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest], *connect.ServerStream[sdp_go.RevlinkWarmupResponse]) error + // Lists all the available item types that can be discovered by sources that are running and healthy + ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) + // Get status of a single source by UUID + GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) + // Get and set onboarding status for users + GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) + SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) + // List team members in the current user's account (excludes the active user) + ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) + // Get welcome information for the current user + GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) + // Set github installation ID for the account + SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) + // this will unset the github installation ID for the account, allowing the user to install the github app again + // it will also remove the organisation profile, so we no longer generate signals for that org + UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) +} + +// NewManagementServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + managementServiceMethods := sdp_go.File_account_proto.Services().ByName("ManagementService").Methods() + managementServiceGetAccountHandler := connect.NewUnaryHandler( + ManagementServiceGetAccountProcedure, + svc.GetAccount, + connect.WithSchema(managementServiceMethods.ByName("GetAccount")), + connect.WithHandlerOptions(opts...), + ) + managementServiceDeleteAccountHandler := connect.NewUnaryHandler( + ManagementServiceDeleteAccountProcedure, + svc.DeleteAccount, + connect.WithSchema(managementServiceMethods.ByName("DeleteAccount")), + connect.WithHandlerOptions(opts...), + ) + managementServiceListSourcesHandler := connect.NewUnaryHandler( + ManagementServiceListSourcesProcedure, + svc.ListSources, + connect.WithSchema(managementServiceMethods.ByName("ListSources")), + connect.WithHandlerOptions(opts...), + ) + managementServiceCreateSourceHandler := connect.NewUnaryHandler( + ManagementServiceCreateSourceProcedure, + svc.CreateSource, + connect.WithSchema(managementServiceMethods.ByName("CreateSource")), + connect.WithHandlerOptions(opts...), + ) + managementServiceGetSourceHandler := connect.NewUnaryHandler( + ManagementServiceGetSourceProcedure, + svc.GetSource, + connect.WithSchema(managementServiceMethods.ByName("GetSource")), + connect.WithHandlerOptions(opts...), + ) + managementServiceUpdateSourceHandler := connect.NewUnaryHandler( + ManagementServiceUpdateSourceProcedure, + svc.UpdateSource, + connect.WithSchema(managementServiceMethods.ByName("UpdateSource")), + connect.WithHandlerOptions(opts...), + ) + managementServiceDeleteSourceHandler := connect.NewUnaryHandler( + ManagementServiceDeleteSourceProcedure, + svc.DeleteSource, + connect.WithSchema(managementServiceMethods.ByName("DeleteSource")), + connect.WithHandlerOptions(opts...), + ) + managementServiceListAllSourcesStatusHandler := connect.NewUnaryHandler( + ManagementServiceListAllSourcesStatusProcedure, + svc.ListAllSourcesStatus, + connect.WithSchema(managementServiceMethods.ByName("ListAllSourcesStatus")), + connect.WithHandlerOptions(opts...), + ) + managementServiceListActiveSourcesStatusHandler := connect.NewUnaryHandler( + ManagementServiceListActiveSourcesStatusProcedure, + svc.ListActiveSourcesStatus, + connect.WithSchema(managementServiceMethods.ByName("ListActiveSourcesStatus")), + connect.WithHandlerOptions(opts...), + ) + managementServiceSubmitSourceHeartbeatHandler := connect.NewUnaryHandler( + ManagementServiceSubmitSourceHeartbeatProcedure, + svc.SubmitSourceHeartbeat, + connect.WithSchema(managementServiceMethods.ByName("SubmitSourceHeartbeat")), + connect.WithHandlerOptions(opts...), + ) + managementServiceKeepaliveSourcesHandler := connect.NewUnaryHandler( + ManagementServiceKeepaliveSourcesProcedure, + svc.KeepaliveSources, + connect.WithSchema(managementServiceMethods.ByName("KeepaliveSources")), + connect.WithHandlerOptions(opts...), + ) + managementServiceCreateTokenHandler := connect.NewUnaryHandler( + ManagementServiceCreateTokenProcedure, + svc.CreateToken, + connect.WithSchema(managementServiceMethods.ByName("CreateToken")), + connect.WithHandlerOptions(opts...), + ) + managementServiceRevlinkWarmupHandler := connect.NewServerStreamHandler( + ManagementServiceRevlinkWarmupProcedure, + svc.RevlinkWarmup, + connect.WithSchema(managementServiceMethods.ByName("RevlinkWarmup")), + connect.WithHandlerOptions(opts...), + ) + managementServiceListAvailableItemTypesHandler := connect.NewUnaryHandler( + ManagementServiceListAvailableItemTypesProcedure, + svc.ListAvailableItemTypes, + connect.WithSchema(managementServiceMethods.ByName("ListAvailableItemTypes")), + connect.WithHandlerOptions(opts...), + ) + managementServiceGetSourceStatusHandler := connect.NewUnaryHandler( + ManagementServiceGetSourceStatusProcedure, + svc.GetSourceStatus, + connect.WithSchema(managementServiceMethods.ByName("GetSourceStatus")), + connect.WithHandlerOptions(opts...), + ) + managementServiceGetUserOnboardingStatusHandler := connect.NewUnaryHandler( + ManagementServiceGetUserOnboardingStatusProcedure, + svc.GetUserOnboardingStatus, + connect.WithSchema(managementServiceMethods.ByName("GetUserOnboardingStatus")), + connect.WithHandlerOptions(opts...), + ) + managementServiceSetUserOnboardingStatusHandler := connect.NewUnaryHandler( + ManagementServiceSetUserOnboardingStatusProcedure, + svc.SetUserOnboardingStatus, + connect.WithSchema(managementServiceMethods.ByName("SetUserOnboardingStatus")), + connect.WithHandlerOptions(opts...), + ) + managementServiceListTeamMembersHandler := connect.NewUnaryHandler( + ManagementServiceListTeamMembersProcedure, + svc.ListTeamMembers, + connect.WithSchema(managementServiceMethods.ByName("ListTeamMembers")), + connect.WithHandlerOptions(opts...), + ) + managementServiceGetWelcomeScreenInformationHandler := connect.NewUnaryHandler( + ManagementServiceGetWelcomeScreenInformationProcedure, + svc.GetWelcomeScreenInformation, + connect.WithSchema(managementServiceMethods.ByName("GetWelcomeScreenInformation")), + connect.WithHandlerOptions(opts...), + ) + managementServiceSetGithubInstallationIDHandler := connect.NewUnaryHandler( + ManagementServiceSetGithubInstallationIDProcedure, + svc.SetGithubInstallationID, + connect.WithSchema(managementServiceMethods.ByName("SetGithubInstallationID")), + connect.WithHandlerOptions(opts...), + ) + managementServiceUnsetGithubInstallationIDHandler := connect.NewUnaryHandler( + ManagementServiceUnsetGithubInstallationIDProcedure, + svc.UnsetGithubInstallationID, + connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), + connect.WithHandlerOptions(opts...), + ) + return "/account.ManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ManagementServiceGetAccountProcedure: + managementServiceGetAccountHandler.ServeHTTP(w, r) + case ManagementServiceDeleteAccountProcedure: + managementServiceDeleteAccountHandler.ServeHTTP(w, r) + case ManagementServiceListSourcesProcedure: + managementServiceListSourcesHandler.ServeHTTP(w, r) + case ManagementServiceCreateSourceProcedure: + managementServiceCreateSourceHandler.ServeHTTP(w, r) + case ManagementServiceGetSourceProcedure: + managementServiceGetSourceHandler.ServeHTTP(w, r) + case ManagementServiceUpdateSourceProcedure: + managementServiceUpdateSourceHandler.ServeHTTP(w, r) + case ManagementServiceDeleteSourceProcedure: + managementServiceDeleteSourceHandler.ServeHTTP(w, r) + case ManagementServiceListAllSourcesStatusProcedure: + managementServiceListAllSourcesStatusHandler.ServeHTTP(w, r) + case ManagementServiceListActiveSourcesStatusProcedure: + managementServiceListActiveSourcesStatusHandler.ServeHTTP(w, r) + case ManagementServiceSubmitSourceHeartbeatProcedure: + managementServiceSubmitSourceHeartbeatHandler.ServeHTTP(w, r) + case ManagementServiceKeepaliveSourcesProcedure: + managementServiceKeepaliveSourcesHandler.ServeHTTP(w, r) + case ManagementServiceCreateTokenProcedure: + managementServiceCreateTokenHandler.ServeHTTP(w, r) + case ManagementServiceRevlinkWarmupProcedure: + managementServiceRevlinkWarmupHandler.ServeHTTP(w, r) + case ManagementServiceListAvailableItemTypesProcedure: + managementServiceListAvailableItemTypesHandler.ServeHTTP(w, r) + case ManagementServiceGetSourceStatusProcedure: + managementServiceGetSourceStatusHandler.ServeHTTP(w, r) + case ManagementServiceGetUserOnboardingStatusProcedure: + managementServiceGetUserOnboardingStatusHandler.ServeHTTP(w, r) + case ManagementServiceSetUserOnboardingStatusProcedure: + managementServiceSetUserOnboardingStatusHandler.ServeHTTP(w, r) + case ManagementServiceListTeamMembersProcedure: + managementServiceListTeamMembersHandler.ServeHTTP(w, r) + case ManagementServiceGetWelcomeScreenInformationProcedure: + managementServiceGetWelcomeScreenInformationHandler.ServeHTTP(w, r) + case ManagementServiceSetGithubInstallationIDProcedure: + managementServiceSetGithubInstallationIDHandler.ServeHTTP(w, r) + case ManagementServiceUnsetGithubInstallationIDProcedure: + managementServiceUnsetGithubInstallationIDHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedManagementServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedManagementServiceHandler struct{} + +func (UnimplementedManagementServiceHandler) GetAccount(context.Context, *connect.Request[sdp_go.GetAccountRequest]) (*connect.Response[sdp_go.GetAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetAccount is not implemented")) +} + +func (UnimplementedManagementServiceHandler) DeleteAccount(context.Context, *connect.Request[sdp_go.DeleteAccountRequest]) (*connect.Response[sdp_go.DeleteAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.DeleteAccount is not implemented")) +} + +func (UnimplementedManagementServiceHandler) ListSources(context.Context, *connect.Request[sdp_go.ListSourcesRequest]) (*connect.Response[sdp_go.ListSourcesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListSources is not implemented")) +} + +func (UnimplementedManagementServiceHandler) CreateSource(context.Context, *connect.Request[sdp_go.CreateSourceRequest]) (*connect.Response[sdp_go.CreateSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.CreateSource is not implemented")) +} + +func (UnimplementedManagementServiceHandler) GetSource(context.Context, *connect.Request[sdp_go.GetSourceRequest]) (*connect.Response[sdp_go.GetSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetSource is not implemented")) +} + +func (UnimplementedManagementServiceHandler) UpdateSource(context.Context, *connect.Request[sdp_go.UpdateSourceRequest]) (*connect.Response[sdp_go.UpdateSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UpdateSource is not implemented")) +} + +func (UnimplementedManagementServiceHandler) DeleteSource(context.Context, *connect.Request[sdp_go.DeleteSourceRequest]) (*connect.Response[sdp_go.DeleteSourceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.DeleteSource is not implemented")) +} + +func (UnimplementedManagementServiceHandler) ListAllSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListAllSourcesStatus is not implemented")) +} + +func (UnimplementedManagementServiceHandler) ListActiveSourcesStatus(context.Context, *connect.Request[sdp_go.ListAllSourcesStatusRequest]) (*connect.Response[sdp_go.ListAllSourcesStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListActiveSourcesStatus is not implemented")) +} + +func (UnimplementedManagementServiceHandler) SubmitSourceHeartbeat(context.Context, *connect.Request[sdp_go.SubmitSourceHeartbeatRequest]) (*connect.Response[sdp_go.SubmitSourceHeartbeatResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SubmitSourceHeartbeat is not implemented")) +} + +func (UnimplementedManagementServiceHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.KeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.KeepaliveSources is not implemented")) +} + +func (UnimplementedManagementServiceHandler) CreateToken(context.Context, *connect.Request[sdp_go.CreateTokenRequest]) (*connect.Response[sdp_go.CreateTokenResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.CreateToken is not implemented")) +} + +func (UnimplementedManagementServiceHandler) RevlinkWarmup(context.Context, *connect.Request[sdp_go.RevlinkWarmupRequest], *connect.ServerStream[sdp_go.RevlinkWarmupResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.RevlinkWarmup is not implemented")) +} + +func (UnimplementedManagementServiceHandler) ListAvailableItemTypes(context.Context, *connect.Request[sdp_go.ListAvailableItemTypesRequest]) (*connect.Response[sdp_go.ListAvailableItemTypesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListAvailableItemTypes is not implemented")) +} + +func (UnimplementedManagementServiceHandler) GetSourceStatus(context.Context, *connect.Request[sdp_go.GetSourceStatusRequest]) (*connect.Response[sdp_go.GetSourceStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetSourceStatus is not implemented")) +} + +func (UnimplementedManagementServiceHandler) GetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.GetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.GetUserOnboardingStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetUserOnboardingStatus is not implemented")) +} + +func (UnimplementedManagementServiceHandler) SetUserOnboardingStatus(context.Context, *connect.Request[sdp_go.SetUserOnboardingStatusRequest]) (*connect.Response[sdp_go.SetUserOnboardingStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SetUserOnboardingStatus is not implemented")) +} + +func (UnimplementedManagementServiceHandler) ListTeamMembers(context.Context, *connect.Request[sdp_go.ListTeamMembersRequest]) (*connect.Response[sdp_go.ListTeamMembersResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.ListTeamMembers is not implemented")) +} + +func (UnimplementedManagementServiceHandler) GetWelcomeScreenInformation(context.Context, *connect.Request[sdp_go.GetWelcomeScreenInformationRequest]) (*connect.Response[sdp_go.GetWelcomeScreenInformationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetWelcomeScreenInformation is not implemented")) +} + +func (UnimplementedManagementServiceHandler) SetGithubInstallationID(context.Context, *connect.Request[sdp_go.SetGithubInstallationIDRequest]) (*connect.Response[sdp_go.SetGithubInstallationIDResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.SetGithubInstallationID is not implemented")) +} + +func (UnimplementedManagementServiceHandler) UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UnsetGithubInstallationID is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/apikeys.connect.go b/go/sdp-go/sdpconnect/apikeys.connect.go new file mode 100644 index 00000000..62be0125 --- /dev/null +++ b/go/sdp-go/sdpconnect/apikeys.connect.go @@ -0,0 +1,298 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: apikeys.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ApiKeyServiceName is the fully-qualified name of the ApiKeyService service. + ApiKeyServiceName = "apikeys.ApiKeyService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ApiKeyServiceCreateAPIKeyProcedure is the fully-qualified name of the ApiKeyService's + // CreateAPIKey RPC. + ApiKeyServiceCreateAPIKeyProcedure = "/apikeys.ApiKeyService/CreateAPIKey" + // ApiKeyServiceRefreshAPIKeyProcedure is the fully-qualified name of the ApiKeyService's + // RefreshAPIKey RPC. + ApiKeyServiceRefreshAPIKeyProcedure = "/apikeys.ApiKeyService/RefreshAPIKey" + // ApiKeyServiceGetAPIKeyProcedure is the fully-qualified name of the ApiKeyService's GetAPIKey RPC. + ApiKeyServiceGetAPIKeyProcedure = "/apikeys.ApiKeyService/GetAPIKey" + // ApiKeyServiceUpdateAPIKeyProcedure is the fully-qualified name of the ApiKeyService's + // UpdateAPIKey RPC. + ApiKeyServiceUpdateAPIKeyProcedure = "/apikeys.ApiKeyService/UpdateAPIKey" + // ApiKeyServiceListAPIKeysProcedure is the fully-qualified name of the ApiKeyService's ListAPIKeys + // RPC. + ApiKeyServiceListAPIKeysProcedure = "/apikeys.ApiKeyService/ListAPIKeys" + // ApiKeyServiceDeleteAPIKeyProcedure is the fully-qualified name of the ApiKeyService's + // DeleteAPIKey RPC. + ApiKeyServiceDeleteAPIKeyProcedure = "/apikeys.ApiKeyService/DeleteAPIKey" + // ApiKeyServiceExchangeKeyForTokenProcedure is the fully-qualified name of the ApiKeyService's + // ExchangeKeyForToken RPC. + ApiKeyServiceExchangeKeyForTokenProcedure = "/apikeys.ApiKeyService/ExchangeKeyForToken" +) + +// ApiKeyServiceClient is a client for the apikeys.ApiKeyService service. +type ApiKeyServiceClient interface { + // Creates an API key, pending access token generation from Auth0. The key + // cannot be used until the user has been redirected to the given URL which + // allows Auth0 to actually generate an access token + CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) + // Refreshes an API key, returning a new one with the same metadata and + // properties. The response will be the same as CreateAPIKey, and requires + // the same redirect handling to authenticate the new key. + RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) + GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) + UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) + ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) + DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) + // Exchanges an Overmind API key for an Oauth access token. That token can + // then be used to access all other Overmind APIs + ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) +} + +// NewApiKeyServiceClient constructs a client for the apikeys.ApiKeyService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewApiKeyServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ApiKeyServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + apiKeyServiceMethods := sdp_go.File_apikeys_proto.Services().ByName("ApiKeyService").Methods() + return &apiKeyServiceClient{ + createAPIKey: connect.NewClient[sdp_go.CreateAPIKeyRequest, sdp_go.CreateAPIKeyResponse]( + httpClient, + baseURL+ApiKeyServiceCreateAPIKeyProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("CreateAPIKey")), + connect.WithClientOptions(opts...), + ), + refreshAPIKey: connect.NewClient[sdp_go.RefreshAPIKeyRequest, sdp_go.RefreshAPIKeyResponse]( + httpClient, + baseURL+ApiKeyServiceRefreshAPIKeyProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("RefreshAPIKey")), + connect.WithClientOptions(opts...), + ), + getAPIKey: connect.NewClient[sdp_go.GetAPIKeyRequest, sdp_go.GetAPIKeyResponse]( + httpClient, + baseURL+ApiKeyServiceGetAPIKeyProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("GetAPIKey")), + connect.WithClientOptions(opts...), + ), + updateAPIKey: connect.NewClient[sdp_go.UpdateAPIKeyRequest, sdp_go.UpdateAPIKeyResponse]( + httpClient, + baseURL+ApiKeyServiceUpdateAPIKeyProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("UpdateAPIKey")), + connect.WithClientOptions(opts...), + ), + listAPIKeys: connect.NewClient[sdp_go.ListAPIKeysRequest, sdp_go.ListAPIKeysResponse]( + httpClient, + baseURL+ApiKeyServiceListAPIKeysProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("ListAPIKeys")), + connect.WithClientOptions(opts...), + ), + deleteAPIKey: connect.NewClient[sdp_go.DeleteAPIKeyRequest, sdp_go.DeleteAPIKeyResponse]( + httpClient, + baseURL+ApiKeyServiceDeleteAPIKeyProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("DeleteAPIKey")), + connect.WithClientOptions(opts...), + ), + exchangeKeyForToken: connect.NewClient[sdp_go.ExchangeKeyForTokenRequest, sdp_go.ExchangeKeyForTokenResponse]( + httpClient, + baseURL+ApiKeyServiceExchangeKeyForTokenProcedure, + connect.WithSchema(apiKeyServiceMethods.ByName("ExchangeKeyForToken")), + connect.WithClientOptions(opts...), + ), + } +} + +// apiKeyServiceClient implements ApiKeyServiceClient. +type apiKeyServiceClient struct { + createAPIKey *connect.Client[sdp_go.CreateAPIKeyRequest, sdp_go.CreateAPIKeyResponse] + refreshAPIKey *connect.Client[sdp_go.RefreshAPIKeyRequest, sdp_go.RefreshAPIKeyResponse] + getAPIKey *connect.Client[sdp_go.GetAPIKeyRequest, sdp_go.GetAPIKeyResponse] + updateAPIKey *connect.Client[sdp_go.UpdateAPIKeyRequest, sdp_go.UpdateAPIKeyResponse] + listAPIKeys *connect.Client[sdp_go.ListAPIKeysRequest, sdp_go.ListAPIKeysResponse] + deleteAPIKey *connect.Client[sdp_go.DeleteAPIKeyRequest, sdp_go.DeleteAPIKeyResponse] + exchangeKeyForToken *connect.Client[sdp_go.ExchangeKeyForTokenRequest, sdp_go.ExchangeKeyForTokenResponse] +} + +// CreateAPIKey calls apikeys.ApiKeyService.CreateAPIKey. +func (c *apiKeyServiceClient) CreateAPIKey(ctx context.Context, req *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) { + return c.createAPIKey.CallUnary(ctx, req) +} + +// RefreshAPIKey calls apikeys.ApiKeyService.RefreshAPIKey. +func (c *apiKeyServiceClient) RefreshAPIKey(ctx context.Context, req *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) { + return c.refreshAPIKey.CallUnary(ctx, req) +} + +// GetAPIKey calls apikeys.ApiKeyService.GetAPIKey. +func (c *apiKeyServiceClient) GetAPIKey(ctx context.Context, req *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) { + return c.getAPIKey.CallUnary(ctx, req) +} + +// UpdateAPIKey calls apikeys.ApiKeyService.UpdateAPIKey. +func (c *apiKeyServiceClient) UpdateAPIKey(ctx context.Context, req *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) { + return c.updateAPIKey.CallUnary(ctx, req) +} + +// ListAPIKeys calls apikeys.ApiKeyService.ListAPIKeys. +func (c *apiKeyServiceClient) ListAPIKeys(ctx context.Context, req *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) { + return c.listAPIKeys.CallUnary(ctx, req) +} + +// DeleteAPIKey calls apikeys.ApiKeyService.DeleteAPIKey. +func (c *apiKeyServiceClient) DeleteAPIKey(ctx context.Context, req *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) { + return c.deleteAPIKey.CallUnary(ctx, req) +} + +// ExchangeKeyForToken calls apikeys.ApiKeyService.ExchangeKeyForToken. +func (c *apiKeyServiceClient) ExchangeKeyForToken(ctx context.Context, req *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) { + return c.exchangeKeyForToken.CallUnary(ctx, req) +} + +// ApiKeyServiceHandler is an implementation of the apikeys.ApiKeyService service. +type ApiKeyServiceHandler interface { + // Creates an API key, pending access token generation from Auth0. The key + // cannot be used until the user has been redirected to the given URL which + // allows Auth0 to actually generate an access token + CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) + // Refreshes an API key, returning a new one with the same metadata and + // properties. The response will be the same as CreateAPIKey, and requires + // the same redirect handling to authenticate the new key. + RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) + GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) + UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) + ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) + DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) + // Exchanges an Overmind API key for an Oauth access token. That token can + // then be used to access all other Overmind APIs + ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) +} + +// NewApiKeyServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewApiKeyServiceHandler(svc ApiKeyServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + apiKeyServiceMethods := sdp_go.File_apikeys_proto.Services().ByName("ApiKeyService").Methods() + apiKeyServiceCreateAPIKeyHandler := connect.NewUnaryHandler( + ApiKeyServiceCreateAPIKeyProcedure, + svc.CreateAPIKey, + connect.WithSchema(apiKeyServiceMethods.ByName("CreateAPIKey")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceRefreshAPIKeyHandler := connect.NewUnaryHandler( + ApiKeyServiceRefreshAPIKeyProcedure, + svc.RefreshAPIKey, + connect.WithSchema(apiKeyServiceMethods.ByName("RefreshAPIKey")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceGetAPIKeyHandler := connect.NewUnaryHandler( + ApiKeyServiceGetAPIKeyProcedure, + svc.GetAPIKey, + connect.WithSchema(apiKeyServiceMethods.ByName("GetAPIKey")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceUpdateAPIKeyHandler := connect.NewUnaryHandler( + ApiKeyServiceUpdateAPIKeyProcedure, + svc.UpdateAPIKey, + connect.WithSchema(apiKeyServiceMethods.ByName("UpdateAPIKey")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceListAPIKeysHandler := connect.NewUnaryHandler( + ApiKeyServiceListAPIKeysProcedure, + svc.ListAPIKeys, + connect.WithSchema(apiKeyServiceMethods.ByName("ListAPIKeys")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceDeleteAPIKeyHandler := connect.NewUnaryHandler( + ApiKeyServiceDeleteAPIKeyProcedure, + svc.DeleteAPIKey, + connect.WithSchema(apiKeyServiceMethods.ByName("DeleteAPIKey")), + connect.WithHandlerOptions(opts...), + ) + apiKeyServiceExchangeKeyForTokenHandler := connect.NewUnaryHandler( + ApiKeyServiceExchangeKeyForTokenProcedure, + svc.ExchangeKeyForToken, + connect.WithSchema(apiKeyServiceMethods.ByName("ExchangeKeyForToken")), + connect.WithHandlerOptions(opts...), + ) + return "/apikeys.ApiKeyService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ApiKeyServiceCreateAPIKeyProcedure: + apiKeyServiceCreateAPIKeyHandler.ServeHTTP(w, r) + case ApiKeyServiceRefreshAPIKeyProcedure: + apiKeyServiceRefreshAPIKeyHandler.ServeHTTP(w, r) + case ApiKeyServiceGetAPIKeyProcedure: + apiKeyServiceGetAPIKeyHandler.ServeHTTP(w, r) + case ApiKeyServiceUpdateAPIKeyProcedure: + apiKeyServiceUpdateAPIKeyHandler.ServeHTTP(w, r) + case ApiKeyServiceListAPIKeysProcedure: + apiKeyServiceListAPIKeysHandler.ServeHTTP(w, r) + case ApiKeyServiceDeleteAPIKeyProcedure: + apiKeyServiceDeleteAPIKeyHandler.ServeHTTP(w, r) + case ApiKeyServiceExchangeKeyForTokenProcedure: + apiKeyServiceExchangeKeyForTokenHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedApiKeyServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedApiKeyServiceHandler struct{} + +func (UnimplementedApiKeyServiceHandler) CreateAPIKey(context.Context, *connect.Request[sdp_go.CreateAPIKeyRequest]) (*connect.Response[sdp_go.CreateAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.CreateAPIKey is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) RefreshAPIKey(context.Context, *connect.Request[sdp_go.RefreshAPIKeyRequest]) (*connect.Response[sdp_go.RefreshAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.RefreshAPIKey is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) GetAPIKey(context.Context, *connect.Request[sdp_go.GetAPIKeyRequest]) (*connect.Response[sdp_go.GetAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.GetAPIKey is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) UpdateAPIKey(context.Context, *connect.Request[sdp_go.UpdateAPIKeyRequest]) (*connect.Response[sdp_go.UpdateAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.UpdateAPIKey is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) ListAPIKeys(context.Context, *connect.Request[sdp_go.ListAPIKeysRequest]) (*connect.Response[sdp_go.ListAPIKeysResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.ListAPIKeys is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) DeleteAPIKey(context.Context, *connect.Request[sdp_go.DeleteAPIKeyRequest]) (*connect.Response[sdp_go.DeleteAPIKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.DeleteAPIKey is not implemented")) +} + +func (UnimplementedApiKeyServiceHandler) ExchangeKeyForToken(context.Context, *connect.Request[sdp_go.ExchangeKeyForTokenRequest]) (*connect.Response[sdp_go.ExchangeKeyForTokenResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("apikeys.ApiKeyService.ExchangeKeyForToken is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/area51.connect.go b/go/sdp-go/sdpconnect/area51.connect.go new file mode 100644 index 00000000..2cfec9cb --- /dev/null +++ b/go/sdp-go/sdpconnect/area51.connect.go @@ -0,0 +1,113 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: area51.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // Area51ServiceName is the fully-qualified name of the Area51Service service. + Area51ServiceName = "area51.Area51Service" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // Area51ServiceGetChangeArchiveProcedure is the fully-qualified name of the Area51Service's + // GetChangeArchive RPC. + Area51ServiceGetChangeArchiveProcedure = "/area51.Area51Service/GetChangeArchive" +) + +// Area51ServiceClient is a client for the area51.Area51Service service. +type Area51ServiceClient interface { + // This is not implemented at all, it prevents javascript generation errors + // we manually use the generated sdp objects in area51 service + GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) +} + +// NewArea51ServiceClient constructs a client for the area51.Area51Service service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewArea51ServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) Area51ServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + area51ServiceMethods := sdp_go.File_area51_proto.Services().ByName("Area51Service").Methods() + return &area51ServiceClient{ + getChangeArchive: connect.NewClient[sdp_go.GetChangeArchiveRequest, sdp_go.GetChangeArchiveResponse]( + httpClient, + baseURL+Area51ServiceGetChangeArchiveProcedure, + connect.WithSchema(area51ServiceMethods.ByName("GetChangeArchive")), + connect.WithClientOptions(opts...), + ), + } +} + +// area51ServiceClient implements Area51ServiceClient. +type area51ServiceClient struct { + getChangeArchive *connect.Client[sdp_go.GetChangeArchiveRequest, sdp_go.GetChangeArchiveResponse] +} + +// GetChangeArchive calls area51.Area51Service.GetChangeArchive. +func (c *area51ServiceClient) GetChangeArchive(ctx context.Context, req *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) { + return c.getChangeArchive.CallUnary(ctx, req) +} + +// Area51ServiceHandler is an implementation of the area51.Area51Service service. +type Area51ServiceHandler interface { + // This is not implemented at all, it prevents javascript generation errors + // we manually use the generated sdp objects in area51 service + GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) +} + +// NewArea51ServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewArea51ServiceHandler(svc Area51ServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + area51ServiceMethods := sdp_go.File_area51_proto.Services().ByName("Area51Service").Methods() + area51ServiceGetChangeArchiveHandler := connect.NewUnaryHandler( + Area51ServiceGetChangeArchiveProcedure, + svc.GetChangeArchive, + connect.WithSchema(area51ServiceMethods.ByName("GetChangeArchive")), + connect.WithHandlerOptions(opts...), + ) + return "/area51.Area51Service/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case Area51ServiceGetChangeArchiveProcedure: + area51ServiceGetChangeArchiveHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedArea51ServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedArea51ServiceHandler struct{} + +func (UnimplementedArea51ServiceHandler) GetChangeArchive(context.Context, *connect.Request[sdp_go.GetChangeArchiveRequest]) (*connect.Response[sdp_go.GetChangeArchiveResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("area51.Area51Service.GetChangeArchive is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/auth0support.connect.go b/go/sdp-go/sdpconnect/auth0support.connect.go new file mode 100644 index 00000000..5435916e --- /dev/null +++ b/go/sdp-go/sdpconnect/auth0support.connect.go @@ -0,0 +1,145 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: auth0support.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // Auth0SupportName is the fully-qualified name of the Auth0Support service. + Auth0SupportName = "auth0support.Auth0Support" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // Auth0SupportCreateUserProcedure is the fully-qualified name of the Auth0Support's CreateUser RPC. + Auth0SupportCreateUserProcedure = "/auth0support.Auth0Support/CreateUser" + // Auth0SupportKeepaliveSourcesProcedure is the fully-qualified name of the Auth0Support's + // KeepaliveSources RPC. + Auth0SupportKeepaliveSourcesProcedure = "/auth0support.Auth0Support/KeepaliveSources" +) + +// Auth0SupportClient is a client for the auth0support.Auth0Support service. +type Auth0SupportClient interface { + // create a new user on first login + CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) + // Updates sources to keep them running in the background. This is called on + // login by auth0 to give us a chance to boot up sources while the app is + // loading. + KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) +} + +// NewAuth0SupportClient constructs a client for the auth0support.Auth0Support service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewAuth0SupportClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) Auth0SupportClient { + baseURL = strings.TrimRight(baseURL, "/") + auth0SupportMethods := sdp_go.File_auth0support_proto.Services().ByName("Auth0Support").Methods() + return &auth0SupportClient{ + createUser: connect.NewClient[sdp_go.Auth0CreateUserRequest, sdp_go.Auth0CreateUserResponse]( + httpClient, + baseURL+Auth0SupportCreateUserProcedure, + connect.WithSchema(auth0SupportMethods.ByName("CreateUser")), + connect.WithClientOptions(opts...), + ), + keepaliveSources: connect.NewClient[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse]( + httpClient, + baseURL+Auth0SupportKeepaliveSourcesProcedure, + connect.WithSchema(auth0SupportMethods.ByName("KeepaliveSources")), + connect.WithClientOptions(opts...), + ), + } +} + +// auth0SupportClient implements Auth0SupportClient. +type auth0SupportClient struct { + createUser *connect.Client[sdp_go.Auth0CreateUserRequest, sdp_go.Auth0CreateUserResponse] + keepaliveSources *connect.Client[sdp_go.AdminKeepaliveSourcesRequest, sdp_go.KeepaliveSourcesResponse] +} + +// CreateUser calls auth0support.Auth0Support.CreateUser. +func (c *auth0SupportClient) CreateUser(ctx context.Context, req *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) { + return c.createUser.CallUnary(ctx, req) +} + +// KeepaliveSources calls auth0support.Auth0Support.KeepaliveSources. +func (c *auth0SupportClient) KeepaliveSources(ctx context.Context, req *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return c.keepaliveSources.CallUnary(ctx, req) +} + +// Auth0SupportHandler is an implementation of the auth0support.Auth0Support service. +type Auth0SupportHandler interface { + // create a new user on first login + CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) + // Updates sources to keep them running in the background. This is called on + // login by auth0 to give us a chance to boot up sources while the app is + // loading. + KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) +} + +// NewAuth0SupportHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewAuth0SupportHandler(svc Auth0SupportHandler, opts ...connect.HandlerOption) (string, http.Handler) { + auth0SupportMethods := sdp_go.File_auth0support_proto.Services().ByName("Auth0Support").Methods() + auth0SupportCreateUserHandler := connect.NewUnaryHandler( + Auth0SupportCreateUserProcedure, + svc.CreateUser, + connect.WithSchema(auth0SupportMethods.ByName("CreateUser")), + connect.WithHandlerOptions(opts...), + ) + auth0SupportKeepaliveSourcesHandler := connect.NewUnaryHandler( + Auth0SupportKeepaliveSourcesProcedure, + svc.KeepaliveSources, + connect.WithSchema(auth0SupportMethods.ByName("KeepaliveSources")), + connect.WithHandlerOptions(opts...), + ) + return "/auth0support.Auth0Support/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case Auth0SupportCreateUserProcedure: + auth0SupportCreateUserHandler.ServeHTTP(w, r) + case Auth0SupportKeepaliveSourcesProcedure: + auth0SupportKeepaliveSourcesHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedAuth0SupportHandler returns CodeUnimplemented from all methods. +type UnimplementedAuth0SupportHandler struct{} + +func (UnimplementedAuth0SupportHandler) CreateUser(context.Context, *connect.Request[sdp_go.Auth0CreateUserRequest]) (*connect.Response[sdp_go.Auth0CreateUserResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auth0support.Auth0Support.CreateUser is not implemented")) +} + +func (UnimplementedAuth0SupportHandler) KeepaliveSources(context.Context, *connect.Request[sdp_go.AdminKeepaliveSourcesRequest]) (*connect.Response[sdp_go.KeepaliveSourcesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auth0support.Auth0Support.KeepaliveSources is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/bookmarks.connect.go b/go/sdp-go/sdpconnect/bookmarks.connect.go new file mode 100644 index 00000000..579ff6ad --- /dev/null +++ b/go/sdp-go/sdpconnect/bookmarks.connect.go @@ -0,0 +1,262 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: bookmarks.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // BookmarksServiceName is the fully-qualified name of the BookmarksService service. + BookmarksServiceName = "bookmarks.BookmarksService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // BookmarksServiceListBookmarksProcedure is the fully-qualified name of the BookmarksService's + // ListBookmarks RPC. + BookmarksServiceListBookmarksProcedure = "/bookmarks.BookmarksService/ListBookmarks" + // BookmarksServiceCreateBookmarkProcedure is the fully-qualified name of the BookmarksService's + // CreateBookmark RPC. + BookmarksServiceCreateBookmarkProcedure = "/bookmarks.BookmarksService/CreateBookmark" + // BookmarksServiceGetBookmarkProcedure is the fully-qualified name of the BookmarksService's + // GetBookmark RPC. + BookmarksServiceGetBookmarkProcedure = "/bookmarks.BookmarksService/GetBookmark" + // BookmarksServiceUpdateBookmarkProcedure is the fully-qualified name of the BookmarksService's + // UpdateBookmark RPC. + BookmarksServiceUpdateBookmarkProcedure = "/bookmarks.BookmarksService/UpdateBookmark" + // BookmarksServiceDeleteBookmarkProcedure is the fully-qualified name of the BookmarksService's + // DeleteBookmark RPC. + BookmarksServiceDeleteBookmarkProcedure = "/bookmarks.BookmarksService/DeleteBookmark" + // BookmarksServiceGetAffectedBookmarksProcedure is the fully-qualified name of the + // BookmarksService's GetAffectedBookmarks RPC. + BookmarksServiceGetAffectedBookmarksProcedure = "/bookmarks.BookmarksService/GetAffectedBookmarks" +) + +// BookmarksServiceClient is a client for the bookmarks.BookmarksService service. +type BookmarksServiceClient interface { + // ListBookmarks returns all bookmarks of the current user. note that this does not include the actual bookmark data, use GetBookmark for that + ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) + // CreateBookmark creates a new bookmark + CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) + // GetBookmark returns the bookmark with the given UUID. This can also return snapshots as bookmarks and will strip the stored items from the response. + GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) + UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) + DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) + // a helper method to find all affected apps for a given blast radius snapshot + GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) +} + +// NewBookmarksServiceClient constructs a client for the bookmarks.BookmarksService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewBookmarksServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) BookmarksServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + bookmarksServiceMethods := sdp_go.File_bookmarks_proto.Services().ByName("BookmarksService").Methods() + return &bookmarksServiceClient{ + listBookmarks: connect.NewClient[sdp_go.ListBookmarksRequest, sdp_go.ListBookmarkResponse]( + httpClient, + baseURL+BookmarksServiceListBookmarksProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("ListBookmarks")), + connect.WithClientOptions(opts...), + ), + createBookmark: connect.NewClient[sdp_go.CreateBookmarkRequest, sdp_go.CreateBookmarkResponse]( + httpClient, + baseURL+BookmarksServiceCreateBookmarkProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("CreateBookmark")), + connect.WithClientOptions(opts...), + ), + getBookmark: connect.NewClient[sdp_go.GetBookmarkRequest, sdp_go.GetBookmarkResponse]( + httpClient, + baseURL+BookmarksServiceGetBookmarkProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("GetBookmark")), + connect.WithClientOptions(opts...), + ), + updateBookmark: connect.NewClient[sdp_go.UpdateBookmarkRequest, sdp_go.UpdateBookmarkResponse]( + httpClient, + baseURL+BookmarksServiceUpdateBookmarkProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("UpdateBookmark")), + connect.WithClientOptions(opts...), + ), + deleteBookmark: connect.NewClient[sdp_go.DeleteBookmarkRequest, sdp_go.DeleteBookmarkResponse]( + httpClient, + baseURL+BookmarksServiceDeleteBookmarkProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("DeleteBookmark")), + connect.WithClientOptions(opts...), + ), + getAffectedBookmarks: connect.NewClient[sdp_go.GetAffectedBookmarksRequest, sdp_go.GetAffectedBookmarksResponse]( + httpClient, + baseURL+BookmarksServiceGetAffectedBookmarksProcedure, + connect.WithSchema(bookmarksServiceMethods.ByName("GetAffectedBookmarks")), + connect.WithClientOptions(opts...), + ), + } +} + +// bookmarksServiceClient implements BookmarksServiceClient. +type bookmarksServiceClient struct { + listBookmarks *connect.Client[sdp_go.ListBookmarksRequest, sdp_go.ListBookmarkResponse] + createBookmark *connect.Client[sdp_go.CreateBookmarkRequest, sdp_go.CreateBookmarkResponse] + getBookmark *connect.Client[sdp_go.GetBookmarkRequest, sdp_go.GetBookmarkResponse] + updateBookmark *connect.Client[sdp_go.UpdateBookmarkRequest, sdp_go.UpdateBookmarkResponse] + deleteBookmark *connect.Client[sdp_go.DeleteBookmarkRequest, sdp_go.DeleteBookmarkResponse] + getAffectedBookmarks *connect.Client[sdp_go.GetAffectedBookmarksRequest, sdp_go.GetAffectedBookmarksResponse] +} + +// ListBookmarks calls bookmarks.BookmarksService.ListBookmarks. +func (c *bookmarksServiceClient) ListBookmarks(ctx context.Context, req *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) { + return c.listBookmarks.CallUnary(ctx, req) +} + +// CreateBookmark calls bookmarks.BookmarksService.CreateBookmark. +func (c *bookmarksServiceClient) CreateBookmark(ctx context.Context, req *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) { + return c.createBookmark.CallUnary(ctx, req) +} + +// GetBookmark calls bookmarks.BookmarksService.GetBookmark. +func (c *bookmarksServiceClient) GetBookmark(ctx context.Context, req *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) { + return c.getBookmark.CallUnary(ctx, req) +} + +// UpdateBookmark calls bookmarks.BookmarksService.UpdateBookmark. +func (c *bookmarksServiceClient) UpdateBookmark(ctx context.Context, req *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) { + return c.updateBookmark.CallUnary(ctx, req) +} + +// DeleteBookmark calls bookmarks.BookmarksService.DeleteBookmark. +func (c *bookmarksServiceClient) DeleteBookmark(ctx context.Context, req *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) { + return c.deleteBookmark.CallUnary(ctx, req) +} + +// GetAffectedBookmarks calls bookmarks.BookmarksService.GetAffectedBookmarks. +func (c *bookmarksServiceClient) GetAffectedBookmarks(ctx context.Context, req *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) { + return c.getAffectedBookmarks.CallUnary(ctx, req) +} + +// BookmarksServiceHandler is an implementation of the bookmarks.BookmarksService service. +type BookmarksServiceHandler interface { + // ListBookmarks returns all bookmarks of the current user. note that this does not include the actual bookmark data, use GetBookmark for that + ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) + // CreateBookmark creates a new bookmark + CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) + // GetBookmark returns the bookmark with the given UUID. This can also return snapshots as bookmarks and will strip the stored items from the response. + GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) + UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) + DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) + // a helper method to find all affected apps for a given blast radius snapshot + GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) +} + +// NewBookmarksServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewBookmarksServiceHandler(svc BookmarksServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + bookmarksServiceMethods := sdp_go.File_bookmarks_proto.Services().ByName("BookmarksService").Methods() + bookmarksServiceListBookmarksHandler := connect.NewUnaryHandler( + BookmarksServiceListBookmarksProcedure, + svc.ListBookmarks, + connect.WithSchema(bookmarksServiceMethods.ByName("ListBookmarks")), + connect.WithHandlerOptions(opts...), + ) + bookmarksServiceCreateBookmarkHandler := connect.NewUnaryHandler( + BookmarksServiceCreateBookmarkProcedure, + svc.CreateBookmark, + connect.WithSchema(bookmarksServiceMethods.ByName("CreateBookmark")), + connect.WithHandlerOptions(opts...), + ) + bookmarksServiceGetBookmarkHandler := connect.NewUnaryHandler( + BookmarksServiceGetBookmarkProcedure, + svc.GetBookmark, + connect.WithSchema(bookmarksServiceMethods.ByName("GetBookmark")), + connect.WithHandlerOptions(opts...), + ) + bookmarksServiceUpdateBookmarkHandler := connect.NewUnaryHandler( + BookmarksServiceUpdateBookmarkProcedure, + svc.UpdateBookmark, + connect.WithSchema(bookmarksServiceMethods.ByName("UpdateBookmark")), + connect.WithHandlerOptions(opts...), + ) + bookmarksServiceDeleteBookmarkHandler := connect.NewUnaryHandler( + BookmarksServiceDeleteBookmarkProcedure, + svc.DeleteBookmark, + connect.WithSchema(bookmarksServiceMethods.ByName("DeleteBookmark")), + connect.WithHandlerOptions(opts...), + ) + bookmarksServiceGetAffectedBookmarksHandler := connect.NewUnaryHandler( + BookmarksServiceGetAffectedBookmarksProcedure, + svc.GetAffectedBookmarks, + connect.WithSchema(bookmarksServiceMethods.ByName("GetAffectedBookmarks")), + connect.WithHandlerOptions(opts...), + ) + return "/bookmarks.BookmarksService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case BookmarksServiceListBookmarksProcedure: + bookmarksServiceListBookmarksHandler.ServeHTTP(w, r) + case BookmarksServiceCreateBookmarkProcedure: + bookmarksServiceCreateBookmarkHandler.ServeHTTP(w, r) + case BookmarksServiceGetBookmarkProcedure: + bookmarksServiceGetBookmarkHandler.ServeHTTP(w, r) + case BookmarksServiceUpdateBookmarkProcedure: + bookmarksServiceUpdateBookmarkHandler.ServeHTTP(w, r) + case BookmarksServiceDeleteBookmarkProcedure: + bookmarksServiceDeleteBookmarkHandler.ServeHTTP(w, r) + case BookmarksServiceGetAffectedBookmarksProcedure: + bookmarksServiceGetAffectedBookmarksHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedBookmarksServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedBookmarksServiceHandler struct{} + +func (UnimplementedBookmarksServiceHandler) ListBookmarks(context.Context, *connect.Request[sdp_go.ListBookmarksRequest]) (*connect.Response[sdp_go.ListBookmarkResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.ListBookmarks is not implemented")) +} + +func (UnimplementedBookmarksServiceHandler) CreateBookmark(context.Context, *connect.Request[sdp_go.CreateBookmarkRequest]) (*connect.Response[sdp_go.CreateBookmarkResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.CreateBookmark is not implemented")) +} + +func (UnimplementedBookmarksServiceHandler) GetBookmark(context.Context, *connect.Request[sdp_go.GetBookmarkRequest]) (*connect.Response[sdp_go.GetBookmarkResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.GetBookmark is not implemented")) +} + +func (UnimplementedBookmarksServiceHandler) UpdateBookmark(context.Context, *connect.Request[sdp_go.UpdateBookmarkRequest]) (*connect.Response[sdp_go.UpdateBookmarkResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.UpdateBookmark is not implemented")) +} + +func (UnimplementedBookmarksServiceHandler) DeleteBookmark(context.Context, *connect.Request[sdp_go.DeleteBookmarkRequest]) (*connect.Response[sdp_go.DeleteBookmarkResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.DeleteBookmark is not implemented")) +} + +func (UnimplementedBookmarksServiceHandler) GetAffectedBookmarks(context.Context, *connect.Request[sdp_go.GetAffectedBookmarksRequest]) (*connect.Response[sdp_go.GetAffectedBookmarksResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("bookmarks.BookmarksService.GetAffectedBookmarks is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/changes.connect.go b/go/sdp-go/sdpconnect/changes.connect.go new file mode 100644 index 00000000..2988499a --- /dev/null +++ b/go/sdp-go/sdpconnect/changes.connect.go @@ -0,0 +1,1132 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: changes.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ChangesServiceName is the fully-qualified name of the ChangesService service. + ChangesServiceName = "changes.ChangesService" + // LabelServiceName is the fully-qualified name of the LabelService service. + LabelServiceName = "changes.LabelService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ChangesServiceListChangesProcedure is the fully-qualified name of the ChangesService's + // ListChanges RPC. + ChangesServiceListChangesProcedure = "/changes.ChangesService/ListChanges" + // ChangesServiceListChangesByStatusProcedure is the fully-qualified name of the ChangesService's + // ListChangesByStatus RPC. + ChangesServiceListChangesByStatusProcedure = "/changes.ChangesService/ListChangesByStatus" + // ChangesServiceCreateChangeProcedure is the fully-qualified name of the ChangesService's + // CreateChange RPC. + ChangesServiceCreateChangeProcedure = "/changes.ChangesService/CreateChange" + // ChangesServiceGetChangeProcedure is the fully-qualified name of the ChangesService's GetChange + // RPC. + ChangesServiceGetChangeProcedure = "/changes.ChangesService/GetChange" + // ChangesServiceGetChangeByTicketLinkProcedure is the fully-qualified name of the ChangesService's + // GetChangeByTicketLink RPC. + ChangesServiceGetChangeByTicketLinkProcedure = "/changes.ChangesService/GetChangeByTicketLink" + // ChangesServiceGetChangeSummaryProcedure is the fully-qualified name of the ChangesService's + // GetChangeSummary RPC. + ChangesServiceGetChangeSummaryProcedure = "/changes.ChangesService/GetChangeSummary" + // ChangesServiceGetChangeTimelineV2Procedure is the fully-qualified name of the ChangesService's + // GetChangeTimelineV2 RPC. + ChangesServiceGetChangeTimelineV2Procedure = "/changes.ChangesService/GetChangeTimelineV2" + // ChangesServiceGetChangeRisksProcedure is the fully-qualified name of the ChangesService's + // GetChangeRisks RPC. + ChangesServiceGetChangeRisksProcedure = "/changes.ChangesService/GetChangeRisks" + // ChangesServiceUpdateChangeProcedure is the fully-qualified name of the ChangesService's + // UpdateChange RPC. + ChangesServiceUpdateChangeProcedure = "/changes.ChangesService/UpdateChange" + // ChangesServiceDeleteChangeProcedure is the fully-qualified name of the ChangesService's + // DeleteChange RPC. + ChangesServiceDeleteChangeProcedure = "/changes.ChangesService/DeleteChange" + // ChangesServiceListChangesBySnapshotUUIDProcedure is the fully-qualified name of the + // ChangesService's ListChangesBySnapshotUUID RPC. + ChangesServiceListChangesBySnapshotUUIDProcedure = "/changes.ChangesService/ListChangesBySnapshotUUID" + // ChangesServiceRefreshStateProcedure is the fully-qualified name of the ChangesService's + // RefreshState RPC. + ChangesServiceRefreshStateProcedure = "/changes.ChangesService/RefreshState" + // ChangesServiceStartChangeProcedure is the fully-qualified name of the ChangesService's + // StartChange RPC. + ChangesServiceStartChangeProcedure = "/changes.ChangesService/StartChange" + // ChangesServiceEndChangeProcedure is the fully-qualified name of the ChangesService's EndChange + // RPC. + ChangesServiceEndChangeProcedure = "/changes.ChangesService/EndChange" + // ChangesServiceStartChangeSimpleProcedure is the fully-qualified name of the ChangesService's + // StartChangeSimple RPC. + ChangesServiceStartChangeSimpleProcedure = "/changes.ChangesService/StartChangeSimple" + // ChangesServiceEndChangeSimpleProcedure is the fully-qualified name of the ChangesService's + // EndChangeSimple RPC. + ChangesServiceEndChangeSimpleProcedure = "/changes.ChangesService/EndChangeSimple" + // ChangesServiceListHomeChangesProcedure is the fully-qualified name of the ChangesService's + // ListHomeChanges RPC. + ChangesServiceListHomeChangesProcedure = "/changes.ChangesService/ListHomeChanges" + // ChangesServiceStartChangeAnalysisProcedure is the fully-qualified name of the ChangesService's + // StartChangeAnalysis RPC. + ChangesServiceStartChangeAnalysisProcedure = "/changes.ChangesService/StartChangeAnalysis" + // ChangesServiceListChangingItemsSummaryProcedure is the fully-qualified name of the + // ChangesService's ListChangingItemsSummary RPC. + ChangesServiceListChangingItemsSummaryProcedure = "/changes.ChangesService/ListChangingItemsSummary" + // ChangesServiceGetDiffProcedure is the fully-qualified name of the ChangesService's GetDiff RPC. + ChangesServiceGetDiffProcedure = "/changes.ChangesService/GetDiff" + // ChangesServicePopulateChangeFiltersProcedure is the fully-qualified name of the ChangesService's + // PopulateChangeFilters RPC. + ChangesServicePopulateChangeFiltersProcedure = "/changes.ChangesService/PopulateChangeFilters" + // ChangesServiceGenerateRiskFixProcedure is the fully-qualified name of the ChangesService's + // GenerateRiskFix RPC. + ChangesServiceGenerateRiskFixProcedure = "/changes.ChangesService/GenerateRiskFix" + // ChangesServiceGetHypothesesDetailsProcedure is the fully-qualified name of the ChangesService's + // GetHypothesesDetails RPC. + ChangesServiceGetHypothesesDetailsProcedure = "/changes.ChangesService/GetHypothesesDetails" + // ChangesServiceGetChangeSignalsProcedure is the fully-qualified name of the ChangesService's + // GetChangeSignals RPC. + ChangesServiceGetChangeSignalsProcedure = "/changes.ChangesService/GetChangeSignals" + // LabelServiceListLabelRulesProcedure is the fully-qualified name of the LabelService's + // ListLabelRules RPC. + LabelServiceListLabelRulesProcedure = "/changes.LabelService/ListLabelRules" + // LabelServiceCreateLabelRuleProcedure is the fully-qualified name of the LabelService's + // CreateLabelRule RPC. + LabelServiceCreateLabelRuleProcedure = "/changes.LabelService/CreateLabelRule" + // LabelServiceGetLabelRuleProcedure is the fully-qualified name of the LabelService's GetLabelRule + // RPC. + LabelServiceGetLabelRuleProcedure = "/changes.LabelService/GetLabelRule" + // LabelServiceUpdateLabelRuleProcedure is the fully-qualified name of the LabelService's + // UpdateLabelRule RPC. + LabelServiceUpdateLabelRuleProcedure = "/changes.LabelService/UpdateLabelRule" + // LabelServiceDeleteLabelRuleProcedure is the fully-qualified name of the LabelService's + // DeleteLabelRule RPC. + LabelServiceDeleteLabelRuleProcedure = "/changes.LabelService/DeleteLabelRule" + // LabelServiceTestLabelRuleProcedure is the fully-qualified name of the LabelService's + // TestLabelRule RPC. + LabelServiceTestLabelRuleProcedure = "/changes.LabelService/TestLabelRule" + // LabelServiceReapplyLabelRuleInTimeRangeProcedure is the fully-qualified name of the + // LabelService's ReapplyLabelRuleInTimeRange RPC. + LabelServiceReapplyLabelRuleInTimeRangeProcedure = "/changes.LabelService/ReapplyLabelRuleInTimeRange" +) + +// ChangesServiceClient is a client for the changes.ChangesService service. +type ChangesServiceClient interface { + // Lists all changes + ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) + // list all changes in a specific status + ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) + // Creates a new change + CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) + // Gets the details of an existing change + GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) + // Get a change by the ticket link + GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) + // Gets the details of an existing change in markdown format + GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) + // Gets the full timeline for this change, this will send one response + // immediately and then hold the connection open, and send the entire + // timeline again if there are any changes + GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) + // This is used on the blast radius page to get the risks and status for a change. + GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) + // Updates an existing change + UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) + // Deletes a change + DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) + // Lists all changes for a snapshot UUID + ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) + // Ask the gateway to refresh all internal caches and status slots + // The RPC will return immediately doing all processing in the background + RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) + // Executing this RPC take a snapshot of the current blast radius and store it + // in `systemBeforeSnapshotUUID` and then advance the status to + // `STATUS_HAPPENING`. It can only be called once per change. + StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.ServerStreamForClient[sdp_go.StartChangeResponse], error) + // Takes the "after" snapshot, stores it in `systemAfterSnapshotUUID`, calculates + // the change diff and stores it as a list of DiffedItems and + // advances the change status to `STATUS_DONE` + EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.ServerStreamForClient[sdp_go.EndChangeResponse], error) + // Simple version of StartChange that returns immediately after enqueuing the job. + // Use this instead of StartChange for non-streaming clients. + StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) + // Simple version of EndChange that returns immediately after enqueuing the job. + // Use this instead of EndChange for non-streaming clients. + EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) + // Lists all changes, designed for use in the changes home page + ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) + // Start the change analysis process. This will calculate various things + // blast radius, risks etc. This will return immediately and + // the results can be fetched using the other RPCs + StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) + // Gets the diff summary for all items that were planned to change as part of + // this change. This includes the high level details of the item, and the + // status (e.g. changed, deleted) but not the diff itself + ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) + // Gets the full diff of everything that changed as part of this "change". + // This includes all items and also edges between them + GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) + // List all the available repos, authors and statuses that can be used to populate the dropdown filters + PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) + // Generates an AI-powered fix suggestion for a specific risk + GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) + // The full details of all of the hypotheses that were considered or are being + // considered as part of this change. + GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) + // Gets all signals for a change, including: + // - Overall signal for the change + // - Top level signals for each category + // - Routineness signals per item + // - Individual custom signals + // This is similar to GetChangeSummary but focused on signals data + GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) +} + +// NewChangesServiceClient constructs a client for the changes.ChangesService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewChangesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ChangesServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + changesServiceMethods := sdp_go.File_changes_proto.Services().ByName("ChangesService").Methods() + return &changesServiceClient{ + listChanges: connect.NewClient[sdp_go.ListChangesRequest, sdp_go.ListChangesResponse]( + httpClient, + baseURL+ChangesServiceListChangesProcedure, + connect.WithSchema(changesServiceMethods.ByName("ListChanges")), + connect.WithClientOptions(opts...), + ), + listChangesByStatus: connect.NewClient[sdp_go.ListChangesByStatusRequest, sdp_go.ListChangesByStatusResponse]( + httpClient, + baseURL+ChangesServiceListChangesByStatusProcedure, + connect.WithSchema(changesServiceMethods.ByName("ListChangesByStatus")), + connect.WithClientOptions(opts...), + ), + createChange: connect.NewClient[sdp_go.CreateChangeRequest, sdp_go.CreateChangeResponse]( + httpClient, + baseURL+ChangesServiceCreateChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("CreateChange")), + connect.WithClientOptions(opts...), + ), + getChange: connect.NewClient[sdp_go.GetChangeRequest, sdp_go.GetChangeResponse]( + httpClient, + baseURL+ChangesServiceGetChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetChange")), + connect.WithClientOptions(opts...), + ), + getChangeByTicketLink: connect.NewClient[sdp_go.GetChangeByTicketLinkRequest, sdp_go.GetChangeResponse]( + httpClient, + baseURL+ChangesServiceGetChangeByTicketLinkProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetChangeByTicketLink")), + connect.WithClientOptions(opts...), + ), + getChangeSummary: connect.NewClient[sdp_go.GetChangeSummaryRequest, sdp_go.GetChangeSummaryResponse]( + httpClient, + baseURL+ChangesServiceGetChangeSummaryProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetChangeSummary")), + connect.WithClientOptions(opts...), + ), + getChangeTimelineV2: connect.NewClient[sdp_go.GetChangeTimelineV2Request, sdp_go.GetChangeTimelineV2Response]( + httpClient, + baseURL+ChangesServiceGetChangeTimelineV2Procedure, + connect.WithSchema(changesServiceMethods.ByName("GetChangeTimelineV2")), + connect.WithClientOptions(opts...), + ), + getChangeRisks: connect.NewClient[sdp_go.GetChangeRisksRequest, sdp_go.GetChangeRisksResponse]( + httpClient, + baseURL+ChangesServiceGetChangeRisksProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetChangeRisks")), + connect.WithClientOptions(opts...), + ), + updateChange: connect.NewClient[sdp_go.UpdateChangeRequest, sdp_go.UpdateChangeResponse]( + httpClient, + baseURL+ChangesServiceUpdateChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("UpdateChange")), + connect.WithClientOptions(opts...), + ), + deleteChange: connect.NewClient[sdp_go.DeleteChangeRequest, sdp_go.DeleteChangeResponse]( + httpClient, + baseURL+ChangesServiceDeleteChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("DeleteChange")), + connect.WithClientOptions(opts...), + ), + listChangesBySnapshotUUID: connect.NewClient[sdp_go.ListChangesBySnapshotUUIDRequest, sdp_go.ListChangesBySnapshotUUIDResponse]( + httpClient, + baseURL+ChangesServiceListChangesBySnapshotUUIDProcedure, + connect.WithSchema(changesServiceMethods.ByName("ListChangesBySnapshotUUID")), + connect.WithClientOptions(opts...), + ), + refreshState: connect.NewClient[sdp_go.RefreshStateRequest, sdp_go.RefreshStateResponse]( + httpClient, + baseURL+ChangesServiceRefreshStateProcedure, + connect.WithSchema(changesServiceMethods.ByName("RefreshState")), + connect.WithClientOptions(opts...), + ), + startChange: connect.NewClient[sdp_go.StartChangeRequest, sdp_go.StartChangeResponse]( + httpClient, + baseURL+ChangesServiceStartChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("StartChange")), + connect.WithClientOptions(opts...), + ), + endChange: connect.NewClient[sdp_go.EndChangeRequest, sdp_go.EndChangeResponse]( + httpClient, + baseURL+ChangesServiceEndChangeProcedure, + connect.WithSchema(changesServiceMethods.ByName("EndChange")), + connect.WithClientOptions(opts...), + ), + startChangeSimple: connect.NewClient[sdp_go.StartChangeRequest, sdp_go.StartChangeSimpleResponse]( + httpClient, + baseURL+ChangesServiceStartChangeSimpleProcedure, + connect.WithSchema(changesServiceMethods.ByName("StartChangeSimple")), + connect.WithClientOptions(opts...), + ), + endChangeSimple: connect.NewClient[sdp_go.EndChangeRequest, sdp_go.EndChangeSimpleResponse]( + httpClient, + baseURL+ChangesServiceEndChangeSimpleProcedure, + connect.WithSchema(changesServiceMethods.ByName("EndChangeSimple")), + connect.WithClientOptions(opts...), + ), + listHomeChanges: connect.NewClient[sdp_go.ListHomeChangesRequest, sdp_go.ListHomeChangesResponse]( + httpClient, + baseURL+ChangesServiceListHomeChangesProcedure, + connect.WithSchema(changesServiceMethods.ByName("ListHomeChanges")), + connect.WithClientOptions(opts...), + ), + startChangeAnalysis: connect.NewClient[sdp_go.StartChangeAnalysisRequest, sdp_go.StartChangeAnalysisResponse]( + httpClient, + baseURL+ChangesServiceStartChangeAnalysisProcedure, + connect.WithSchema(changesServiceMethods.ByName("StartChangeAnalysis")), + connect.WithClientOptions(opts...), + ), + listChangingItemsSummary: connect.NewClient[sdp_go.ListChangingItemsSummaryRequest, sdp_go.ListChangingItemsSummaryResponse]( + httpClient, + baseURL+ChangesServiceListChangingItemsSummaryProcedure, + connect.WithSchema(changesServiceMethods.ByName("ListChangingItemsSummary")), + connect.WithClientOptions(opts...), + ), + getDiff: connect.NewClient[sdp_go.GetDiffRequest, sdp_go.GetDiffResponse]( + httpClient, + baseURL+ChangesServiceGetDiffProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetDiff")), + connect.WithClientOptions(opts...), + ), + populateChangeFilters: connect.NewClient[sdp_go.PopulateChangeFiltersRequest, sdp_go.PopulateChangeFiltersResponse]( + httpClient, + baseURL+ChangesServicePopulateChangeFiltersProcedure, + connect.WithSchema(changesServiceMethods.ByName("PopulateChangeFilters")), + connect.WithClientOptions(opts...), + ), + generateRiskFix: connect.NewClient[sdp_go.GenerateRiskFixRequest, sdp_go.GenerateRiskFixResponse]( + httpClient, + baseURL+ChangesServiceGenerateRiskFixProcedure, + connect.WithSchema(changesServiceMethods.ByName("GenerateRiskFix")), + connect.WithClientOptions(opts...), + ), + getHypothesesDetails: connect.NewClient[sdp_go.GetHypothesesDetailsRequest, sdp_go.GetHypothesesDetailsResponse]( + httpClient, + baseURL+ChangesServiceGetHypothesesDetailsProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetHypothesesDetails")), + connect.WithClientOptions(opts...), + ), + getChangeSignals: connect.NewClient[sdp_go.GetChangeSignalsRequest, sdp_go.GetChangeSignalsResponse]( + httpClient, + baseURL+ChangesServiceGetChangeSignalsProcedure, + connect.WithSchema(changesServiceMethods.ByName("GetChangeSignals")), + connect.WithClientOptions(opts...), + ), + } +} + +// changesServiceClient implements ChangesServiceClient. +type changesServiceClient struct { + listChanges *connect.Client[sdp_go.ListChangesRequest, sdp_go.ListChangesResponse] + listChangesByStatus *connect.Client[sdp_go.ListChangesByStatusRequest, sdp_go.ListChangesByStatusResponse] + createChange *connect.Client[sdp_go.CreateChangeRequest, sdp_go.CreateChangeResponse] + getChange *connect.Client[sdp_go.GetChangeRequest, sdp_go.GetChangeResponse] + getChangeByTicketLink *connect.Client[sdp_go.GetChangeByTicketLinkRequest, sdp_go.GetChangeResponse] + getChangeSummary *connect.Client[sdp_go.GetChangeSummaryRequest, sdp_go.GetChangeSummaryResponse] + getChangeTimelineV2 *connect.Client[sdp_go.GetChangeTimelineV2Request, sdp_go.GetChangeTimelineV2Response] + getChangeRisks *connect.Client[sdp_go.GetChangeRisksRequest, sdp_go.GetChangeRisksResponse] + updateChange *connect.Client[sdp_go.UpdateChangeRequest, sdp_go.UpdateChangeResponse] + deleteChange *connect.Client[sdp_go.DeleteChangeRequest, sdp_go.DeleteChangeResponse] + listChangesBySnapshotUUID *connect.Client[sdp_go.ListChangesBySnapshotUUIDRequest, sdp_go.ListChangesBySnapshotUUIDResponse] + refreshState *connect.Client[sdp_go.RefreshStateRequest, sdp_go.RefreshStateResponse] + startChange *connect.Client[sdp_go.StartChangeRequest, sdp_go.StartChangeResponse] + endChange *connect.Client[sdp_go.EndChangeRequest, sdp_go.EndChangeResponse] + startChangeSimple *connect.Client[sdp_go.StartChangeRequest, sdp_go.StartChangeSimpleResponse] + endChangeSimple *connect.Client[sdp_go.EndChangeRequest, sdp_go.EndChangeSimpleResponse] + listHomeChanges *connect.Client[sdp_go.ListHomeChangesRequest, sdp_go.ListHomeChangesResponse] + startChangeAnalysis *connect.Client[sdp_go.StartChangeAnalysisRequest, sdp_go.StartChangeAnalysisResponse] + listChangingItemsSummary *connect.Client[sdp_go.ListChangingItemsSummaryRequest, sdp_go.ListChangingItemsSummaryResponse] + getDiff *connect.Client[sdp_go.GetDiffRequest, sdp_go.GetDiffResponse] + populateChangeFilters *connect.Client[sdp_go.PopulateChangeFiltersRequest, sdp_go.PopulateChangeFiltersResponse] + generateRiskFix *connect.Client[sdp_go.GenerateRiskFixRequest, sdp_go.GenerateRiskFixResponse] + getHypothesesDetails *connect.Client[sdp_go.GetHypothesesDetailsRequest, sdp_go.GetHypothesesDetailsResponse] + getChangeSignals *connect.Client[sdp_go.GetChangeSignalsRequest, sdp_go.GetChangeSignalsResponse] +} + +// ListChanges calls changes.ChangesService.ListChanges. +func (c *changesServiceClient) ListChanges(ctx context.Context, req *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) { + return c.listChanges.CallUnary(ctx, req) +} + +// ListChangesByStatus calls changes.ChangesService.ListChangesByStatus. +func (c *changesServiceClient) ListChangesByStatus(ctx context.Context, req *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) { + return c.listChangesByStatus.CallUnary(ctx, req) +} + +// CreateChange calls changes.ChangesService.CreateChange. +func (c *changesServiceClient) CreateChange(ctx context.Context, req *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) { + return c.createChange.CallUnary(ctx, req) +} + +// GetChange calls changes.ChangesService.GetChange. +func (c *changesServiceClient) GetChange(ctx context.Context, req *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { + return c.getChange.CallUnary(ctx, req) +} + +// GetChangeByTicketLink calls changes.ChangesService.GetChangeByTicketLink. +func (c *changesServiceClient) GetChangeByTicketLink(ctx context.Context, req *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { + return c.getChangeByTicketLink.CallUnary(ctx, req) +} + +// GetChangeSummary calls changes.ChangesService.GetChangeSummary. +func (c *changesServiceClient) GetChangeSummary(ctx context.Context, req *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) { + return c.getChangeSummary.CallUnary(ctx, req) +} + +// GetChangeTimelineV2 calls changes.ChangesService.GetChangeTimelineV2. +func (c *changesServiceClient) GetChangeTimelineV2(ctx context.Context, req *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) { + return c.getChangeTimelineV2.CallUnary(ctx, req) +} + +// GetChangeRisks calls changes.ChangesService.GetChangeRisks. +func (c *changesServiceClient) GetChangeRisks(ctx context.Context, req *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) { + return c.getChangeRisks.CallUnary(ctx, req) +} + +// UpdateChange calls changes.ChangesService.UpdateChange. +func (c *changesServiceClient) UpdateChange(ctx context.Context, req *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) { + return c.updateChange.CallUnary(ctx, req) +} + +// DeleteChange calls changes.ChangesService.DeleteChange. +func (c *changesServiceClient) DeleteChange(ctx context.Context, req *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) { + return c.deleteChange.CallUnary(ctx, req) +} + +// ListChangesBySnapshotUUID calls changes.ChangesService.ListChangesBySnapshotUUID. +func (c *changesServiceClient) ListChangesBySnapshotUUID(ctx context.Context, req *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) { + return c.listChangesBySnapshotUUID.CallUnary(ctx, req) +} + +// RefreshState calls changes.ChangesService.RefreshState. +func (c *changesServiceClient) RefreshState(ctx context.Context, req *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) { + return c.refreshState.CallUnary(ctx, req) +} + +// StartChange calls changes.ChangesService.StartChange. +func (c *changesServiceClient) StartChange(ctx context.Context, req *connect.Request[sdp_go.StartChangeRequest]) (*connect.ServerStreamForClient[sdp_go.StartChangeResponse], error) { + return c.startChange.CallServerStream(ctx, req) +} + +// EndChange calls changes.ChangesService.EndChange. +func (c *changesServiceClient) EndChange(ctx context.Context, req *connect.Request[sdp_go.EndChangeRequest]) (*connect.ServerStreamForClient[sdp_go.EndChangeResponse], error) { + return c.endChange.CallServerStream(ctx, req) +} + +// StartChangeSimple calls changes.ChangesService.StartChangeSimple. +func (c *changesServiceClient) StartChangeSimple(ctx context.Context, req *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) { + return c.startChangeSimple.CallUnary(ctx, req) +} + +// EndChangeSimple calls changes.ChangesService.EndChangeSimple. +func (c *changesServiceClient) EndChangeSimple(ctx context.Context, req *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) { + return c.endChangeSimple.CallUnary(ctx, req) +} + +// ListHomeChanges calls changes.ChangesService.ListHomeChanges. +func (c *changesServiceClient) ListHomeChanges(ctx context.Context, req *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) { + return c.listHomeChanges.CallUnary(ctx, req) +} + +// StartChangeAnalysis calls changes.ChangesService.StartChangeAnalysis. +func (c *changesServiceClient) StartChangeAnalysis(ctx context.Context, req *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) { + return c.startChangeAnalysis.CallUnary(ctx, req) +} + +// ListChangingItemsSummary calls changes.ChangesService.ListChangingItemsSummary. +func (c *changesServiceClient) ListChangingItemsSummary(ctx context.Context, req *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) { + return c.listChangingItemsSummary.CallUnary(ctx, req) +} + +// GetDiff calls changes.ChangesService.GetDiff. +func (c *changesServiceClient) GetDiff(ctx context.Context, req *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) { + return c.getDiff.CallUnary(ctx, req) +} + +// PopulateChangeFilters calls changes.ChangesService.PopulateChangeFilters. +func (c *changesServiceClient) PopulateChangeFilters(ctx context.Context, req *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) { + return c.populateChangeFilters.CallUnary(ctx, req) +} + +// GenerateRiskFix calls changes.ChangesService.GenerateRiskFix. +func (c *changesServiceClient) GenerateRiskFix(ctx context.Context, req *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) { + return c.generateRiskFix.CallUnary(ctx, req) +} + +// GetHypothesesDetails calls changes.ChangesService.GetHypothesesDetails. +func (c *changesServiceClient) GetHypothesesDetails(ctx context.Context, req *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) { + return c.getHypothesesDetails.CallUnary(ctx, req) +} + +// GetChangeSignals calls changes.ChangesService.GetChangeSignals. +func (c *changesServiceClient) GetChangeSignals(ctx context.Context, req *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) { + return c.getChangeSignals.CallUnary(ctx, req) +} + +// ChangesServiceHandler is an implementation of the changes.ChangesService service. +type ChangesServiceHandler interface { + // Lists all changes + ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) + // list all changes in a specific status + ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) + // Creates a new change + CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) + // Gets the details of an existing change + GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) + // Get a change by the ticket link + GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) + // Gets the details of an existing change in markdown format + GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) + // Gets the full timeline for this change, this will send one response + // immediately and then hold the connection open, and send the entire + // timeline again if there are any changes + GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) + // This is used on the blast radius page to get the risks and status for a change. + GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) + // Updates an existing change + UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) + // Deletes a change + DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) + // Lists all changes for a snapshot UUID + ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) + // Ask the gateway to refresh all internal caches and status slots + // The RPC will return immediately doing all processing in the background + RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) + // Executing this RPC take a snapshot of the current blast radius and store it + // in `systemBeforeSnapshotUUID` and then advance the status to + // `STATUS_HAPPENING`. It can only be called once per change. + StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest], *connect.ServerStream[sdp_go.StartChangeResponse]) error + // Takes the "after" snapshot, stores it in `systemAfterSnapshotUUID`, calculates + // the change diff and stores it as a list of DiffedItems and + // advances the change status to `STATUS_DONE` + EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest], *connect.ServerStream[sdp_go.EndChangeResponse]) error + // Simple version of StartChange that returns immediately after enqueuing the job. + // Use this instead of StartChange for non-streaming clients. + StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) + // Simple version of EndChange that returns immediately after enqueuing the job. + // Use this instead of EndChange for non-streaming clients. + EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) + // Lists all changes, designed for use in the changes home page + ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) + // Start the change analysis process. This will calculate various things + // blast radius, risks etc. This will return immediately and + // the results can be fetched using the other RPCs + StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) + // Gets the diff summary for all items that were planned to change as part of + // this change. This includes the high level details of the item, and the + // status (e.g. changed, deleted) but not the diff itself + ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) + // Gets the full diff of everything that changed as part of this "change". + // This includes all items and also edges between them + GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) + // List all the available repos, authors and statuses that can be used to populate the dropdown filters + PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) + // Generates an AI-powered fix suggestion for a specific risk + GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) + // The full details of all of the hypotheses that were considered or are being + // considered as part of this change. + GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) + // Gets all signals for a change, including: + // - Overall signal for the change + // - Top level signals for each category + // - Routineness signals per item + // - Individual custom signals + // This is similar to GetChangeSummary but focused on signals data + GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) +} + +// NewChangesServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewChangesServiceHandler(svc ChangesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + changesServiceMethods := sdp_go.File_changes_proto.Services().ByName("ChangesService").Methods() + changesServiceListChangesHandler := connect.NewUnaryHandler( + ChangesServiceListChangesProcedure, + svc.ListChanges, + connect.WithSchema(changesServiceMethods.ByName("ListChanges")), + connect.WithHandlerOptions(opts...), + ) + changesServiceListChangesByStatusHandler := connect.NewUnaryHandler( + ChangesServiceListChangesByStatusProcedure, + svc.ListChangesByStatus, + connect.WithSchema(changesServiceMethods.ByName("ListChangesByStatus")), + connect.WithHandlerOptions(opts...), + ) + changesServiceCreateChangeHandler := connect.NewUnaryHandler( + ChangesServiceCreateChangeProcedure, + svc.CreateChange, + connect.WithSchema(changesServiceMethods.ByName("CreateChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeHandler := connect.NewUnaryHandler( + ChangesServiceGetChangeProcedure, + svc.GetChange, + connect.WithSchema(changesServiceMethods.ByName("GetChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeByTicketLinkHandler := connect.NewUnaryHandler( + ChangesServiceGetChangeByTicketLinkProcedure, + svc.GetChangeByTicketLink, + connect.WithSchema(changesServiceMethods.ByName("GetChangeByTicketLink")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeSummaryHandler := connect.NewUnaryHandler( + ChangesServiceGetChangeSummaryProcedure, + svc.GetChangeSummary, + connect.WithSchema(changesServiceMethods.ByName("GetChangeSummary")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeTimelineV2Handler := connect.NewUnaryHandler( + ChangesServiceGetChangeTimelineV2Procedure, + svc.GetChangeTimelineV2, + connect.WithSchema(changesServiceMethods.ByName("GetChangeTimelineV2")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeRisksHandler := connect.NewUnaryHandler( + ChangesServiceGetChangeRisksProcedure, + svc.GetChangeRisks, + connect.WithSchema(changesServiceMethods.ByName("GetChangeRisks")), + connect.WithHandlerOptions(opts...), + ) + changesServiceUpdateChangeHandler := connect.NewUnaryHandler( + ChangesServiceUpdateChangeProcedure, + svc.UpdateChange, + connect.WithSchema(changesServiceMethods.ByName("UpdateChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceDeleteChangeHandler := connect.NewUnaryHandler( + ChangesServiceDeleteChangeProcedure, + svc.DeleteChange, + connect.WithSchema(changesServiceMethods.ByName("DeleteChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceListChangesBySnapshotUUIDHandler := connect.NewUnaryHandler( + ChangesServiceListChangesBySnapshotUUIDProcedure, + svc.ListChangesBySnapshotUUID, + connect.WithSchema(changesServiceMethods.ByName("ListChangesBySnapshotUUID")), + connect.WithHandlerOptions(opts...), + ) + changesServiceRefreshStateHandler := connect.NewUnaryHandler( + ChangesServiceRefreshStateProcedure, + svc.RefreshState, + connect.WithSchema(changesServiceMethods.ByName("RefreshState")), + connect.WithHandlerOptions(opts...), + ) + changesServiceStartChangeHandler := connect.NewServerStreamHandler( + ChangesServiceStartChangeProcedure, + svc.StartChange, + connect.WithSchema(changesServiceMethods.ByName("StartChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceEndChangeHandler := connect.NewServerStreamHandler( + ChangesServiceEndChangeProcedure, + svc.EndChange, + connect.WithSchema(changesServiceMethods.ByName("EndChange")), + connect.WithHandlerOptions(opts...), + ) + changesServiceStartChangeSimpleHandler := connect.NewUnaryHandler( + ChangesServiceStartChangeSimpleProcedure, + svc.StartChangeSimple, + connect.WithSchema(changesServiceMethods.ByName("StartChangeSimple")), + connect.WithHandlerOptions(opts...), + ) + changesServiceEndChangeSimpleHandler := connect.NewUnaryHandler( + ChangesServiceEndChangeSimpleProcedure, + svc.EndChangeSimple, + connect.WithSchema(changesServiceMethods.ByName("EndChangeSimple")), + connect.WithHandlerOptions(opts...), + ) + changesServiceListHomeChangesHandler := connect.NewUnaryHandler( + ChangesServiceListHomeChangesProcedure, + svc.ListHomeChanges, + connect.WithSchema(changesServiceMethods.ByName("ListHomeChanges")), + connect.WithHandlerOptions(opts...), + ) + changesServiceStartChangeAnalysisHandler := connect.NewUnaryHandler( + ChangesServiceStartChangeAnalysisProcedure, + svc.StartChangeAnalysis, + connect.WithSchema(changesServiceMethods.ByName("StartChangeAnalysis")), + connect.WithHandlerOptions(opts...), + ) + changesServiceListChangingItemsSummaryHandler := connect.NewUnaryHandler( + ChangesServiceListChangingItemsSummaryProcedure, + svc.ListChangingItemsSummary, + connect.WithSchema(changesServiceMethods.ByName("ListChangingItemsSummary")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetDiffHandler := connect.NewUnaryHandler( + ChangesServiceGetDiffProcedure, + svc.GetDiff, + connect.WithSchema(changesServiceMethods.ByName("GetDiff")), + connect.WithHandlerOptions(opts...), + ) + changesServicePopulateChangeFiltersHandler := connect.NewUnaryHandler( + ChangesServicePopulateChangeFiltersProcedure, + svc.PopulateChangeFilters, + connect.WithSchema(changesServiceMethods.ByName("PopulateChangeFilters")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGenerateRiskFixHandler := connect.NewUnaryHandler( + ChangesServiceGenerateRiskFixProcedure, + svc.GenerateRiskFix, + connect.WithSchema(changesServiceMethods.ByName("GenerateRiskFix")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetHypothesesDetailsHandler := connect.NewUnaryHandler( + ChangesServiceGetHypothesesDetailsProcedure, + svc.GetHypothesesDetails, + connect.WithSchema(changesServiceMethods.ByName("GetHypothesesDetails")), + connect.WithHandlerOptions(opts...), + ) + changesServiceGetChangeSignalsHandler := connect.NewUnaryHandler( + ChangesServiceGetChangeSignalsProcedure, + svc.GetChangeSignals, + connect.WithSchema(changesServiceMethods.ByName("GetChangeSignals")), + connect.WithHandlerOptions(opts...), + ) + return "/changes.ChangesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ChangesServiceListChangesProcedure: + changesServiceListChangesHandler.ServeHTTP(w, r) + case ChangesServiceListChangesByStatusProcedure: + changesServiceListChangesByStatusHandler.ServeHTTP(w, r) + case ChangesServiceCreateChangeProcedure: + changesServiceCreateChangeHandler.ServeHTTP(w, r) + case ChangesServiceGetChangeProcedure: + changesServiceGetChangeHandler.ServeHTTP(w, r) + case ChangesServiceGetChangeByTicketLinkProcedure: + changesServiceGetChangeByTicketLinkHandler.ServeHTTP(w, r) + case ChangesServiceGetChangeSummaryProcedure: + changesServiceGetChangeSummaryHandler.ServeHTTP(w, r) + case ChangesServiceGetChangeTimelineV2Procedure: + changesServiceGetChangeTimelineV2Handler.ServeHTTP(w, r) + case ChangesServiceGetChangeRisksProcedure: + changesServiceGetChangeRisksHandler.ServeHTTP(w, r) + case ChangesServiceUpdateChangeProcedure: + changesServiceUpdateChangeHandler.ServeHTTP(w, r) + case ChangesServiceDeleteChangeProcedure: + changesServiceDeleteChangeHandler.ServeHTTP(w, r) + case ChangesServiceListChangesBySnapshotUUIDProcedure: + changesServiceListChangesBySnapshotUUIDHandler.ServeHTTP(w, r) + case ChangesServiceRefreshStateProcedure: + changesServiceRefreshStateHandler.ServeHTTP(w, r) + case ChangesServiceStartChangeProcedure: + changesServiceStartChangeHandler.ServeHTTP(w, r) + case ChangesServiceEndChangeProcedure: + changesServiceEndChangeHandler.ServeHTTP(w, r) + case ChangesServiceStartChangeSimpleProcedure: + changesServiceStartChangeSimpleHandler.ServeHTTP(w, r) + case ChangesServiceEndChangeSimpleProcedure: + changesServiceEndChangeSimpleHandler.ServeHTTP(w, r) + case ChangesServiceListHomeChangesProcedure: + changesServiceListHomeChangesHandler.ServeHTTP(w, r) + case ChangesServiceStartChangeAnalysisProcedure: + changesServiceStartChangeAnalysisHandler.ServeHTTP(w, r) + case ChangesServiceListChangingItemsSummaryProcedure: + changesServiceListChangingItemsSummaryHandler.ServeHTTP(w, r) + case ChangesServiceGetDiffProcedure: + changesServiceGetDiffHandler.ServeHTTP(w, r) + case ChangesServicePopulateChangeFiltersProcedure: + changesServicePopulateChangeFiltersHandler.ServeHTTP(w, r) + case ChangesServiceGenerateRiskFixProcedure: + changesServiceGenerateRiskFixHandler.ServeHTTP(w, r) + case ChangesServiceGetHypothesesDetailsProcedure: + changesServiceGetHypothesesDetailsHandler.ServeHTTP(w, r) + case ChangesServiceGetChangeSignalsProcedure: + changesServiceGetChangeSignalsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedChangesServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedChangesServiceHandler struct{} + +func (UnimplementedChangesServiceHandler) ListChanges(context.Context, *connect.Request[sdp_go.ListChangesRequest]) (*connect.Response[sdp_go.ListChangesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChanges is not implemented")) +} + +func (UnimplementedChangesServiceHandler) ListChangesByStatus(context.Context, *connect.Request[sdp_go.ListChangesByStatusRequest]) (*connect.Response[sdp_go.ListChangesByStatusResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangesByStatus is not implemented")) +} + +func (UnimplementedChangesServiceHandler) CreateChange(context.Context, *connect.Request[sdp_go.CreateChangeRequest]) (*connect.Response[sdp_go.CreateChangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.CreateChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChange(context.Context, *connect.Request[sdp_go.GetChangeRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChangeByTicketLink(context.Context, *connect.Request[sdp_go.GetChangeByTicketLinkRequest]) (*connect.Response[sdp_go.GetChangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeByTicketLink is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChangeSummary(context.Context, *connect.Request[sdp_go.GetChangeSummaryRequest]) (*connect.Response[sdp_go.GetChangeSummaryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeSummary is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChangeTimelineV2(context.Context, *connect.Request[sdp_go.GetChangeTimelineV2Request]) (*connect.Response[sdp_go.GetChangeTimelineV2Response], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeTimelineV2 is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChangeRisks(context.Context, *connect.Request[sdp_go.GetChangeRisksRequest]) (*connect.Response[sdp_go.GetChangeRisksResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeRisks is not implemented")) +} + +func (UnimplementedChangesServiceHandler) UpdateChange(context.Context, *connect.Request[sdp_go.UpdateChangeRequest]) (*connect.Response[sdp_go.UpdateChangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.UpdateChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) DeleteChange(context.Context, *connect.Request[sdp_go.DeleteChangeRequest]) (*connect.Response[sdp_go.DeleteChangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.DeleteChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) ListChangesBySnapshotUUID(context.Context, *connect.Request[sdp_go.ListChangesBySnapshotUUIDRequest]) (*connect.Response[sdp_go.ListChangesBySnapshotUUIDResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangesBySnapshotUUID is not implemented")) +} + +func (UnimplementedChangesServiceHandler) RefreshState(context.Context, *connect.Request[sdp_go.RefreshStateRequest]) (*connect.Response[sdp_go.RefreshStateResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.RefreshState is not implemented")) +} + +func (UnimplementedChangesServiceHandler) StartChange(context.Context, *connect.Request[sdp_go.StartChangeRequest], *connect.ServerStream[sdp_go.StartChangeResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) EndChange(context.Context, *connect.Request[sdp_go.EndChangeRequest], *connect.ServerStream[sdp_go.EndChangeResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.EndChange is not implemented")) +} + +func (UnimplementedChangesServiceHandler) StartChangeSimple(context.Context, *connect.Request[sdp_go.StartChangeRequest]) (*connect.Response[sdp_go.StartChangeSimpleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChangeSimple is not implemented")) +} + +func (UnimplementedChangesServiceHandler) EndChangeSimple(context.Context, *connect.Request[sdp_go.EndChangeRequest]) (*connect.Response[sdp_go.EndChangeSimpleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.EndChangeSimple is not implemented")) +} + +func (UnimplementedChangesServiceHandler) ListHomeChanges(context.Context, *connect.Request[sdp_go.ListHomeChangesRequest]) (*connect.Response[sdp_go.ListHomeChangesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListHomeChanges is not implemented")) +} + +func (UnimplementedChangesServiceHandler) StartChangeAnalysis(context.Context, *connect.Request[sdp_go.StartChangeAnalysisRequest]) (*connect.Response[sdp_go.StartChangeAnalysisResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.StartChangeAnalysis is not implemented")) +} + +func (UnimplementedChangesServiceHandler) ListChangingItemsSummary(context.Context, *connect.Request[sdp_go.ListChangingItemsSummaryRequest]) (*connect.Response[sdp_go.ListChangingItemsSummaryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.ListChangingItemsSummary is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetDiff(context.Context, *connect.Request[sdp_go.GetDiffRequest]) (*connect.Response[sdp_go.GetDiffResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetDiff is not implemented")) +} + +func (UnimplementedChangesServiceHandler) PopulateChangeFilters(context.Context, *connect.Request[sdp_go.PopulateChangeFiltersRequest]) (*connect.Response[sdp_go.PopulateChangeFiltersResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.PopulateChangeFilters is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GenerateRiskFix(context.Context, *connect.Request[sdp_go.GenerateRiskFixRequest]) (*connect.Response[sdp_go.GenerateRiskFixResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GenerateRiskFix is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetHypothesesDetails(context.Context, *connect.Request[sdp_go.GetHypothesesDetailsRequest]) (*connect.Response[sdp_go.GetHypothesesDetailsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetHypothesesDetails is not implemented")) +} + +func (UnimplementedChangesServiceHandler) GetChangeSignals(context.Context, *connect.Request[sdp_go.GetChangeSignalsRequest]) (*connect.Response[sdp_go.GetChangeSignalsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.ChangesService.GetChangeSignals is not implemented")) +} + +// LabelServiceClient is a client for the changes.LabelService service. +type LabelServiceClient interface { + // Lists all label rules for an account + ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) + // Creates a new label rule + CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) + // Gets the details of a label rule + GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) + // Updates a label rule + UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) + // Deletes a label rule + // this also removes the label from all changes that are currently labelled with this rule + DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) + // Test a label rule against a list of changes, this does not apply the label to the changes, it only tests if the label should be applied + TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest]) (*connect.ServerStreamForClient[sdp_go.TestLabelRuleResponse], error) + // Re-apply a label rule across all changes within a specified time window: + // 1. Removes the label from all relevant changes in the period that match this rule + // 2. Applies (or re-applies) the label to eligible changes in the period + ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) +} + +// NewLabelServiceClient constructs a client for the changes.LabelService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewLabelServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) LabelServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + labelServiceMethods := sdp_go.File_changes_proto.Services().ByName("LabelService").Methods() + return &labelServiceClient{ + listLabelRules: connect.NewClient[sdp_go.ListLabelRulesRequest, sdp_go.ListLabelRulesResponse]( + httpClient, + baseURL+LabelServiceListLabelRulesProcedure, + connect.WithSchema(labelServiceMethods.ByName("ListLabelRules")), + connect.WithClientOptions(opts...), + ), + createLabelRule: connect.NewClient[sdp_go.CreateLabelRuleRequest, sdp_go.CreateLabelRuleResponse]( + httpClient, + baseURL+LabelServiceCreateLabelRuleProcedure, + connect.WithSchema(labelServiceMethods.ByName("CreateLabelRule")), + connect.WithClientOptions(opts...), + ), + getLabelRule: connect.NewClient[sdp_go.GetLabelRuleRequest, sdp_go.GetLabelRuleResponse]( + httpClient, + baseURL+LabelServiceGetLabelRuleProcedure, + connect.WithSchema(labelServiceMethods.ByName("GetLabelRule")), + connect.WithClientOptions(opts...), + ), + updateLabelRule: connect.NewClient[sdp_go.UpdateLabelRuleRequest, sdp_go.UpdateLabelRuleResponse]( + httpClient, + baseURL+LabelServiceUpdateLabelRuleProcedure, + connect.WithSchema(labelServiceMethods.ByName("UpdateLabelRule")), + connect.WithClientOptions(opts...), + ), + deleteLabelRule: connect.NewClient[sdp_go.DeleteLabelRuleRequest, sdp_go.DeleteLabelRuleResponse]( + httpClient, + baseURL+LabelServiceDeleteLabelRuleProcedure, + connect.WithSchema(labelServiceMethods.ByName("DeleteLabelRule")), + connect.WithClientOptions(opts...), + ), + testLabelRule: connect.NewClient[sdp_go.TestLabelRuleRequest, sdp_go.TestLabelRuleResponse]( + httpClient, + baseURL+LabelServiceTestLabelRuleProcedure, + connect.WithSchema(labelServiceMethods.ByName("TestLabelRule")), + connect.WithClientOptions(opts...), + ), + reapplyLabelRuleInTimeRange: connect.NewClient[sdp_go.ReapplyLabelRuleInTimeRangeRequest, sdp_go.ReapplyLabelRuleInTimeRangeResponse]( + httpClient, + baseURL+LabelServiceReapplyLabelRuleInTimeRangeProcedure, + connect.WithSchema(labelServiceMethods.ByName("ReapplyLabelRuleInTimeRange")), + connect.WithClientOptions(opts...), + ), + } +} + +// labelServiceClient implements LabelServiceClient. +type labelServiceClient struct { + listLabelRules *connect.Client[sdp_go.ListLabelRulesRequest, sdp_go.ListLabelRulesResponse] + createLabelRule *connect.Client[sdp_go.CreateLabelRuleRequest, sdp_go.CreateLabelRuleResponse] + getLabelRule *connect.Client[sdp_go.GetLabelRuleRequest, sdp_go.GetLabelRuleResponse] + updateLabelRule *connect.Client[sdp_go.UpdateLabelRuleRequest, sdp_go.UpdateLabelRuleResponse] + deleteLabelRule *connect.Client[sdp_go.DeleteLabelRuleRequest, sdp_go.DeleteLabelRuleResponse] + testLabelRule *connect.Client[sdp_go.TestLabelRuleRequest, sdp_go.TestLabelRuleResponse] + reapplyLabelRuleInTimeRange *connect.Client[sdp_go.ReapplyLabelRuleInTimeRangeRequest, sdp_go.ReapplyLabelRuleInTimeRangeResponse] +} + +// ListLabelRules calls changes.LabelService.ListLabelRules. +func (c *labelServiceClient) ListLabelRules(ctx context.Context, req *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) { + return c.listLabelRules.CallUnary(ctx, req) +} + +// CreateLabelRule calls changes.LabelService.CreateLabelRule. +func (c *labelServiceClient) CreateLabelRule(ctx context.Context, req *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) { + return c.createLabelRule.CallUnary(ctx, req) +} + +// GetLabelRule calls changes.LabelService.GetLabelRule. +func (c *labelServiceClient) GetLabelRule(ctx context.Context, req *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) { + return c.getLabelRule.CallUnary(ctx, req) +} + +// UpdateLabelRule calls changes.LabelService.UpdateLabelRule. +func (c *labelServiceClient) UpdateLabelRule(ctx context.Context, req *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) { + return c.updateLabelRule.CallUnary(ctx, req) +} + +// DeleteLabelRule calls changes.LabelService.DeleteLabelRule. +func (c *labelServiceClient) DeleteLabelRule(ctx context.Context, req *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) { + return c.deleteLabelRule.CallUnary(ctx, req) +} + +// TestLabelRule calls changes.LabelService.TestLabelRule. +func (c *labelServiceClient) TestLabelRule(ctx context.Context, req *connect.Request[sdp_go.TestLabelRuleRequest]) (*connect.ServerStreamForClient[sdp_go.TestLabelRuleResponse], error) { + return c.testLabelRule.CallServerStream(ctx, req) +} + +// ReapplyLabelRuleInTimeRange calls changes.LabelService.ReapplyLabelRuleInTimeRange. +func (c *labelServiceClient) ReapplyLabelRuleInTimeRange(ctx context.Context, req *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) { + return c.reapplyLabelRuleInTimeRange.CallUnary(ctx, req) +} + +// LabelServiceHandler is an implementation of the changes.LabelService service. +type LabelServiceHandler interface { + // Lists all label rules for an account + ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) + // Creates a new label rule + CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) + // Gets the details of a label rule + GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) + // Updates a label rule + UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) + // Deletes a label rule + // this also removes the label from all changes that are currently labelled with this rule + DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) + // Test a label rule against a list of changes, this does not apply the label to the changes, it only tests if the label should be applied + TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest], *connect.ServerStream[sdp_go.TestLabelRuleResponse]) error + // Re-apply a label rule across all changes within a specified time window: + // 1. Removes the label from all relevant changes in the period that match this rule + // 2. Applies (or re-applies) the label to eligible changes in the period + ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) +} + +// NewLabelServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewLabelServiceHandler(svc LabelServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + labelServiceMethods := sdp_go.File_changes_proto.Services().ByName("LabelService").Methods() + labelServiceListLabelRulesHandler := connect.NewUnaryHandler( + LabelServiceListLabelRulesProcedure, + svc.ListLabelRules, + connect.WithSchema(labelServiceMethods.ByName("ListLabelRules")), + connect.WithHandlerOptions(opts...), + ) + labelServiceCreateLabelRuleHandler := connect.NewUnaryHandler( + LabelServiceCreateLabelRuleProcedure, + svc.CreateLabelRule, + connect.WithSchema(labelServiceMethods.ByName("CreateLabelRule")), + connect.WithHandlerOptions(opts...), + ) + labelServiceGetLabelRuleHandler := connect.NewUnaryHandler( + LabelServiceGetLabelRuleProcedure, + svc.GetLabelRule, + connect.WithSchema(labelServiceMethods.ByName("GetLabelRule")), + connect.WithHandlerOptions(opts...), + ) + labelServiceUpdateLabelRuleHandler := connect.NewUnaryHandler( + LabelServiceUpdateLabelRuleProcedure, + svc.UpdateLabelRule, + connect.WithSchema(labelServiceMethods.ByName("UpdateLabelRule")), + connect.WithHandlerOptions(opts...), + ) + labelServiceDeleteLabelRuleHandler := connect.NewUnaryHandler( + LabelServiceDeleteLabelRuleProcedure, + svc.DeleteLabelRule, + connect.WithSchema(labelServiceMethods.ByName("DeleteLabelRule")), + connect.WithHandlerOptions(opts...), + ) + labelServiceTestLabelRuleHandler := connect.NewServerStreamHandler( + LabelServiceTestLabelRuleProcedure, + svc.TestLabelRule, + connect.WithSchema(labelServiceMethods.ByName("TestLabelRule")), + connect.WithHandlerOptions(opts...), + ) + labelServiceReapplyLabelRuleInTimeRangeHandler := connect.NewUnaryHandler( + LabelServiceReapplyLabelRuleInTimeRangeProcedure, + svc.ReapplyLabelRuleInTimeRange, + connect.WithSchema(labelServiceMethods.ByName("ReapplyLabelRuleInTimeRange")), + connect.WithHandlerOptions(opts...), + ) + return "/changes.LabelService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case LabelServiceListLabelRulesProcedure: + labelServiceListLabelRulesHandler.ServeHTTP(w, r) + case LabelServiceCreateLabelRuleProcedure: + labelServiceCreateLabelRuleHandler.ServeHTTP(w, r) + case LabelServiceGetLabelRuleProcedure: + labelServiceGetLabelRuleHandler.ServeHTTP(w, r) + case LabelServiceUpdateLabelRuleProcedure: + labelServiceUpdateLabelRuleHandler.ServeHTTP(w, r) + case LabelServiceDeleteLabelRuleProcedure: + labelServiceDeleteLabelRuleHandler.ServeHTTP(w, r) + case LabelServiceTestLabelRuleProcedure: + labelServiceTestLabelRuleHandler.ServeHTTP(w, r) + case LabelServiceReapplyLabelRuleInTimeRangeProcedure: + labelServiceReapplyLabelRuleInTimeRangeHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedLabelServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedLabelServiceHandler struct{} + +func (UnimplementedLabelServiceHandler) ListLabelRules(context.Context, *connect.Request[sdp_go.ListLabelRulesRequest]) (*connect.Response[sdp_go.ListLabelRulesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.ListLabelRules is not implemented")) +} + +func (UnimplementedLabelServiceHandler) CreateLabelRule(context.Context, *connect.Request[sdp_go.CreateLabelRuleRequest]) (*connect.Response[sdp_go.CreateLabelRuleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.CreateLabelRule is not implemented")) +} + +func (UnimplementedLabelServiceHandler) GetLabelRule(context.Context, *connect.Request[sdp_go.GetLabelRuleRequest]) (*connect.Response[sdp_go.GetLabelRuleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.GetLabelRule is not implemented")) +} + +func (UnimplementedLabelServiceHandler) UpdateLabelRule(context.Context, *connect.Request[sdp_go.UpdateLabelRuleRequest]) (*connect.Response[sdp_go.UpdateLabelRuleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.UpdateLabelRule is not implemented")) +} + +func (UnimplementedLabelServiceHandler) DeleteLabelRule(context.Context, *connect.Request[sdp_go.DeleteLabelRuleRequest]) (*connect.Response[sdp_go.DeleteLabelRuleResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.DeleteLabelRule is not implemented")) +} + +func (UnimplementedLabelServiceHandler) TestLabelRule(context.Context, *connect.Request[sdp_go.TestLabelRuleRequest], *connect.ServerStream[sdp_go.TestLabelRuleResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.TestLabelRule is not implemented")) +} + +func (UnimplementedLabelServiceHandler) ReapplyLabelRuleInTimeRange(context.Context, *connect.Request[sdp_go.ReapplyLabelRuleInTimeRangeRequest]) (*connect.Response[sdp_go.ReapplyLabelRuleInTimeRangeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("changes.LabelService.ReapplyLabelRuleInTimeRange is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/cli.connect.go b/go/sdp-go/sdpconnect/cli.connect.go new file mode 100644 index 00000000..df4d52ff --- /dev/null +++ b/go/sdp-go/sdpconnect/cli.connect.go @@ -0,0 +1,136 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: cli.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ConfigServiceName is the fully-qualified name of the ConfigService service. + ConfigServiceName = "cli.ConfigService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ConfigServiceGetConfigProcedure is the fully-qualified name of the ConfigService's GetConfig RPC. + ConfigServiceGetConfigProcedure = "/cli.ConfigService/GetConfig" + // ConfigServiceSetConfigProcedure is the fully-qualified name of the ConfigService's SetConfig RPC. + ConfigServiceSetConfigProcedure = "/cli.ConfigService/SetConfig" +) + +// ConfigServiceClient is a client for the cli.ConfigService service. +type ConfigServiceClient interface { + GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) + SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) +} + +// NewConfigServiceClient constructs a client for the cli.ConfigService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewConfigServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConfigServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + configServiceMethods := sdp_go.File_cli_proto.Services().ByName("ConfigService").Methods() + return &configServiceClient{ + getConfig: connect.NewClient[sdp_go.GetConfigRequest, sdp_go.GetConfigResponse]( + httpClient, + baseURL+ConfigServiceGetConfigProcedure, + connect.WithSchema(configServiceMethods.ByName("GetConfig")), + connect.WithClientOptions(opts...), + ), + setConfig: connect.NewClient[sdp_go.SetConfigRequest, sdp_go.SetConfigResponse]( + httpClient, + baseURL+ConfigServiceSetConfigProcedure, + connect.WithSchema(configServiceMethods.ByName("SetConfig")), + connect.WithClientOptions(opts...), + ), + } +} + +// configServiceClient implements ConfigServiceClient. +type configServiceClient struct { + getConfig *connect.Client[sdp_go.GetConfigRequest, sdp_go.GetConfigResponse] + setConfig *connect.Client[sdp_go.SetConfigRequest, sdp_go.SetConfigResponse] +} + +// GetConfig calls cli.ConfigService.GetConfig. +func (c *configServiceClient) GetConfig(ctx context.Context, req *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) { + return c.getConfig.CallUnary(ctx, req) +} + +// SetConfig calls cli.ConfigService.SetConfig. +func (c *configServiceClient) SetConfig(ctx context.Context, req *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) { + return c.setConfig.CallUnary(ctx, req) +} + +// ConfigServiceHandler is an implementation of the cli.ConfigService service. +type ConfigServiceHandler interface { + GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) + SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) +} + +// NewConfigServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewConfigServiceHandler(svc ConfigServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + configServiceMethods := sdp_go.File_cli_proto.Services().ByName("ConfigService").Methods() + configServiceGetConfigHandler := connect.NewUnaryHandler( + ConfigServiceGetConfigProcedure, + svc.GetConfig, + connect.WithSchema(configServiceMethods.ByName("GetConfig")), + connect.WithHandlerOptions(opts...), + ) + configServiceSetConfigHandler := connect.NewUnaryHandler( + ConfigServiceSetConfigProcedure, + svc.SetConfig, + connect.WithSchema(configServiceMethods.ByName("SetConfig")), + connect.WithHandlerOptions(opts...), + ) + return "/cli.ConfigService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ConfigServiceGetConfigProcedure: + configServiceGetConfigHandler.ServeHTTP(w, r) + case ConfigServiceSetConfigProcedure: + configServiceSetConfigHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedConfigServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedConfigServiceHandler struct{} + +func (UnimplementedConfigServiceHandler) GetConfig(context.Context, *connect.Request[sdp_go.GetConfigRequest]) (*connect.Response[sdp_go.GetConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("cli.ConfigService.GetConfig is not implemented")) +} + +func (UnimplementedConfigServiceHandler) SetConfig(context.Context, *connect.Request[sdp_go.SetConfigRequest]) (*connect.Response[sdp_go.SetConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("cli.ConfigService.SetConfig is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/config.connect.go b/go/sdp-go/sdpconnect/config.connect.go new file mode 100644 index 00000000..86befb73 --- /dev/null +++ b/go/sdp-go/sdpconnect/config.connect.go @@ -0,0 +1,434 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: config.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ConfigurationServiceName is the fully-qualified name of the ConfigurationService service. + ConfigurationServiceName = "config.ConfigurationService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ConfigurationServiceGetAccountConfigProcedure is the fully-qualified name of the + // ConfigurationService's GetAccountConfig RPC. + ConfigurationServiceGetAccountConfigProcedure = "/config.ConfigurationService/GetAccountConfig" + // ConfigurationServiceUpdateAccountConfigProcedure is the fully-qualified name of the + // ConfigurationService's UpdateAccountConfig RPC. + ConfigurationServiceUpdateAccountConfigProcedure = "/config.ConfigurationService/UpdateAccountConfig" + // ConfigurationServiceCreateHcpConfigProcedure is the fully-qualified name of the + // ConfigurationService's CreateHcpConfig RPC. + ConfigurationServiceCreateHcpConfigProcedure = "/config.ConfigurationService/CreateHcpConfig" + // ConfigurationServiceGetHcpConfigProcedure is the fully-qualified name of the + // ConfigurationService's GetHcpConfig RPC. + ConfigurationServiceGetHcpConfigProcedure = "/config.ConfigurationService/GetHcpConfig" + // ConfigurationServiceDeleteHcpConfigProcedure is the fully-qualified name of the + // ConfigurationService's DeleteHcpConfig RPC. + ConfigurationServiceDeleteHcpConfigProcedure = "/config.ConfigurationService/DeleteHcpConfig" + // ConfigurationServiceReplaceHcpApiKeyProcedure is the fully-qualified name of the + // ConfigurationService's ReplaceHcpApiKey RPC. + ConfigurationServiceReplaceHcpApiKeyProcedure = "/config.ConfigurationService/ReplaceHcpApiKey" + // ConfigurationServiceGetSignalConfigProcedure is the fully-qualified name of the + // ConfigurationService's GetSignalConfig RPC. + ConfigurationServiceGetSignalConfigProcedure = "/config.ConfigurationService/GetSignalConfig" + // ConfigurationServiceUpdateSignalConfigProcedure is the fully-qualified name of the + // ConfigurationService's UpdateSignalConfig RPC. + ConfigurationServiceUpdateSignalConfigProcedure = "/config.ConfigurationService/UpdateSignalConfig" + // ConfigurationServiceGetGithubAppInformationProcedure is the fully-qualified name of the + // ConfigurationService's GetGithubAppInformation RPC. + ConfigurationServiceGetGithubAppInformationProcedure = "/config.ConfigurationService/GetGithubAppInformation" + // ConfigurationServiceRegenerateGithubAppProfileProcedure is the fully-qualified name of the + // ConfigurationService's RegenerateGithubAppProfile RPC. + ConfigurationServiceRegenerateGithubAppProfileProcedure = "/config.ConfigurationService/RegenerateGithubAppProfile" + // ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure is the fully-qualified + // name of the ConfigurationService's DeleteGithubAppProfileAndGithubInstallationID RPC. + ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure = "/config.ConfigurationService/DeleteGithubAppProfileAndGithubInstallationID" +) + +// ConfigurationServiceClient is a client for the config.ConfigurationService service. +type ConfigurationServiceClient interface { + // Get the account config for the user's account + GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) + // Update the account config for the user's account + UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) + // Create a new HCP Terraform config for the user's account. This follows + // the same flow as CreateAPIKey, to create a new API key that is then used + // for the HCP Terraform endpoint URL. + CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) + // Get the existing HCP Terraform config for the user's account. + GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) + // Remove the existing HCP Terraform config from the user's account. + DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) + // Replace the API key backing the HCP Terraform integration with a fresh + // one. The old API key is revoked. The endpoint URL and HMAC secret are + // preserved. Follows the same OAuth flow as CreateHcpConfig. + ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) + // Get the signal config for the account + GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) + // Update the signal config for the account + UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) + // Github app + // we will be displaying app installation information for this account on the github integrations page + GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) + // regenerate the github app profile, this information is used for signal processing + RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) + // remove the github app installation id and github organisation profile from the signal config + // this will not uninstall the app from github, that must be done manually by the user + DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) +} + +// NewConfigurationServiceClient constructs a client for the config.ConfigurationService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewConfigurationServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConfigurationServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + configurationServiceMethods := sdp_go.File_config_proto.Services().ByName("ConfigurationService").Methods() + return &configurationServiceClient{ + getAccountConfig: connect.NewClient[sdp_go.GetAccountConfigRequest, sdp_go.GetAccountConfigResponse]( + httpClient, + baseURL+ConfigurationServiceGetAccountConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("GetAccountConfig")), + connect.WithClientOptions(opts...), + ), + updateAccountConfig: connect.NewClient[sdp_go.UpdateAccountConfigRequest, sdp_go.UpdateAccountConfigResponse]( + httpClient, + baseURL+ConfigurationServiceUpdateAccountConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("UpdateAccountConfig")), + connect.WithClientOptions(opts...), + ), + createHcpConfig: connect.NewClient[sdp_go.CreateHcpConfigRequest, sdp_go.CreateHcpConfigResponse]( + httpClient, + baseURL+ConfigurationServiceCreateHcpConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("CreateHcpConfig")), + connect.WithClientOptions(opts...), + ), + getHcpConfig: connect.NewClient[sdp_go.GetHcpConfigRequest, sdp_go.GetHcpConfigResponse]( + httpClient, + baseURL+ConfigurationServiceGetHcpConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("GetHcpConfig")), + connect.WithClientOptions(opts...), + ), + deleteHcpConfig: connect.NewClient[sdp_go.DeleteHcpConfigRequest, sdp_go.DeleteHcpConfigResponse]( + httpClient, + baseURL+ConfigurationServiceDeleteHcpConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), + connect.WithClientOptions(opts...), + ), + replaceHcpApiKey: connect.NewClient[sdp_go.ReplaceHcpApiKeyRequest, sdp_go.ReplaceHcpApiKeyResponse]( + httpClient, + baseURL+ConfigurationServiceReplaceHcpApiKeyProcedure, + connect.WithSchema(configurationServiceMethods.ByName("ReplaceHcpApiKey")), + connect.WithClientOptions(opts...), + ), + getSignalConfig: connect.NewClient[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse]( + httpClient, + baseURL+ConfigurationServiceGetSignalConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("GetSignalConfig")), + connect.WithClientOptions(opts...), + ), + updateSignalConfig: connect.NewClient[sdp_go.UpdateSignalConfigRequest, sdp_go.UpdateSignalConfigResponse]( + httpClient, + baseURL+ConfigurationServiceUpdateSignalConfigProcedure, + connect.WithSchema(configurationServiceMethods.ByName("UpdateSignalConfig")), + connect.WithClientOptions(opts...), + ), + getGithubAppInformation: connect.NewClient[sdp_go.GetGithubAppInformationRequest, sdp_go.GetGithubAppInformationResponse]( + httpClient, + baseURL+ConfigurationServiceGetGithubAppInformationProcedure, + connect.WithSchema(configurationServiceMethods.ByName("GetGithubAppInformation")), + connect.WithClientOptions(opts...), + ), + regenerateGithubAppProfile: connect.NewClient[sdp_go.RegenerateGithubAppProfileRequest, sdp_go.RegenerateGithubAppProfileResponse]( + httpClient, + baseURL+ConfigurationServiceRegenerateGithubAppProfileProcedure, + connect.WithSchema(configurationServiceMethods.ByName("RegenerateGithubAppProfile")), + connect.WithClientOptions(opts...), + ), + deleteGithubAppProfileAndGithubInstallationID: connect.NewClient[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest, sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse]( + httpClient, + baseURL+ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure, + connect.WithSchema(configurationServiceMethods.ByName("DeleteGithubAppProfileAndGithubInstallationID")), + connect.WithClientOptions(opts...), + ), + } +} + +// configurationServiceClient implements ConfigurationServiceClient. +type configurationServiceClient struct { + getAccountConfig *connect.Client[sdp_go.GetAccountConfigRequest, sdp_go.GetAccountConfigResponse] + updateAccountConfig *connect.Client[sdp_go.UpdateAccountConfigRequest, sdp_go.UpdateAccountConfigResponse] + createHcpConfig *connect.Client[sdp_go.CreateHcpConfigRequest, sdp_go.CreateHcpConfigResponse] + getHcpConfig *connect.Client[sdp_go.GetHcpConfigRequest, sdp_go.GetHcpConfigResponse] + deleteHcpConfig *connect.Client[sdp_go.DeleteHcpConfigRequest, sdp_go.DeleteHcpConfigResponse] + replaceHcpApiKey *connect.Client[sdp_go.ReplaceHcpApiKeyRequest, sdp_go.ReplaceHcpApiKeyResponse] + getSignalConfig *connect.Client[sdp_go.GetSignalConfigRequest, sdp_go.GetSignalConfigResponse] + updateSignalConfig *connect.Client[sdp_go.UpdateSignalConfigRequest, sdp_go.UpdateSignalConfigResponse] + getGithubAppInformation *connect.Client[sdp_go.GetGithubAppInformationRequest, sdp_go.GetGithubAppInformationResponse] + regenerateGithubAppProfile *connect.Client[sdp_go.RegenerateGithubAppProfileRequest, sdp_go.RegenerateGithubAppProfileResponse] + deleteGithubAppProfileAndGithubInstallationID *connect.Client[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest, sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse] +} + +// GetAccountConfig calls config.ConfigurationService.GetAccountConfig. +func (c *configurationServiceClient) GetAccountConfig(ctx context.Context, req *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) { + return c.getAccountConfig.CallUnary(ctx, req) +} + +// UpdateAccountConfig calls config.ConfigurationService.UpdateAccountConfig. +func (c *configurationServiceClient) UpdateAccountConfig(ctx context.Context, req *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) { + return c.updateAccountConfig.CallUnary(ctx, req) +} + +// CreateHcpConfig calls config.ConfigurationService.CreateHcpConfig. +func (c *configurationServiceClient) CreateHcpConfig(ctx context.Context, req *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) { + return c.createHcpConfig.CallUnary(ctx, req) +} + +// GetHcpConfig calls config.ConfigurationService.GetHcpConfig. +func (c *configurationServiceClient) GetHcpConfig(ctx context.Context, req *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) { + return c.getHcpConfig.CallUnary(ctx, req) +} + +// DeleteHcpConfig calls config.ConfigurationService.DeleteHcpConfig. +func (c *configurationServiceClient) DeleteHcpConfig(ctx context.Context, req *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) { + return c.deleteHcpConfig.CallUnary(ctx, req) +} + +// ReplaceHcpApiKey calls config.ConfigurationService.ReplaceHcpApiKey. +func (c *configurationServiceClient) ReplaceHcpApiKey(ctx context.Context, req *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) { + return c.replaceHcpApiKey.CallUnary(ctx, req) +} + +// GetSignalConfig calls config.ConfigurationService.GetSignalConfig. +func (c *configurationServiceClient) GetSignalConfig(ctx context.Context, req *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { + return c.getSignalConfig.CallUnary(ctx, req) +} + +// UpdateSignalConfig calls config.ConfigurationService.UpdateSignalConfig. +func (c *configurationServiceClient) UpdateSignalConfig(ctx context.Context, req *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) { + return c.updateSignalConfig.CallUnary(ctx, req) +} + +// GetGithubAppInformation calls config.ConfigurationService.GetGithubAppInformation. +func (c *configurationServiceClient) GetGithubAppInformation(ctx context.Context, req *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) { + return c.getGithubAppInformation.CallUnary(ctx, req) +} + +// RegenerateGithubAppProfile calls config.ConfigurationService.RegenerateGithubAppProfile. +func (c *configurationServiceClient) RegenerateGithubAppProfile(ctx context.Context, req *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) { + return c.regenerateGithubAppProfile.CallUnary(ctx, req) +} + +// DeleteGithubAppProfileAndGithubInstallationID calls +// config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID. +func (c *configurationServiceClient) DeleteGithubAppProfileAndGithubInstallationID(ctx context.Context, req *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) { + return c.deleteGithubAppProfileAndGithubInstallationID.CallUnary(ctx, req) +} + +// ConfigurationServiceHandler is an implementation of the config.ConfigurationService service. +type ConfigurationServiceHandler interface { + // Get the account config for the user's account + GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) + // Update the account config for the user's account + UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) + // Create a new HCP Terraform config for the user's account. This follows + // the same flow as CreateAPIKey, to create a new API key that is then used + // for the HCP Terraform endpoint URL. + CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) + // Get the existing HCP Terraform config for the user's account. + GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) + // Remove the existing HCP Terraform config from the user's account. + DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) + // Replace the API key backing the HCP Terraform integration with a fresh + // one. The old API key is revoked. The endpoint URL and HMAC secret are + // preserved. Follows the same OAuth flow as CreateHcpConfig. + ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) + // Get the signal config for the account + GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) + // Update the signal config for the account + UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) + // Github app + // we will be displaying app installation information for this account on the github integrations page + GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) + // regenerate the github app profile, this information is used for signal processing + RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) + // remove the github app installation id and github organisation profile from the signal config + // this will not uninstall the app from github, that must be done manually by the user + DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) +} + +// NewConfigurationServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewConfigurationServiceHandler(svc ConfigurationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + configurationServiceMethods := sdp_go.File_config_proto.Services().ByName("ConfigurationService").Methods() + configurationServiceGetAccountConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceGetAccountConfigProcedure, + svc.GetAccountConfig, + connect.WithSchema(configurationServiceMethods.ByName("GetAccountConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceUpdateAccountConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceUpdateAccountConfigProcedure, + svc.UpdateAccountConfig, + connect.WithSchema(configurationServiceMethods.ByName("UpdateAccountConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceCreateHcpConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceCreateHcpConfigProcedure, + svc.CreateHcpConfig, + connect.WithSchema(configurationServiceMethods.ByName("CreateHcpConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceGetHcpConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceGetHcpConfigProcedure, + svc.GetHcpConfig, + connect.WithSchema(configurationServiceMethods.ByName("GetHcpConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceDeleteHcpConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceDeleteHcpConfigProcedure, + svc.DeleteHcpConfig, + connect.WithSchema(configurationServiceMethods.ByName("DeleteHcpConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceReplaceHcpApiKeyHandler := connect.NewUnaryHandler( + ConfigurationServiceReplaceHcpApiKeyProcedure, + svc.ReplaceHcpApiKey, + connect.WithSchema(configurationServiceMethods.ByName("ReplaceHcpApiKey")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceGetSignalConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceGetSignalConfigProcedure, + svc.GetSignalConfig, + connect.WithSchema(configurationServiceMethods.ByName("GetSignalConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceUpdateSignalConfigHandler := connect.NewUnaryHandler( + ConfigurationServiceUpdateSignalConfigProcedure, + svc.UpdateSignalConfig, + connect.WithSchema(configurationServiceMethods.ByName("UpdateSignalConfig")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceGetGithubAppInformationHandler := connect.NewUnaryHandler( + ConfigurationServiceGetGithubAppInformationProcedure, + svc.GetGithubAppInformation, + connect.WithSchema(configurationServiceMethods.ByName("GetGithubAppInformation")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceRegenerateGithubAppProfileHandler := connect.NewUnaryHandler( + ConfigurationServiceRegenerateGithubAppProfileProcedure, + svc.RegenerateGithubAppProfile, + connect.WithSchema(configurationServiceMethods.ByName("RegenerateGithubAppProfile")), + connect.WithHandlerOptions(opts...), + ) + configurationServiceDeleteGithubAppProfileAndGithubInstallationIDHandler := connect.NewUnaryHandler( + ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure, + svc.DeleteGithubAppProfileAndGithubInstallationID, + connect.WithSchema(configurationServiceMethods.ByName("DeleteGithubAppProfileAndGithubInstallationID")), + connect.WithHandlerOptions(opts...), + ) + return "/config.ConfigurationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ConfigurationServiceGetAccountConfigProcedure: + configurationServiceGetAccountConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceUpdateAccountConfigProcedure: + configurationServiceUpdateAccountConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceCreateHcpConfigProcedure: + configurationServiceCreateHcpConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceGetHcpConfigProcedure: + configurationServiceGetHcpConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceDeleteHcpConfigProcedure: + configurationServiceDeleteHcpConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceReplaceHcpApiKeyProcedure: + configurationServiceReplaceHcpApiKeyHandler.ServeHTTP(w, r) + case ConfigurationServiceGetSignalConfigProcedure: + configurationServiceGetSignalConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceUpdateSignalConfigProcedure: + configurationServiceUpdateSignalConfigHandler.ServeHTTP(w, r) + case ConfigurationServiceGetGithubAppInformationProcedure: + configurationServiceGetGithubAppInformationHandler.ServeHTTP(w, r) + case ConfigurationServiceRegenerateGithubAppProfileProcedure: + configurationServiceRegenerateGithubAppProfileHandler.ServeHTTP(w, r) + case ConfigurationServiceDeleteGithubAppProfileAndGithubInstallationIDProcedure: + configurationServiceDeleteGithubAppProfileAndGithubInstallationIDHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedConfigurationServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedConfigurationServiceHandler struct{} + +func (UnimplementedConfigurationServiceHandler) GetAccountConfig(context.Context, *connect.Request[sdp_go.GetAccountConfigRequest]) (*connect.Response[sdp_go.GetAccountConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetAccountConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) UpdateAccountConfig(context.Context, *connect.Request[sdp_go.UpdateAccountConfigRequest]) (*connect.Response[sdp_go.UpdateAccountConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.UpdateAccountConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) CreateHcpConfig(context.Context, *connect.Request[sdp_go.CreateHcpConfigRequest]) (*connect.Response[sdp_go.CreateHcpConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.CreateHcpConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) GetHcpConfig(context.Context, *connect.Request[sdp_go.GetHcpConfigRequest]) (*connect.Response[sdp_go.GetHcpConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetHcpConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) DeleteHcpConfig(context.Context, *connect.Request[sdp_go.DeleteHcpConfigRequest]) (*connect.Response[sdp_go.DeleteHcpConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.DeleteHcpConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) ReplaceHcpApiKey(context.Context, *connect.Request[sdp_go.ReplaceHcpApiKeyRequest]) (*connect.Response[sdp_go.ReplaceHcpApiKeyResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.ReplaceHcpApiKey is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) GetSignalConfig(context.Context, *connect.Request[sdp_go.GetSignalConfigRequest]) (*connect.Response[sdp_go.GetSignalConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetSignalConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) UpdateSignalConfig(context.Context, *connect.Request[sdp_go.UpdateSignalConfigRequest]) (*connect.Response[sdp_go.UpdateSignalConfigResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.UpdateSignalConfig is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) GetGithubAppInformation(context.Context, *connect.Request[sdp_go.GetGithubAppInformationRequest]) (*connect.Response[sdp_go.GetGithubAppInformationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.GetGithubAppInformation is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) RegenerateGithubAppProfile(context.Context, *connect.Request[sdp_go.RegenerateGithubAppProfileRequest]) (*connect.Response[sdp_go.RegenerateGithubAppProfileResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.RegenerateGithubAppProfile is not implemented")) +} + +func (UnimplementedConfigurationServiceHandler) DeleteGithubAppProfileAndGithubInstallationID(context.Context, *connect.Request[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDRequest]) (*connect.Response[sdp_go.DeleteGithubAppProfileAndGithubInstallationIDResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("config.ConfigurationService.DeleteGithubAppProfileAndGithubInstallationID is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/invites.connect.go b/go/sdp-go/sdpconnect/invites.connect.go new file mode 100644 index 00000000..7830e156 --- /dev/null +++ b/go/sdp-go/sdpconnect/invites.connect.go @@ -0,0 +1,196 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: invites.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // InviteServiceName is the fully-qualified name of the InviteService service. + InviteServiceName = "invites.InviteService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // InviteServiceCreateInviteProcedure is the fully-qualified name of the InviteService's + // CreateInvite RPC. + InviteServiceCreateInviteProcedure = "/invites.InviteService/CreateInvite" + // InviteServiceListInvitesProcedure is the fully-qualified name of the InviteService's ListInvites + // RPC. + InviteServiceListInvitesProcedure = "/invites.InviteService/ListInvites" + // InviteServiceRevokeInviteProcedure is the fully-qualified name of the InviteService's + // RevokeInvite RPC. + InviteServiceRevokeInviteProcedure = "/invites.InviteService/RevokeInvite" + // InviteServiceResendInviteProcedure is the fully-qualified name of the InviteService's + // ResendInvite RPC. + InviteServiceResendInviteProcedure = "/invites.InviteService/ResendInvite" +) + +// InviteServiceClient is a client for the invites.InviteService service. +type InviteServiceClient interface { + CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) + ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) + RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) + ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) +} + +// NewInviteServiceClient constructs a client for the invites.InviteService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewInviteServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) InviteServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + inviteServiceMethods := sdp_go.File_invites_proto.Services().ByName("InviteService").Methods() + return &inviteServiceClient{ + createInvite: connect.NewClient[sdp_go.CreateInviteRequest, sdp_go.CreateInviteResponse]( + httpClient, + baseURL+InviteServiceCreateInviteProcedure, + connect.WithSchema(inviteServiceMethods.ByName("CreateInvite")), + connect.WithClientOptions(opts...), + ), + listInvites: connect.NewClient[sdp_go.ListInvitesRequest, sdp_go.ListInvitesResponse]( + httpClient, + baseURL+InviteServiceListInvitesProcedure, + connect.WithSchema(inviteServiceMethods.ByName("ListInvites")), + connect.WithClientOptions(opts...), + ), + revokeInvite: connect.NewClient[sdp_go.RevokeInviteRequest, sdp_go.RevokeInviteResponse]( + httpClient, + baseURL+InviteServiceRevokeInviteProcedure, + connect.WithSchema(inviteServiceMethods.ByName("RevokeInvite")), + connect.WithClientOptions(opts...), + ), + resendInvite: connect.NewClient[sdp_go.ResendInviteRequest, sdp_go.ResendInviteResponse]( + httpClient, + baseURL+InviteServiceResendInviteProcedure, + connect.WithSchema(inviteServiceMethods.ByName("ResendInvite")), + connect.WithClientOptions(opts...), + ), + } +} + +// inviteServiceClient implements InviteServiceClient. +type inviteServiceClient struct { + createInvite *connect.Client[sdp_go.CreateInviteRequest, sdp_go.CreateInviteResponse] + listInvites *connect.Client[sdp_go.ListInvitesRequest, sdp_go.ListInvitesResponse] + revokeInvite *connect.Client[sdp_go.RevokeInviteRequest, sdp_go.RevokeInviteResponse] + resendInvite *connect.Client[sdp_go.ResendInviteRequest, sdp_go.ResendInviteResponse] +} + +// CreateInvite calls invites.InviteService.CreateInvite. +func (c *inviteServiceClient) CreateInvite(ctx context.Context, req *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) { + return c.createInvite.CallUnary(ctx, req) +} + +// ListInvites calls invites.InviteService.ListInvites. +func (c *inviteServiceClient) ListInvites(ctx context.Context, req *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) { + return c.listInvites.CallUnary(ctx, req) +} + +// RevokeInvite calls invites.InviteService.RevokeInvite. +func (c *inviteServiceClient) RevokeInvite(ctx context.Context, req *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) { + return c.revokeInvite.CallUnary(ctx, req) +} + +// ResendInvite calls invites.InviteService.ResendInvite. +func (c *inviteServiceClient) ResendInvite(ctx context.Context, req *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) { + return c.resendInvite.CallUnary(ctx, req) +} + +// InviteServiceHandler is an implementation of the invites.InviteService service. +type InviteServiceHandler interface { + CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) + ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) + RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) + ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) +} + +// NewInviteServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewInviteServiceHandler(svc InviteServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + inviteServiceMethods := sdp_go.File_invites_proto.Services().ByName("InviteService").Methods() + inviteServiceCreateInviteHandler := connect.NewUnaryHandler( + InviteServiceCreateInviteProcedure, + svc.CreateInvite, + connect.WithSchema(inviteServiceMethods.ByName("CreateInvite")), + connect.WithHandlerOptions(opts...), + ) + inviteServiceListInvitesHandler := connect.NewUnaryHandler( + InviteServiceListInvitesProcedure, + svc.ListInvites, + connect.WithSchema(inviteServiceMethods.ByName("ListInvites")), + connect.WithHandlerOptions(opts...), + ) + inviteServiceRevokeInviteHandler := connect.NewUnaryHandler( + InviteServiceRevokeInviteProcedure, + svc.RevokeInvite, + connect.WithSchema(inviteServiceMethods.ByName("RevokeInvite")), + connect.WithHandlerOptions(opts...), + ) + inviteServiceResendInviteHandler := connect.NewUnaryHandler( + InviteServiceResendInviteProcedure, + svc.ResendInvite, + connect.WithSchema(inviteServiceMethods.ByName("ResendInvite")), + connect.WithHandlerOptions(opts...), + ) + return "/invites.InviteService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case InviteServiceCreateInviteProcedure: + inviteServiceCreateInviteHandler.ServeHTTP(w, r) + case InviteServiceListInvitesProcedure: + inviteServiceListInvitesHandler.ServeHTTP(w, r) + case InviteServiceRevokeInviteProcedure: + inviteServiceRevokeInviteHandler.ServeHTTP(w, r) + case InviteServiceResendInviteProcedure: + inviteServiceResendInviteHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedInviteServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedInviteServiceHandler struct{} + +func (UnimplementedInviteServiceHandler) CreateInvite(context.Context, *connect.Request[sdp_go.CreateInviteRequest]) (*connect.Response[sdp_go.CreateInviteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.CreateInvite is not implemented")) +} + +func (UnimplementedInviteServiceHandler) ListInvites(context.Context, *connect.Request[sdp_go.ListInvitesRequest]) (*connect.Response[sdp_go.ListInvitesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.ListInvites is not implemented")) +} + +func (UnimplementedInviteServiceHandler) RevokeInvite(context.Context, *connect.Request[sdp_go.RevokeInviteRequest]) (*connect.Response[sdp_go.RevokeInviteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.RevokeInvite is not implemented")) +} + +func (UnimplementedInviteServiceHandler) ResendInvite(context.Context, *connect.Request[sdp_go.ResendInviteRequest]) (*connect.Response[sdp_go.ResendInviteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("invites.InviteService.ResendInvite is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/logs.connect.go b/go/sdp-go/sdpconnect/logs.connect.go new file mode 100644 index 00000000..daf1c535 --- /dev/null +++ b/go/sdp-go/sdpconnect/logs.connect.go @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: logs.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // LogsServiceName is the fully-qualified name of the LogsService service. + LogsServiceName = "logs.LogsService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // LogsServiceGetLogRecordsProcedure is the fully-qualified name of the LogsService's GetLogRecords + // RPC. + LogsServiceGetLogRecordsProcedure = "/logs.LogsService/GetLogRecords" +) + +// LogsServiceClient is a client for the logs.LogsService service. +type LogsServiceClient interface { + // GetLogRecords returns a stream of log records from the upstream API. The + // source is expected to use sane defaults within the limits of the + // underlying API and SDP capabilities (message size, etc). Each chunk is + // roughly a page of the upstream APIs pagination. + GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest]) (*connect.ServerStreamForClient[sdp_go.GetLogRecordsResponse], error) +} + +// NewLogsServiceClient constructs a client for the logs.LogsService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewLogsServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) LogsServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + logsServiceMethods := sdp_go.File_logs_proto.Services().ByName("LogsService").Methods() + return &logsServiceClient{ + getLogRecords: connect.NewClient[sdp_go.GetLogRecordsRequest, sdp_go.GetLogRecordsResponse]( + httpClient, + baseURL+LogsServiceGetLogRecordsProcedure, + connect.WithSchema(logsServiceMethods.ByName("GetLogRecords")), + connect.WithClientOptions(opts...), + ), + } +} + +// logsServiceClient implements LogsServiceClient. +type logsServiceClient struct { + getLogRecords *connect.Client[sdp_go.GetLogRecordsRequest, sdp_go.GetLogRecordsResponse] +} + +// GetLogRecords calls logs.LogsService.GetLogRecords. +func (c *logsServiceClient) GetLogRecords(ctx context.Context, req *connect.Request[sdp_go.GetLogRecordsRequest]) (*connect.ServerStreamForClient[sdp_go.GetLogRecordsResponse], error) { + return c.getLogRecords.CallServerStream(ctx, req) +} + +// LogsServiceHandler is an implementation of the logs.LogsService service. +type LogsServiceHandler interface { + // GetLogRecords returns a stream of log records from the upstream API. The + // source is expected to use sane defaults within the limits of the + // underlying API and SDP capabilities (message size, etc). Each chunk is + // roughly a page of the upstream APIs pagination. + GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest], *connect.ServerStream[sdp_go.GetLogRecordsResponse]) error +} + +// NewLogsServiceHandler builds an HTTP handler from the service implementation. It returns the path +// on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewLogsServiceHandler(svc LogsServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + logsServiceMethods := sdp_go.File_logs_proto.Services().ByName("LogsService").Methods() + logsServiceGetLogRecordsHandler := connect.NewServerStreamHandler( + LogsServiceGetLogRecordsProcedure, + svc.GetLogRecords, + connect.WithSchema(logsServiceMethods.ByName("GetLogRecords")), + connect.WithHandlerOptions(opts...), + ) + return "/logs.LogsService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case LogsServiceGetLogRecordsProcedure: + logsServiceGetLogRecordsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedLogsServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedLogsServiceHandler struct{} + +func (UnimplementedLogsServiceHandler) GetLogRecords(context.Context, *connect.Request[sdp_go.GetLogRecordsRequest], *connect.ServerStream[sdp_go.GetLogRecordsResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("logs.LogsService.GetLogRecords is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/revlink.connect.go b/go/sdp-go/sdpconnect/revlink.connect.go new file mode 100644 index 00000000..7d706d9b --- /dev/null +++ b/go/sdp-go/sdpconnect/revlink.connect.go @@ -0,0 +1,189 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: revlink.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // RevlinkServiceName is the fully-qualified name of the RevlinkService service. + RevlinkServiceName = "revlink.RevlinkService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // RevlinkServiceGetReverseEdgesProcedure is the fully-qualified name of the RevlinkService's + // GetReverseEdges RPC. + RevlinkServiceGetReverseEdgesProcedure = "/revlink.RevlinkService/GetReverseEdges" + // RevlinkServiceIngestGatewayResponsesProcedure is the fully-qualified name of the RevlinkService's + // IngestGatewayResponses RPC. + RevlinkServiceIngestGatewayResponsesProcedure = "/revlink.RevlinkService/IngestGatewayResponses" + // RevlinkServiceCheckpointProcedure is the fully-qualified name of the RevlinkService's Checkpoint + // RPC. + RevlinkServiceCheckpointProcedure = "/revlink.RevlinkService/Checkpoint" +) + +// RevlinkServiceClient is a client for the revlink.RevlinkService service. +type RevlinkServiceClient interface { + // Gets reverse edges for a given item + GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) + // Ingests a stream of gateway responses + IngestGatewayResponses(context.Context) *connect.ClientStreamForClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] + // Waits until all currently submitted gateway responses are committed to + // the database. This is primarily intended for tests to ensure that setup + // was completed. + // + // Note that this does only count the first try of each insertion; retries + // are not considered. + // + // Note2 that this is implemented in memory, so there is no guarantee + // that this will work in a distributed environment. + Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) +} + +// NewRevlinkServiceClient constructs a client for the revlink.RevlinkService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewRevlinkServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RevlinkServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + revlinkServiceMethods := sdp_go.File_revlink_proto.Services().ByName("RevlinkService").Methods() + return &revlinkServiceClient{ + getReverseEdges: connect.NewClient[sdp_go.GetReverseEdgesRequest, sdp_go.GetReverseEdgesResponse]( + httpClient, + baseURL+RevlinkServiceGetReverseEdgesProcedure, + connect.WithSchema(revlinkServiceMethods.ByName("GetReverseEdges")), + connect.WithClientOptions(opts...), + ), + ingestGatewayResponses: connect.NewClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse]( + httpClient, + baseURL+RevlinkServiceIngestGatewayResponsesProcedure, + connect.WithSchema(revlinkServiceMethods.ByName("IngestGatewayResponses")), + connect.WithClientOptions(opts...), + ), + checkpoint: connect.NewClient[sdp_go.CheckpointRequest, sdp_go.CheckpointResponse]( + httpClient, + baseURL+RevlinkServiceCheckpointProcedure, + connect.WithSchema(revlinkServiceMethods.ByName("Checkpoint")), + connect.WithClientOptions(opts...), + ), + } +} + +// revlinkServiceClient implements RevlinkServiceClient. +type revlinkServiceClient struct { + getReverseEdges *connect.Client[sdp_go.GetReverseEdgesRequest, sdp_go.GetReverseEdgesResponse] + ingestGatewayResponses *connect.Client[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] + checkpoint *connect.Client[sdp_go.CheckpointRequest, sdp_go.CheckpointResponse] +} + +// GetReverseEdges calls revlink.RevlinkService.GetReverseEdges. +func (c *revlinkServiceClient) GetReverseEdges(ctx context.Context, req *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) { + return c.getReverseEdges.CallUnary(ctx, req) +} + +// IngestGatewayResponses calls revlink.RevlinkService.IngestGatewayResponses. +func (c *revlinkServiceClient) IngestGatewayResponses(ctx context.Context) *connect.ClientStreamForClient[sdp_go.IngestGatewayResponseRequest, sdp_go.IngestGatewayResponsesResponse] { + return c.ingestGatewayResponses.CallClientStream(ctx) +} + +// Checkpoint calls revlink.RevlinkService.Checkpoint. +func (c *revlinkServiceClient) Checkpoint(ctx context.Context, req *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) { + return c.checkpoint.CallUnary(ctx, req) +} + +// RevlinkServiceHandler is an implementation of the revlink.RevlinkService service. +type RevlinkServiceHandler interface { + // Gets reverse edges for a given item + GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) + // Ingests a stream of gateway responses + IngestGatewayResponses(context.Context, *connect.ClientStream[sdp_go.IngestGatewayResponseRequest]) (*connect.Response[sdp_go.IngestGatewayResponsesResponse], error) + // Waits until all currently submitted gateway responses are committed to + // the database. This is primarily intended for tests to ensure that setup + // was completed. + // + // Note that this does only count the first try of each insertion; retries + // are not considered. + // + // Note2 that this is implemented in memory, so there is no guarantee + // that this will work in a distributed environment. + Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) +} + +// NewRevlinkServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewRevlinkServiceHandler(svc RevlinkServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + revlinkServiceMethods := sdp_go.File_revlink_proto.Services().ByName("RevlinkService").Methods() + revlinkServiceGetReverseEdgesHandler := connect.NewUnaryHandler( + RevlinkServiceGetReverseEdgesProcedure, + svc.GetReverseEdges, + connect.WithSchema(revlinkServiceMethods.ByName("GetReverseEdges")), + connect.WithHandlerOptions(opts...), + ) + revlinkServiceIngestGatewayResponsesHandler := connect.NewClientStreamHandler( + RevlinkServiceIngestGatewayResponsesProcedure, + svc.IngestGatewayResponses, + connect.WithSchema(revlinkServiceMethods.ByName("IngestGatewayResponses")), + connect.WithHandlerOptions(opts...), + ) + revlinkServiceCheckpointHandler := connect.NewUnaryHandler( + RevlinkServiceCheckpointProcedure, + svc.Checkpoint, + connect.WithSchema(revlinkServiceMethods.ByName("Checkpoint")), + connect.WithHandlerOptions(opts...), + ) + return "/revlink.RevlinkService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case RevlinkServiceGetReverseEdgesProcedure: + revlinkServiceGetReverseEdgesHandler.ServeHTTP(w, r) + case RevlinkServiceIngestGatewayResponsesProcedure: + revlinkServiceIngestGatewayResponsesHandler.ServeHTTP(w, r) + case RevlinkServiceCheckpointProcedure: + revlinkServiceCheckpointHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedRevlinkServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedRevlinkServiceHandler struct{} + +func (UnimplementedRevlinkServiceHandler) GetReverseEdges(context.Context, *connect.Request[sdp_go.GetReverseEdgesRequest]) (*connect.Response[sdp_go.GetReverseEdgesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.GetReverseEdges is not implemented")) +} + +func (UnimplementedRevlinkServiceHandler) IngestGatewayResponses(context.Context, *connect.ClientStream[sdp_go.IngestGatewayResponseRequest]) (*connect.Response[sdp_go.IngestGatewayResponsesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.IngestGatewayResponses is not implemented")) +} + +func (UnimplementedRevlinkServiceHandler) Checkpoint(context.Context, *connect.Request[sdp_go.CheckpointRequest]) (*connect.Response[sdp_go.CheckpointResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("revlink.RevlinkService.Checkpoint is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/signal.connect.go b/go/sdp-go/sdpconnect/signal.connect.go new file mode 100644 index 00000000..8f4ee7c5 --- /dev/null +++ b/go/sdp-go/sdpconnect/signal.connect.go @@ -0,0 +1,330 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: signal.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // SignalServiceName is the fully-qualified name of the SignalService service. + SignalServiceName = "signal.SignalService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // SignalServiceAddSignalProcedure is the fully-qualified name of the SignalService's AddSignal RPC. + SignalServiceAddSignalProcedure = "/signal.SignalService/AddSignal" + // SignalServiceGetSignalsByChangeExternalIDProcedure is the fully-qualified name of the + // SignalService's GetSignalsByChangeExternalID RPC. + SignalServiceGetSignalsByChangeExternalIDProcedure = "/signal.SignalService/GetSignalsByChangeExternalID" + // SignalServiceGetChangeOverviewSignalsProcedure is the fully-qualified name of the SignalService's + // GetChangeOverviewSignals RPC. + SignalServiceGetChangeOverviewSignalsProcedure = "/signal.SignalService/GetChangeOverviewSignals" + // SignalServiceGetItemSignalsProcedure is the fully-qualified name of the SignalService's + // GetItemSignals RPC. + SignalServiceGetItemSignalsProcedure = "/signal.SignalService/GetItemSignals" + // SignalServiceGetItemSignalsV2Procedure is the fully-qualified name of the SignalService's + // GetItemSignalsV2 RPC. + SignalServiceGetItemSignalsV2Procedure = "/signal.SignalService/GetItemSignalsV2" + // SignalServiceGetCustomSignalsByCategoryProcedure is the fully-qualified name of the + // SignalService's GetCustomSignalsByCategory RPC. + SignalServiceGetCustomSignalsByCategoryProcedure = "/signal.SignalService/GetCustomSignalsByCategory" + // SignalServiceGetItemSignalDetailsProcedure is the fully-qualified name of the SignalService's + // GetItemSignalDetails RPC. + SignalServiceGetItemSignalDetailsProcedure = "/signal.SignalService/GetItemSignalDetails" +) + +// SignalServiceClient is a client for the signal.SignalService service. +type SignalServiceClient interface { + // This is an external API to add a signals to a change. + // It will be used by the CLI, the web UI, and other clients. + // It expects the user to provide the properties of the signal, such as name, value, description, and category. + // And it returns the signal that was added, including the machine-generated metadata such as UUID and aggregation ID. + // DOES THIS NEED TO BE UPDATED TO SPECIFY THE LEVEL OF THE SIGNAL? + AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) + // This is an API to get all signals associated with a change by its external ID. It is not used by the frontend. + // It returns a slice/array of Signals. Which includes both the user-facing properties of the signal, and the machine-generated metadata. + // Look at the Signal message for more details. + GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) + // NB for the following we do not need to specify the icons for the Items, as they are derived by the Frontend separately. + // Get all top-level signals for a change. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) + // Get item-level signals for all items in a change. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) + // Get a slice of items, sorted by their aggregate signal value, ascending. + // for each item include + // - an aggregated value for the item, calculated by AggregateSignalScores. + // - a friendly item ref, also before and after + // - a slice of signals for the item, sorted by the signal value, ascending. + // - the status of the item, e.g. "added", "modified", "deleted". + GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) + // Get all custom signals for a change by its external ID and category. They are NOT associated with any item. + // There is no score aggregation or description aggregation at this level. They are aggregated at the GetChangeOverviewSignals level. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) + // Get all signals for attributes/modifications of an item. This will only be used for routineness to start with. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) +} + +// NewSignalServiceClient constructs a client for the signal.SignalService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewSignalServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SignalServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + signalServiceMethods := sdp_go.File_signal_proto.Services().ByName("SignalService").Methods() + return &signalServiceClient{ + addSignal: connect.NewClient[sdp_go.AddSignalRequest, sdp_go.AddSignalResponse]( + httpClient, + baseURL+SignalServiceAddSignalProcedure, + connect.WithSchema(signalServiceMethods.ByName("AddSignal")), + connect.WithClientOptions(opts...), + ), + getSignalsByChangeExternalID: connect.NewClient[sdp_go.GetSignalsByChangeExternalIDRequest, sdp_go.GetSignalsByChangeExternalIDResponse]( + httpClient, + baseURL+SignalServiceGetSignalsByChangeExternalIDProcedure, + connect.WithSchema(signalServiceMethods.ByName("GetSignalsByChangeExternalID")), + connect.WithClientOptions(opts...), + ), + getChangeOverviewSignals: connect.NewClient[sdp_go.GetChangeOverviewSignalsRequest, sdp_go.GetChangeOverviewSignalsResponse]( + httpClient, + baseURL+SignalServiceGetChangeOverviewSignalsProcedure, + connect.WithSchema(signalServiceMethods.ByName("GetChangeOverviewSignals")), + connect.WithClientOptions(opts...), + ), + getItemSignals: connect.NewClient[sdp_go.GetItemSignalsRequest, sdp_go.GetItemSignalsResponse]( + httpClient, + baseURL+SignalServiceGetItemSignalsProcedure, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignals")), + connect.WithClientOptions(opts...), + ), + getItemSignalsV2: connect.NewClient[sdp_go.GetItemSignalsRequestV2, sdp_go.GetItemSignalsResponseV2]( + httpClient, + baseURL+SignalServiceGetItemSignalsV2Procedure, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignalsV2")), + connect.WithClientOptions(opts...), + ), + getCustomSignalsByCategory: connect.NewClient[sdp_go.GetCustomSignalsByCategoryRequest, sdp_go.GetCustomSignalsByCategoryResponse]( + httpClient, + baseURL+SignalServiceGetCustomSignalsByCategoryProcedure, + connect.WithSchema(signalServiceMethods.ByName("GetCustomSignalsByCategory")), + connect.WithClientOptions(opts...), + ), + getItemSignalDetails: connect.NewClient[sdp_go.GetItemSignalDetailsRequest, sdp_go.GetItemSignalDetailsResponse]( + httpClient, + baseURL+SignalServiceGetItemSignalDetailsProcedure, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignalDetails")), + connect.WithClientOptions(opts...), + ), + } +} + +// signalServiceClient implements SignalServiceClient. +type signalServiceClient struct { + addSignal *connect.Client[sdp_go.AddSignalRequest, sdp_go.AddSignalResponse] + getSignalsByChangeExternalID *connect.Client[sdp_go.GetSignalsByChangeExternalIDRequest, sdp_go.GetSignalsByChangeExternalIDResponse] + getChangeOverviewSignals *connect.Client[sdp_go.GetChangeOverviewSignalsRequest, sdp_go.GetChangeOverviewSignalsResponse] + getItemSignals *connect.Client[sdp_go.GetItemSignalsRequest, sdp_go.GetItemSignalsResponse] + getItemSignalsV2 *connect.Client[sdp_go.GetItemSignalsRequestV2, sdp_go.GetItemSignalsResponseV2] + getCustomSignalsByCategory *connect.Client[sdp_go.GetCustomSignalsByCategoryRequest, sdp_go.GetCustomSignalsByCategoryResponse] + getItemSignalDetails *connect.Client[sdp_go.GetItemSignalDetailsRequest, sdp_go.GetItemSignalDetailsResponse] +} + +// AddSignal calls signal.SignalService.AddSignal. +func (c *signalServiceClient) AddSignal(ctx context.Context, req *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) { + return c.addSignal.CallUnary(ctx, req) +} + +// GetSignalsByChangeExternalID calls signal.SignalService.GetSignalsByChangeExternalID. +func (c *signalServiceClient) GetSignalsByChangeExternalID(ctx context.Context, req *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) { + return c.getSignalsByChangeExternalID.CallUnary(ctx, req) +} + +// GetChangeOverviewSignals calls signal.SignalService.GetChangeOverviewSignals. +func (c *signalServiceClient) GetChangeOverviewSignals(ctx context.Context, req *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) { + return c.getChangeOverviewSignals.CallUnary(ctx, req) +} + +// GetItemSignals calls signal.SignalService.GetItemSignals. +func (c *signalServiceClient) GetItemSignals(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) { + return c.getItemSignals.CallUnary(ctx, req) +} + +// GetItemSignalsV2 calls signal.SignalService.GetItemSignalsV2. +func (c *signalServiceClient) GetItemSignalsV2(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) { + return c.getItemSignalsV2.CallUnary(ctx, req) +} + +// GetCustomSignalsByCategory calls signal.SignalService.GetCustomSignalsByCategory. +func (c *signalServiceClient) GetCustomSignalsByCategory(ctx context.Context, req *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) { + return c.getCustomSignalsByCategory.CallUnary(ctx, req) +} + +// GetItemSignalDetails calls signal.SignalService.GetItemSignalDetails. +func (c *signalServiceClient) GetItemSignalDetails(ctx context.Context, req *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) { + return c.getItemSignalDetails.CallUnary(ctx, req) +} + +// SignalServiceHandler is an implementation of the signal.SignalService service. +type SignalServiceHandler interface { + // This is an external API to add a signals to a change. + // It will be used by the CLI, the web UI, and other clients. + // It expects the user to provide the properties of the signal, such as name, value, description, and category. + // And it returns the signal that was added, including the machine-generated metadata such as UUID and aggregation ID. + // DOES THIS NEED TO BE UPDATED TO SPECIFY THE LEVEL OF THE SIGNAL? + AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) + // This is an API to get all signals associated with a change by its external ID. It is not used by the frontend. + // It returns a slice/array of Signals. Which includes both the user-facing properties of the signal, and the machine-generated metadata. + // Look at the Signal message for more details. + GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) + // NB for the following we do not need to specify the icons for the Items, as they are derived by the Frontend separately. + // Get all top-level signals for a change. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) + // Get item-level signals for all items in a change. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) + // Get a slice of items, sorted by their aggregate signal value, ascending. + // for each item include + // - an aggregated value for the item, calculated by AggregateSignalScores. + // - a friendly item ref, also before and after + // - a slice of signals for the item, sorted by the signal value, ascending. + // - the status of the item, e.g. "added", "modified", "deleted". + GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) + // Get all custom signals for a change by its external ID and category. They are NOT associated with any item. + // There is no score aggregation or description aggregation at this level. They are aggregated at the GetChangeOverviewSignals level. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) + // Get all signals for attributes/modifications of an item. This will only be used for routineness to start with. + // They are sorted by the signal value, ascending. From minus 5 to plus 5. + GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) +} + +// NewSignalServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewSignalServiceHandler(svc SignalServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + signalServiceMethods := sdp_go.File_signal_proto.Services().ByName("SignalService").Methods() + signalServiceAddSignalHandler := connect.NewUnaryHandler( + SignalServiceAddSignalProcedure, + svc.AddSignal, + connect.WithSchema(signalServiceMethods.ByName("AddSignal")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetSignalsByChangeExternalIDHandler := connect.NewUnaryHandler( + SignalServiceGetSignalsByChangeExternalIDProcedure, + svc.GetSignalsByChangeExternalID, + connect.WithSchema(signalServiceMethods.ByName("GetSignalsByChangeExternalID")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetChangeOverviewSignalsHandler := connect.NewUnaryHandler( + SignalServiceGetChangeOverviewSignalsProcedure, + svc.GetChangeOverviewSignals, + connect.WithSchema(signalServiceMethods.ByName("GetChangeOverviewSignals")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetItemSignalsHandler := connect.NewUnaryHandler( + SignalServiceGetItemSignalsProcedure, + svc.GetItemSignals, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignals")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetItemSignalsV2Handler := connect.NewUnaryHandler( + SignalServiceGetItemSignalsV2Procedure, + svc.GetItemSignalsV2, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignalsV2")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetCustomSignalsByCategoryHandler := connect.NewUnaryHandler( + SignalServiceGetCustomSignalsByCategoryProcedure, + svc.GetCustomSignalsByCategory, + connect.WithSchema(signalServiceMethods.ByName("GetCustomSignalsByCategory")), + connect.WithHandlerOptions(opts...), + ) + signalServiceGetItemSignalDetailsHandler := connect.NewUnaryHandler( + SignalServiceGetItemSignalDetailsProcedure, + svc.GetItemSignalDetails, + connect.WithSchema(signalServiceMethods.ByName("GetItemSignalDetails")), + connect.WithHandlerOptions(opts...), + ) + return "/signal.SignalService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SignalServiceAddSignalProcedure: + signalServiceAddSignalHandler.ServeHTTP(w, r) + case SignalServiceGetSignalsByChangeExternalIDProcedure: + signalServiceGetSignalsByChangeExternalIDHandler.ServeHTTP(w, r) + case SignalServiceGetChangeOverviewSignalsProcedure: + signalServiceGetChangeOverviewSignalsHandler.ServeHTTP(w, r) + case SignalServiceGetItemSignalsProcedure: + signalServiceGetItemSignalsHandler.ServeHTTP(w, r) + case SignalServiceGetItemSignalsV2Procedure: + signalServiceGetItemSignalsV2Handler.ServeHTTP(w, r) + case SignalServiceGetCustomSignalsByCategoryProcedure: + signalServiceGetCustomSignalsByCategoryHandler.ServeHTTP(w, r) + case SignalServiceGetItemSignalDetailsProcedure: + signalServiceGetItemSignalDetailsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedSignalServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedSignalServiceHandler struct{} + +func (UnimplementedSignalServiceHandler) AddSignal(context.Context, *connect.Request[sdp_go.AddSignalRequest]) (*connect.Response[sdp_go.AddSignalResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.AddSignal is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetSignalsByChangeExternalID(context.Context, *connect.Request[sdp_go.GetSignalsByChangeExternalIDRequest]) (*connect.Response[sdp_go.GetSignalsByChangeExternalIDResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetSignalsByChangeExternalID is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetChangeOverviewSignals(context.Context, *connect.Request[sdp_go.GetChangeOverviewSignalsRequest]) (*connect.Response[sdp_go.GetChangeOverviewSignalsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetChangeOverviewSignals is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetItemSignals(context.Context, *connect.Request[sdp_go.GetItemSignalsRequest]) (*connect.Response[sdp_go.GetItemSignalsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignals is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetItemSignalsV2(context.Context, *connect.Request[sdp_go.GetItemSignalsRequestV2]) (*connect.Response[sdp_go.GetItemSignalsResponseV2], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignalsV2 is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetCustomSignalsByCategory(context.Context, *connect.Request[sdp_go.GetCustomSignalsByCategoryRequest]) (*connect.Response[sdp_go.GetCustomSignalsByCategoryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetCustomSignalsByCategory is not implemented")) +} + +func (UnimplementedSignalServiceHandler) GetItemSignalDetails(context.Context, *connect.Request[sdp_go.GetItemSignalDetailsRequest]) (*connect.Response[sdp_go.GetItemSignalDetailsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("signal.SignalService.GetItemSignalDetails is not implemented")) +} diff --git a/go/sdp-go/sdpconnect/snapshots.connect.go b/go/sdp-go/sdpconnect/snapshots.connect.go new file mode 100644 index 00000000..35566688 --- /dev/null +++ b/go/sdp-go/sdpconnect/snapshots.connect.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: snapshots.proto + +package sdpconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + sdp_go "github.com/overmindtech/cli/go/sdp-go" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // SnapshotsServiceName is the fully-qualified name of the SnapshotsService service. + SnapshotsServiceName = "snapshots.SnapshotsService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // SnapshotsServiceListSnapshotsProcedure is the fully-qualified name of the SnapshotsService's + // ListSnapshots RPC. + SnapshotsServiceListSnapshotsProcedure = "/snapshots.SnapshotsService/ListSnapshots" + // SnapshotsServiceCreateSnapshotProcedure is the fully-qualified name of the SnapshotsService's + // CreateSnapshot RPC. + SnapshotsServiceCreateSnapshotProcedure = "/snapshots.SnapshotsService/CreateSnapshot" + // SnapshotsServiceGetSnapshotProcedure is the fully-qualified name of the SnapshotsService's + // GetSnapshot RPC. + SnapshotsServiceGetSnapshotProcedure = "/snapshots.SnapshotsService/GetSnapshot" + // SnapshotsServiceUpdateSnapshotProcedure is the fully-qualified name of the SnapshotsService's + // UpdateSnapshot RPC. + SnapshotsServiceUpdateSnapshotProcedure = "/snapshots.SnapshotsService/UpdateSnapshot" + // SnapshotsServiceDeleteSnapshotProcedure is the fully-qualified name of the SnapshotsService's + // DeleteSnapshot RPC. + SnapshotsServiceDeleteSnapshotProcedure = "/snapshots.SnapshotsService/DeleteSnapshot" + // SnapshotsServiceListSnapshotByGUNProcedure is the fully-qualified name of the SnapshotsService's + // ListSnapshotByGUN RPC. + SnapshotsServiceListSnapshotByGUNProcedure = "/snapshots.SnapshotsService/ListSnapshotByGUN" +) + +// SnapshotsServiceClient is a client for the snapshots.SnapshotsService service. +type SnapshotsServiceClient interface { + ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) + CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) + GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) + UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) + DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) + ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) +} + +// NewSnapshotsServiceClient constructs a client for the snapshots.SnapshotsService service. By +// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, +// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewSnapshotsServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SnapshotsServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + snapshotsServiceMethods := sdp_go.File_snapshots_proto.Services().ByName("SnapshotsService").Methods() + return &snapshotsServiceClient{ + listSnapshots: connect.NewClient[sdp_go.ListSnapshotsRequest, sdp_go.ListSnapshotResponse]( + httpClient, + baseURL+SnapshotsServiceListSnapshotsProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshots")), + connect.WithClientOptions(opts...), + ), + createSnapshot: connect.NewClient[sdp_go.CreateSnapshotRequest, sdp_go.CreateSnapshotResponse]( + httpClient, + baseURL+SnapshotsServiceCreateSnapshotProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("CreateSnapshot")), + connect.WithClientOptions(opts...), + ), + getSnapshot: connect.NewClient[sdp_go.GetSnapshotRequest, sdp_go.GetSnapshotResponse]( + httpClient, + baseURL+SnapshotsServiceGetSnapshotProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("GetSnapshot")), + connect.WithClientOptions(opts...), + ), + updateSnapshot: connect.NewClient[sdp_go.UpdateSnapshotRequest, sdp_go.UpdateSnapshotResponse]( + httpClient, + baseURL+SnapshotsServiceUpdateSnapshotProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("UpdateSnapshot")), + connect.WithClientOptions(opts...), + ), + deleteSnapshot: connect.NewClient[sdp_go.DeleteSnapshotRequest, sdp_go.DeleteSnapshotResponse]( + httpClient, + baseURL+SnapshotsServiceDeleteSnapshotProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("DeleteSnapshot")), + connect.WithClientOptions(opts...), + ), + listSnapshotByGUN: connect.NewClient[sdp_go.ListSnapshotsByGUNRequest, sdp_go.ListSnapshotsByGUNResponse]( + httpClient, + baseURL+SnapshotsServiceListSnapshotByGUNProcedure, + connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshotByGUN")), + connect.WithClientOptions(opts...), + ), + } +} + +// snapshotsServiceClient implements SnapshotsServiceClient. +type snapshotsServiceClient struct { + listSnapshots *connect.Client[sdp_go.ListSnapshotsRequest, sdp_go.ListSnapshotResponse] + createSnapshot *connect.Client[sdp_go.CreateSnapshotRequest, sdp_go.CreateSnapshotResponse] + getSnapshot *connect.Client[sdp_go.GetSnapshotRequest, sdp_go.GetSnapshotResponse] + updateSnapshot *connect.Client[sdp_go.UpdateSnapshotRequest, sdp_go.UpdateSnapshotResponse] + deleteSnapshot *connect.Client[sdp_go.DeleteSnapshotRequest, sdp_go.DeleteSnapshotResponse] + listSnapshotByGUN *connect.Client[sdp_go.ListSnapshotsByGUNRequest, sdp_go.ListSnapshotsByGUNResponse] +} + +// ListSnapshots calls snapshots.SnapshotsService.ListSnapshots. +func (c *snapshotsServiceClient) ListSnapshots(ctx context.Context, req *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) { + return c.listSnapshots.CallUnary(ctx, req) +} + +// CreateSnapshot calls snapshots.SnapshotsService.CreateSnapshot. +func (c *snapshotsServiceClient) CreateSnapshot(ctx context.Context, req *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) { + return c.createSnapshot.CallUnary(ctx, req) +} + +// GetSnapshot calls snapshots.SnapshotsService.GetSnapshot. +func (c *snapshotsServiceClient) GetSnapshot(ctx context.Context, req *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) { + return c.getSnapshot.CallUnary(ctx, req) +} + +// UpdateSnapshot calls snapshots.SnapshotsService.UpdateSnapshot. +func (c *snapshotsServiceClient) UpdateSnapshot(ctx context.Context, req *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) { + return c.updateSnapshot.CallUnary(ctx, req) +} + +// DeleteSnapshot calls snapshots.SnapshotsService.DeleteSnapshot. +func (c *snapshotsServiceClient) DeleteSnapshot(ctx context.Context, req *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) { + return c.deleteSnapshot.CallUnary(ctx, req) +} + +// ListSnapshotByGUN calls snapshots.SnapshotsService.ListSnapshotByGUN. +func (c *snapshotsServiceClient) ListSnapshotByGUN(ctx context.Context, req *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) { + return c.listSnapshotByGUN.CallUnary(ctx, req) +} + +// SnapshotsServiceHandler is an implementation of the snapshots.SnapshotsService service. +type SnapshotsServiceHandler interface { + ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) + CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) + GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) + UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) + DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) + ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) +} + +// NewSnapshotsServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewSnapshotsServiceHandler(svc SnapshotsServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + snapshotsServiceMethods := sdp_go.File_snapshots_proto.Services().ByName("SnapshotsService").Methods() + snapshotsServiceListSnapshotsHandler := connect.NewUnaryHandler( + SnapshotsServiceListSnapshotsProcedure, + svc.ListSnapshots, + connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshots")), + connect.WithHandlerOptions(opts...), + ) + snapshotsServiceCreateSnapshotHandler := connect.NewUnaryHandler( + SnapshotsServiceCreateSnapshotProcedure, + svc.CreateSnapshot, + connect.WithSchema(snapshotsServiceMethods.ByName("CreateSnapshot")), + connect.WithHandlerOptions(opts...), + ) + snapshotsServiceGetSnapshotHandler := connect.NewUnaryHandler( + SnapshotsServiceGetSnapshotProcedure, + svc.GetSnapshot, + connect.WithSchema(snapshotsServiceMethods.ByName("GetSnapshot")), + connect.WithHandlerOptions(opts...), + ) + snapshotsServiceUpdateSnapshotHandler := connect.NewUnaryHandler( + SnapshotsServiceUpdateSnapshotProcedure, + svc.UpdateSnapshot, + connect.WithSchema(snapshotsServiceMethods.ByName("UpdateSnapshot")), + connect.WithHandlerOptions(opts...), + ) + snapshotsServiceDeleteSnapshotHandler := connect.NewUnaryHandler( + SnapshotsServiceDeleteSnapshotProcedure, + svc.DeleteSnapshot, + connect.WithSchema(snapshotsServiceMethods.ByName("DeleteSnapshot")), + connect.WithHandlerOptions(opts...), + ) + snapshotsServiceListSnapshotByGUNHandler := connect.NewUnaryHandler( + SnapshotsServiceListSnapshotByGUNProcedure, + svc.ListSnapshotByGUN, + connect.WithSchema(snapshotsServiceMethods.ByName("ListSnapshotByGUN")), + connect.WithHandlerOptions(opts...), + ) + return "/snapshots.SnapshotsService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SnapshotsServiceListSnapshotsProcedure: + snapshotsServiceListSnapshotsHandler.ServeHTTP(w, r) + case SnapshotsServiceCreateSnapshotProcedure: + snapshotsServiceCreateSnapshotHandler.ServeHTTP(w, r) + case SnapshotsServiceGetSnapshotProcedure: + snapshotsServiceGetSnapshotHandler.ServeHTTP(w, r) + case SnapshotsServiceUpdateSnapshotProcedure: + snapshotsServiceUpdateSnapshotHandler.ServeHTTP(w, r) + case SnapshotsServiceDeleteSnapshotProcedure: + snapshotsServiceDeleteSnapshotHandler.ServeHTTP(w, r) + case SnapshotsServiceListSnapshotByGUNProcedure: + snapshotsServiceListSnapshotByGUNHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedSnapshotsServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedSnapshotsServiceHandler struct{} + +func (UnimplementedSnapshotsServiceHandler) ListSnapshots(context.Context, *connect.Request[sdp_go.ListSnapshotsRequest]) (*connect.Response[sdp_go.ListSnapshotResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.ListSnapshots is not implemented")) +} + +func (UnimplementedSnapshotsServiceHandler) CreateSnapshot(context.Context, *connect.Request[sdp_go.CreateSnapshotRequest]) (*connect.Response[sdp_go.CreateSnapshotResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.CreateSnapshot is not implemented")) +} + +func (UnimplementedSnapshotsServiceHandler) GetSnapshot(context.Context, *connect.Request[sdp_go.GetSnapshotRequest]) (*connect.Response[sdp_go.GetSnapshotResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.GetSnapshot is not implemented")) +} + +func (UnimplementedSnapshotsServiceHandler) UpdateSnapshot(context.Context, *connect.Request[sdp_go.UpdateSnapshotRequest]) (*connect.Response[sdp_go.UpdateSnapshotResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.UpdateSnapshot is not implemented")) +} + +func (UnimplementedSnapshotsServiceHandler) DeleteSnapshot(context.Context, *connect.Request[sdp_go.DeleteSnapshotRequest]) (*connect.Response[sdp_go.DeleteSnapshotResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.DeleteSnapshot is not implemented")) +} + +func (UnimplementedSnapshotsServiceHandler) ListSnapshotByGUN(context.Context, *connect.Request[sdp_go.ListSnapshotsByGUNRequest]) (*connect.Response[sdp_go.ListSnapshotsByGUNResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("snapshots.SnapshotsService.ListSnapshotByGUN is not implemented")) +} diff --git a/go/sdp-go/sdpws/client.go b/go/sdp-go/sdpws/client.go new file mode 100644 index 00000000..7c7f62cf --- /dev/null +++ b/go/sdp-go/sdpws/client.go @@ -0,0 +1,525 @@ +package sdpws + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "slices" + "sync" + + "github.com/coder/websocket" + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// Client is the main driver for all interactions with a SDP/Gateway websocket. +// +// Internally it holds a map of all active requests, which are identified by a +// UUID, to multiplex incoming responses to the correct caller. Note that the +// request methods block until the response is received, so to send multiple +// requests in parallel, call requestor methods in goroutines, e.g. using a conc +// Pool: +// +// ``` +// +// pool := pool.New().WithContext(ctx).WithCancelOnError().WithFirstError() +// pool.Go(func() error { +// items, err := client.Query(ctx, q) +// if err != nil { +// return err +// } +// // do something with items +// } +// // ... +// pool.Wait() +// +// ``` +// +// Alternatively, pass in a GatewayMessageHandler to receive all messages as +// they come in and send messages directly using `Send()` and then `Wait()` for +// all request IDs. +type Client struct { + conn *websocket.Conn + + handler GatewayMessageHandler + + requestMap map[uuid.UUID]chan *sdp.GatewayResponse + requestMapMu sync.RWMutex + + finishedRequestMap map[uuid.UUID]bool + finishedRequestMapCond *sync.Cond + finishedRequestMapMu sync.Mutex + + err error + errMu sync.Mutex + + closed bool + closedMu sync.Mutex + + // receiveCtx is the context for the receive goroutine + // receiveCancel cancels the receive context + // receiveDone signals when receive has finished + receiveCtx context.Context + receiveCancel context.CancelFunc + receiveDone sync.WaitGroup +} + +// Dial connects to the given URL and returns a new Client. Pass nil as handler +// if you do not need per-message callbacks. +// +// To stop the client, cancel the provided context: +// +// ``` +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// client, err := sdpws.Dial(ctx, gatewayUrl, NewAuthenticatedClient(ctx, otelhttp.DefaultClient), nil) +// ``` +func Dial(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler) (*Client, error) { + return dialImpl(ctx, u, httpClient, handler, true) +} + +// DialBatch connects to the given URL and returns a new Client. Pass nil as +// handler if you do not need per-message callbacks. This method is intended for +// batch processing and sets up opentelemetry propagation. Otherwise this +// equivalent to `Dial()` +func DialBatch(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler) (*Client, error) { + return dialImpl(ctx, u, httpClient, handler, false) +} + +func dialImpl(ctx context.Context, u string, httpClient *http.Client, handler GatewayMessageHandler, interactive bool) (*Client, error) { + if httpClient == nil { + httpClient = tracing.HTTPClient() + } + options := &websocket.DialOptions{ + HTTPClient: httpClient, + } + if !interactive { + options.HTTPHeader = http.Header{ + "X-overmind-interactive": []string{"false"}, + } + } + + //nolint: bodyclose // github.com/coder/websocket reads the body internally + conn, _, err := websocket.Dial(ctx, u, options) + if err != nil { + return nil, err + } + + // the default, 32kB is too small for cert bundles and rds-db-cluster-parameter-groups + conn.SetReadLimit(2 * 1024 * 1024) + + c := &Client{ + conn: conn, + handler: handler, + requestMap: make(map[uuid.UUID]chan *sdp.GatewayResponse), + finishedRequestMap: make(map[uuid.UUID]bool), + } + c.finishedRequestMapCond = sync.NewCond(&c.finishedRequestMapMu) + + // Create a dedicated context for receive() that we can cancel independently + c.receiveCtx, c.receiveCancel = context.WithCancel(ctx) + c.receiveDone.Add(1) + go func() { + defer c.receiveDone.Done() + c.receive(c.receiveCtx) + }() + + return c, nil +} + +func (c *Client) receive(ctx context.Context) { + defer tracing.LogRecoverToReturn(ctx, "sdpws.Client.receive") + for { + // Check if context is cancelled before attempting to read + // This prevents lock acquisition failures when context is cancelled during Close() + if ctx.Err() != nil { + // Context is cancelled - exit gracefully without calling abort + // This prevents "failed to acquire lock" errors when Close() is called + // with a cancelled context. The abort() will be called by Close() itself. + return + } + + // Check if client is already closed before attempting to read + // This prevents errors when Close() is called from another goroutine + if c.Closed() { + return + } + + msg := &sdp.GatewayResponse{} + + typ, r, err := c.conn.Reader(ctx) + if err != nil { + // If context is cancelled, this is expected during Close() and we should + // exit gracefully without calling abort to avoid lock contention. + // The abort() will be called by Close() itself. + if ctx.Err() != nil { + // Context cancelled - exit gracefully + // Don't call abort() here as it may already be closing, which could + // cause "failed to acquire lock" errors + return + } + // Check if this is a normal closure from the remote side + var ce websocket.CloseError + if errors.As(err, &ce) && ce.Code == websocket.StatusNormalClosure { + // Normal closure from remote - exit gracefully + // Call abort() with nil to properly set the closed state + // abort() will handle normal closure gracefully (no error logged) + c.abort(ctx, nil) + return + } + // For other errors, abort normally + c.abort(ctx, fmt.Errorf("failed to initialise websocket reader: %w", err)) + return + } + if typ != websocket.MessageBinary { + c.conn.Close(websocket.StatusUnsupportedData, "expected binary message") + c.abort(ctx, fmt.Errorf("expected binary message for protobuf but got: %v", typ)) + return + } + + b := new(bytes.Buffer) + _, err = b.ReadFrom(r) + if err != nil { + c.abort(ctx, fmt.Errorf("failed to read from websocket: %w", err)) + return + } + + err = proto.Unmarshal(b.Bytes(), msg) + if err != nil { + c.abort(ctx, fmt.Errorf("error unmarshalling message: %w", err)) + return + } + + switch msg.GetResponseType().(type) { + case *sdp.GatewayResponse_NewItem: + item := msg.GetNewItem() + if c.handler != nil { + c.handler.NewItem(ctx, item) + } + u, err := uuid.FromBytes(item.GetMetadata().GetSourceQuery().GetUUID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_NewEdge: + edge := msg.GetNewEdge() + if c.handler != nil { + c.handler.NewEdge(ctx, edge) + } + // TODO: edges are not attached to a specific query, so we can't send them to a request channel + // maybe that's not a problem anyways? + // c, ok := c.getRequestChan(uuid.UUID(edge.Metadata.SourceQuery.UUID)) + // if ok { + // c <- msg + // } + + case *sdp.GatewayResponse_Status: + status := msg.GetStatus() + if c.handler != nil { + c.handler.Status(ctx, status) + } + + case *sdp.GatewayResponse_QueryError: + qe := msg.GetQueryError() + if c.handler != nil { + c.handler.QueryError(ctx, qe) + } + u, err := uuid.FromBytes(qe.GetUUID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_DeleteItem: + item := msg.GetDeleteItem() + if c.handler != nil { + c.handler.DeleteItem(ctx, item) + } + + case *sdp.GatewayResponse_DeleteEdge: + edge := msg.GetDeleteEdge() + if c.handler != nil { + c.handler.DeleteEdge(ctx, edge) + } + + case *sdp.GatewayResponse_UpdateItem: + item := msg.GetUpdateItem() + if c.handler != nil { + c.handler.UpdateItem(ctx, item) + } + + case *sdp.GatewayResponse_SnapshotStoreResult: + result := msg.GetSnapshotStoreResult() + if c.handler != nil { + c.handler.SnapshotStoreResult(ctx, result) + } + u, err := uuid.FromBytes(result.GetMsgID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_SnapshotLoadResult: + result := msg.GetSnapshotLoadResult() + if c.handler != nil { + c.handler.SnapshotLoadResult(ctx, result) + } + u, err := uuid.FromBytes(result.GetMsgID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_BookmarkStoreResult: + result := msg.GetBookmarkStoreResult() + if c.handler != nil { + c.handler.BookmarkStoreResult(ctx, result) + } + u, err := uuid.FromBytes(result.GetMsgID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_BookmarkLoadResult: + result := msg.GetBookmarkLoadResult() + if c.handler != nil { + c.handler.BookmarkLoadResult(ctx, result) + } + u, err := uuid.FromBytes(result.GetMsgID()) + if err == nil { + c.postRequestChan(u, msg) + } + + case *sdp.GatewayResponse_QueryStatus: + qs := msg.GetQueryStatus() + if c.handler != nil { + c.handler.QueryStatus(ctx, qs) + } + u, err := uuid.FromBytes(qs.GetUUID()) + if err == nil { + c.postRequestChan(u, msg) + } + + switch qs.GetStatus() { //nolint: exhaustive // ignore sdp.QueryStatus_UNSPECIFIED, sdp.QueryStatus_STARTED + case sdp.QueryStatus_FINISHED, sdp.QueryStatus_CANCELLED, sdp.QueryStatus_ERRORED: + c.finishRequestChan(u) + } + + case *sdp.GatewayResponse_ChatResponse: + chatResponse := msg.GetChatResponse() + if c.handler != nil { + c.handler.ChatResponse(ctx, chatResponse) + } + c.postRequestChan(uuid.Nil, msg) + + case *sdp.GatewayResponse_ToolStart: + toolStart := msg.GetToolStart() + if c.handler != nil { + c.handler.ToolStart(ctx, toolStart) + } + c.postRequestChan(uuid.Nil, msg) + + case *sdp.GatewayResponse_ToolFinish: + toolFinish := msg.GetToolFinish() + if c.handler != nil { + c.handler.ToolFinish(ctx, toolFinish) + } + c.postRequestChan(uuid.Nil, msg) + + default: + log.WithContext(ctx).WithField("response", msg).WithField("responseType", fmt.Sprintf("%T", msg.GetResponseType())).Warn("unexpected response") + } + } +} + +func (c *Client) send(ctx context.Context, msg *sdp.GatewayRequest) error { + buf, err := proto.Marshal(msg) + if err != nil { + log.WithContext(ctx).WithError(err).WithField("request", msg).Trace("error marshaling request") + c.abort(ctx, err) + return err + } + + err = c.conn.Write(ctx, websocket.MessageBinary, buf) + if err != nil { + log.WithContext(ctx).WithError(err).WithField("request", msg).Trace("error writing request to websocket") + c.abort(ctx, err) + return err + } + return nil +} + +// Wait blocks until all specified requests have been finished. Waiting on a +// closed client returns immediately with no error. +func (c *Client) Wait(ctx context.Context, reqIDs uuid.UUIDs) error { + for { + if c.Closed() { + return nil + } + + // check for context cancellation + if ctx.Err() != nil { + return ctx.Err() + } + + // wrap this in a function so defers can be called (otherwise the lock is held for all loop iterations) + finished := func() bool { + c.finishedRequestMapMu.Lock() + defer c.finishedRequestMapMu.Unlock() + + // remove all finished requests from the list of requests to wait for + reqIDs = slices.DeleteFunc(reqIDs, func(reqID uuid.UUID) bool { + _, ok := c.finishedRequestMap[reqID] + return ok + }) + if len(reqIDs) == 0 { + return true + } + + c.finishedRequestMapCond.Wait() + return false + }() + + if finished { + return nil + } + } +} + +// abort stores the specified error and closes the connection. +func (c *Client) abort(ctx context.Context, err error) { + c.closedMu.Lock() + if c.closed { + c.closedMu.Unlock() + return + } + c.closed = true + c.closedMu.Unlock() + + isNormalClosure := false + var ce websocket.CloseError + if errors.As(err, &ce) { + // tear down the connection without a new error if this is a regular close + isNormalClosure = ce.Code == websocket.StatusNormalClosure + } + + if err != nil && !isNormalClosure { + log.WithContext(ctx).WithError(err).Error("aborting client") + } + c.errMu.Lock() + c.err = errors.Join(c.err, err) + c.errMu.Unlock() + + // Cancel the receive context to stop receive() from reading more messages. + if c.receiveCancel != nil { + c.receiveCancel() + } + + // call this outside of the lock to avoid deadlock should other parts of the + // code try to call abort() when crashing out of a read or write + // Only close the connection if it exists (may be nil in test scenarios) + var closeErr error + if c.conn != nil { + closeErr = c.conn.Close(websocket.StatusNormalClosure, "normal closure") + } + + c.errMu.Lock() + c.err = errors.Join(c.err, closeErr) + c.errMu.Unlock() + + c.closeAllRequestChans() +} + +// Close closes the connection and returns any errors from the underlying connection. +func (c *Client) Close(ctx context.Context) error { + // Cancel the receive context first to stop receive() from reading more messages + if c.receiveCancel != nil { + c.receiveCancel() + } + + // Wait for receive() to finish reading/sending its last message before closing channels. + // This prevents race conditions where we close channels while receive() is still trying + // to send to them. We do this before calling abort() to ensure receive() has finished. + if c.receiveCancel != nil { + c.receiveDone.Wait() + } + + c.abort(ctx, nil) + + c.errMu.Lock() + defer c.errMu.Unlock() + return c.err +} + +func (c *Client) Closed() bool { + c.closedMu.Lock() + defer c.closedMu.Unlock() + return c.closed +} + +func (c *Client) createRequestChan(u uuid.UUID) chan *sdp.GatewayResponse { + r := make(chan *sdp.GatewayResponse, 1) + c.requestMapMu.Lock() + defer c.requestMapMu.Unlock() + c.requestMap[u] = r + return r +} + +func (c *Client) postRequestChan(u uuid.UUID, msg *sdp.GatewayResponse) { + c.requestMapMu.RLock() + r, ok := c.requestMap[u] + c.requestMapMu.RUnlock() + + if !ok { + return + } + + // Check if client is closed before sending. If closed, channels may be closed, + // so we should not attempt to send. With proper context handling, receive() will + // have finished before channels are closed, but we check here as a safety measure. + if c.Closed() { + return + } + + // Use select with receive context to avoid blocking when context is cancelled. + // This prevents deadlock where receive() is blocked on send while Close() is waiting for it. + // During normal operation (context not cancelled), the send case will be selected, + // ensuring no messages are dropped. When context is cancelled, we use non-blocking send. + select { + case <-c.receiveCtx.Done(): + return + case r <- msg: + // Successfully sent (normal operation - blocking until receiver reads) + return + } +} + +func (c *Client) finishRequestChan(u uuid.UUID) { + c.requestMapMu.Lock() + defer c.requestMapMu.Unlock() + + c.finishedRequestMapMu.Lock() + defer c.finishedRequestMapMu.Unlock() + + delete(c.requestMap, u) + c.finishedRequestMap[u] = true + c.finishedRequestMapCond.Broadcast() +} + +func (c *Client) closeAllRequestChans() { + c.requestMapMu.Lock() + defer c.requestMapMu.Unlock() + + c.finishedRequestMapMu.Lock() + defer c.finishedRequestMapMu.Unlock() + + for k, v := range c.requestMap { + close(v) + c.finishedRequestMap[k] = true + } + // clear the map to free up memory + c.requestMap = map[uuid.UUID]chan *sdp.GatewayResponse{} + c.finishedRequestMapCond.Broadcast() +} diff --git a/go/sdp-go/sdpws/client_test.go b/go/sdp-go/sdpws/client_test.go new file mode 100644 index 00000000..57fdd5dd --- /dev/null +++ b/go/sdp-go/sdpws/client_test.go @@ -0,0 +1,1064 @@ +package sdpws + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "sync" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "go.uber.org/goleak" + "google.golang.org/protobuf/proto" +) + +// Helper function to check if a slice contains a string +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// TestServer is a test server for the websocket client. Note that this can only +// handle a single connection at a time. +type testServer struct { + url string + + conn *websocket.Conn + connMu sync.Mutex + + requests []*sdp.GatewayRequest + requestsMu sync.Mutex +} + +func newTestServer(_ context.Context, t *testing.T) (*testServer, func()) { + ts := &testServer{ + requests: make([]*sdp.GatewayRequest, 0), + } + + serveMux := http.NewServeMux() + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, nil) + if err != nil { + return + } + defer func() { + _ = c.Close(websocket.StatusNormalClosure, "") + }() + + ts.connMu.Lock() + ts.conn = c + ts.connMu.Unlock() + + // ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + // defer cancel() + + for { + msg := &sdp.GatewayRequest{} + + typ, reader, err := c.Reader(r.Context()) + if err != nil { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("failed to initialise websocket reader: %v", err)) + return + } + if typ != websocket.MessageBinary { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("expected binary message for protobuf but got: %v", typ)) + t.Fatalf("expected binary message for protobuf but got: %v", typ) + return + } + + b := new(bytes.Buffer) + _, err = b.ReadFrom(reader) + if err != nil { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("failed to read from websocket: %v", err)) + t.Fatalf("failed to read from websocket: %v", err) + return + } + + err = proto.Unmarshal(b.Bytes(), msg) + if err != nil { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error un marshaling message: %v", err)) + t.Fatalf("error un marshaling message: %v", err) + return + } + + ts.requestsMu.Lock() + ts.requests = append(ts.requests, msg) + ts.requestsMu.Unlock() + } + }) + + s := httptest.NewServer(serveMux) + ts.url = s.URL + + return ts, func() { + s.Close() + } +} + +func (ts *testServer) inject(ctx context.Context, msg *sdp.GatewayResponse) { + ts.connMu.Lock() + c := ts.conn + ts.connMu.Unlock() + + buf, err := proto.Marshal(msg) + if err != nil { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error marshaling message: %v", err)) + return + } + + err = c.Write(ctx, websocket.MessageBinary, buf) + if err != nil { + c.Close(websocket.StatusAbnormalClosure, fmt.Sprintf("error writing message: %v", err)) + return + } +} + +func TestClient(t *testing.T) { + defer goleak.VerifyNone(t) + + t.Run("Query", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + u := uuid.New() + + q := &sdp.Query{ + UUID: u[:], + Type: "", + Method: 0, + Query: "", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "", + IgnoreCache: false, + } + + go func() { + time.Sleep(100 * time.Millisecond) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: u[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + }() + + // this will block until the above goroutine has injected the response + _, err = c.QueryOne(ctx, q) + if err != nil { + t.Fatal(err) + } + err = c.Wait(ctx, uuid.UUIDs{u}) + if err != nil { + t.Fatal(err) + } + + ts.requestsMu.Lock() + defer ts.requestsMu.Unlock() + + if len(ts.requests) != 1 { + t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) + } + + recvQ, ok := ts.requests[0].GetRequestType().(*sdp.GatewayRequest_Query) + if !ok || uuid.UUID(recvQ.Query.GetUUID()) != u { + t.Fatalf("expected query, got %v", ts.requests[0]) + } + }) + + t.Run("QueryNotFound", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + u := uuid.New() + + q := &sdp.Query{ + UUID: u[:], + Type: "", + Method: 0, + Query: "", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "", + IgnoreCache: false, + } + + go func() { + time.Sleep(100 * time.Millisecond) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: u[:], + Status: sdp.QueryStatus_STARTED, + }, + }, + }) + time.Sleep(100 * time.Millisecond) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryError{ + QueryError: &sdp.QueryError{ + UUID: u[:], + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "not found", + Scope: "scope", + SourceName: "src name", + ItemType: "item type", + ResponderName: "responder name", + }, + }, + }) + time.Sleep(100 * time.Millisecond) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: u[:], + Status: sdp.QueryStatus_ERRORED, + }, + }, + }) + }() + + // this will block until the above goroutine has injected the response + _, err = c.QueryOne(ctx, q) + if err != nil { + t.Fatal(err) + } + err = c.Wait(ctx, uuid.UUIDs{u}) + if err != nil { + t.Fatal(err) + } + + ts.requestsMu.Lock() + defer ts.requestsMu.Unlock() + + if len(ts.requests) != 1 { + t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) + } + + recvQ, ok := ts.requests[0].GetRequestType().(*sdp.GatewayRequest_Query) + if !ok || uuid.UUID(recvQ.Query.GetUUID()) != u { + t.Fatalf("expected query, got %v", ts.requests[0]) + } + }) + + t.Run("StoreSnapshot", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + u := uuid.New() + + go func() { + time.Sleep(100 * time.Millisecond) + ts.requestsMu.Lock() + msgID := ts.requests[0].GetStoreSnapshot().GetMsgID() + ts.requestsMu.Unlock() + + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_SnapshotStoreResult{ + SnapshotStoreResult: &sdp.SnapshotStoreResult{ + Success: true, + ErrorMessage: "", + MsgID: msgID, + SnapshotID: u[:], + }, + }, + }) + }() + + // this will block until the above goroutine has injected the response + snapu, err := c.StoreSnapshot(ctx, "name", "description") + if err != nil { + t.Fatal(err) + } + if snapu != u { + t.Errorf("expected snapshot id %v, got %v", u, snapu) + } + + ts.requestsMu.Lock() + defer ts.requestsMu.Unlock() + + if len(ts.requests) != 1 { + t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) + } + }) + + t.Run("StoreBookmark", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + u := uuid.New() + + go func() { + time.Sleep(100 * time.Millisecond) + ts.requestsMu.Lock() + msgID := ts.requests[0].GetStoreBookmark().GetMsgID() + ts.requestsMu.Unlock() + + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_BookmarkStoreResult{ + BookmarkStoreResult: &sdp.BookmarkStoreResult{ + Success: true, + ErrorMessage: "", + MsgID: msgID, + BookmarkID: u[:], + }, + }, + }) + }() + + // this will block until the above goroutine has injected the response + snapu, err := c.StoreBookmark(ctx, "name", "description", true) + if err != nil { + t.Fatal(err) + } + if snapu != u { + t.Errorf("expected bookmark id %v, got %v", u, snapu) + } + + ts.requestsMu.Lock() + defer ts.requestsMu.Unlock() + + if len(ts.requests) != 1 { + t.Fatalf("expected 1 request, got %v: %v", len(ts.requests), ts.requests) + } + }) + + t.Run("ConcurrentQueries", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + // Create multiple queries with different UUIDs + numQueries := 5 + queries := make([]*sdp.Query, numQueries) + expectedItems := make(map[string]*sdp.Item) + + for i := range numQueries { + u := uuid.New() + queries[i] = &sdp.Query{ + UUID: u[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: fmt.Sprintf("query-%d", i), + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + // Create expected items that should be returned for each query + expectedItems[u.String()] = &sdp.Item{ + Type: "test", + UniqueAttribute: fmt.Sprintf("item-%d", i), + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: queries[i], + }, + } + } + + // Inject responses in a different order than queries to test proper routing + go func() { + time.Sleep(50 * time.Millisecond) + + // Send responses in reverse order to test UUID-based routing + for i := numQueries - 1; i >= 0; i-- { + u := uuid.UUID(queries[i].GetUUID()) + + // Send an item response first + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: expectedItems[u.String()], + }, + }) + + // Then send the completion status + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: u[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + + // Add a small delay between responses to make race conditions more likely + time.Sleep(10 * time.Millisecond) + } + }() + + // Execute all queries concurrently + type queryResult struct { + index int + items []*sdp.Item + err error + } + + results := make([]queryResult, numQueries) + var wg sync.WaitGroup + + for i := range numQueries { + wg.Add(1) + go func(index int) { + defer wg.Done() + items, err := c.QueryOne(ctx, queries[index]) + results[index] = queryResult{ + index: index, + items: items, + err: err, + } + }(i) + } + + wg.Wait() + + // Verify that each query got the correct response + for i, result := range results { + if result.err != nil { + t.Errorf("Query %d failed: %v", i, result.err) + continue + } + + if len(result.items) != 1 { + t.Errorf("Query %d: expected 1 item, got %d", i, len(result.items)) + continue + } + + receivedItem := result.items[0] + expectedUniqueAttr := fmt.Sprintf("item-%d", i) + + if receivedItem.GetUniqueAttribute() != expectedUniqueAttr { + t.Errorf("Query %d: expected item with unique attribute %s, got %s", + i, expectedUniqueAttr, receivedItem.GetUniqueAttribute()) + } + + // Verify the item's metadata contains the correct source query + if receivedItem.GetMetadata() == nil || receivedItem.GetMetadata().GetSourceQuery() == nil { + t.Errorf("Query %d: item missing metadata or source query", i) + continue + } + + sourceQueryUUID := uuid.UUID(receivedItem.GetMetadata().GetSourceQuery().GetUUID()) + expectedUUID := uuid.UUID(queries[i].GetUUID()) + + if sourceQueryUUID != expectedUUID { + t.Errorf("Query %d: expected source query UUID %s, got %s", + i, expectedUUID, sourceQueryUUID) + } + } + + // Verify that the server received all queries + ts.requestsMu.Lock() + defer ts.requestsMu.Unlock() + + if len(ts.requests) != numQueries { + t.Fatalf("expected %d requests, got %d", numQueries, len(ts.requests)) + } + }) + + t.Run("ResponseMixupPrevention", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + // Create two queries with different UUIDs + query1UUID := uuid.New() + query2UUID := uuid.New() + + query1 := &sdp.Query{ + UUID: query1UUID[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: "query-1", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + query2 := &sdp.Query{ + UUID: query2UUID[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: "query-2", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + // Items that should be returned for each query + item1 := &sdp.Item{ + Type: "test", + UniqueAttribute: "item-for-query-1", + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: query1, + }, + } + + item2 := &sdp.Item{ + Type: "test", + UniqueAttribute: "item-for-query-2", + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: query2, + }, + } + + // Inject responses in a way that could cause mixup if UUIDs aren't handled correctly + go func() { + time.Sleep(50 * time.Millisecond) + + // Send responses for query2 first, then query1 + // If the client doesn't properly route by UUID, responses could get mixed up + + // Send multiple items for query2 + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: item2, + }, + }) + + // Send an item for query1 + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: item1, + }, + }) + + // Send another item for query2 to test multiple items per query + item2_duplicate := &sdp.Item{ + Type: "test", + UniqueAttribute: "item-for-query-2-duplicate", + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: query2, + }, + } + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: item2_duplicate, + }, + }) + + // Complete query1 first (even though we sent its response second) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: query1UUID[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + + // Complete query2 after query1 + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: query2UUID[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + }() + + // Execute both queries concurrently + type result struct { + items []*sdp.Item + err error + } + + var wg sync.WaitGroup + results := make([]result, 2) + + wg.Add(1) + go func() { + defer wg.Done() + items, err := c.QueryOne(ctx, query1) + results[0] = result{items: items, err: err} + }() + + wg.Add(1) + go func() { + defer wg.Done() + items, err := c.QueryOne(ctx, query2) + results[1] = result{items: items, err: err} + }() + + wg.Wait() + + // Verify query1 got the correct response + if results[0].err != nil { + t.Errorf("Query 1 failed: %v", results[0].err) + } else { + if len(results[0].items) != 1 { + t.Errorf("Query 1: expected 1 item, got %d", len(results[0].items)) + } else if results[0].items[0].GetUniqueAttribute() != "item-for-query-1" { + t.Errorf("Query 1: got wrong item: %s", results[0].items[0].GetUniqueAttribute()) + } + } + + // Verify query2 got the correct responses + if results[1].err != nil { + t.Errorf("Query 2 failed: %v", results[1].err) + } else { + if len(results[1].items) != 2 { + t.Errorf("Query 2: expected 2 items, got %d", len(results[1].items)) + } else { + // Check that both items are for query2 + for i, item := range results[1].items { + if !contains([]string{"item-for-query-2", "item-for-query-2-duplicate"}, item.GetUniqueAttribute()) { + t.Errorf("Query 2, item %d: got wrong item: %s", i, item.GetUniqueAttribute()) + } + } + } + } + }) + + t.Run("UUIDRoutingValidation", func(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := context.Background() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + // This test validates that responses are properly routed by UUID + // If the client were reading responses in order (FIFO) instead of by UUID, + // this test would fail because we send responses out of order + + queryA_UUID := uuid.New() + queryB_UUID := uuid.New() + + queryA := &sdp.Query{ + UUID: queryA_UUID[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: "query-A", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + queryB := &sdp.Query{ + UUID: queryB_UUID[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: "query-B", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + // Items that should be returned for each query + itemA := &sdp.Item{ + Type: "test", + UniqueAttribute: "item-A", + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: queryA, + }, + } + + itemB := &sdp.Item{ + Type: "test", + UniqueAttribute: "item-B", + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: queryB, + }, + } + + // Inject responses deliberately out of order + go func() { + time.Sleep(50 * time.Millisecond) + + // Send itemB first (for queryB), then itemA (for queryA) + // If the client doesn't route by UUID, queryA might get itemB + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: itemB, + }, + }) + + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: itemA, + }, + }) + + // Complete queryA first (even though itemA was sent second) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: queryA_UUID[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + + // Complete queryB second + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: queryB_UUID[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + }() + + // Execute queryA - it should get itemA despite itemB being sent first + var wg sync.WaitGroup + type result struct { + items []*sdp.Item + err error + } + + resultsA := make([]result, 1) + resultsB := make([]result, 1) + + wg.Add(1) + go func() { + defer wg.Done() + items, err := c.QueryOne(ctx, queryA) + resultsA[0] = result{items: items, err: err} + }() + + wg.Add(1) + go func() { + defer wg.Done() + items, err := c.QueryOne(ctx, queryB) + resultsB[0] = result{items: items, err: err} + }() + + wg.Wait() + + // Verify queryA got the correct item + if resultsA[0].err != nil { + t.Fatalf("Query A failed: %v", resultsA[0].err) + } + + if len(resultsA[0].items) != 1 { + t.Fatalf("Query A: expected 1 item, got %d", len(resultsA[0].items)) + } + + if resultsA[0].items[0].GetUniqueAttribute() != "item-A" { + t.Errorf("Query A got wrong item: expected 'item-A', got '%s'", resultsA[0].items[0].GetUniqueAttribute()) + } + + // Verify queryB got the correct item + if resultsB[0].err != nil { + t.Fatalf("Query B failed: %v", resultsB[0].err) + } + + if len(resultsB[0].items) != 1 { + t.Fatalf("Query B: expected 1 item, got %d", len(resultsB[0].items)) + } + + if resultsB[0].items[0].GetUniqueAttribute() != "item-B" { + t.Errorf("Query B got wrong item: expected 'item-B', got '%s'", resultsB[0].items[0].GetUniqueAttribute()) + } + }) +} + +// TestRaceConditionOnClose stresses the shutdown path around the historical +// "send on closed channel" bug. Originally, there was a race where: +// 1. postRequestChan is called and acquires the read lock +// 2. postRequestChan looks up a channel and prepares to send +// 3. Another goroutine calls closeAllRequestChans(), which closes all channels +// 4. postRequestChan tries to send on the now-closed channel and panics +// +// The implementation has since been fixed by eliminating this race via proper +// synchronization (e.g. context cancellation followed by waiting for the +// relevant goroutines to finish) instead of relying on recover(). This test +// verifies that no "send on closed channel" panics escape to the caller under +// concurrent activity and that the shutdown logic behaves correctly. +// +// This test is expected to pass cleanly when run with the Go race detector +// enabled (go test -race). It may be run multiple times to increase stress: +// go test -run TestRaceConditionOnClose -race -v -count=100 +func TestRaceConditionOnClose(t *testing.T) { + // Skip on CI to avoid flaky tests in automated environments + if os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" { + t.Skip("Skipping race condition test in CI environment") + } + + // Skip goleak for this stress test as we're intentionally testing race conditions + + ctx := t.Context() + + // Run many iterations to increase probability of hitting the race condition + // The race happens when postRequestChan and closeAllRequestChans run concurrently + iterations := 1000 + panics := 0 + + for iteration := range iterations { + func() { + // Create a minimal client with just the necessary fields + c := &Client{ + requestMap: make(map[uuid.UUID]chan *sdp.GatewayResponse), + finishedRequestMap: make(map[uuid.UUID]bool), + } + c.finishedRequestMapCond = sync.NewCond(&c.finishedRequestMapMu) + + // Initialize context handling to properly test the mechanism + // This simulates what Dial() does + c.receiveCtx, c.receiveCancel = context.WithCancel(ctx) + + // Create multiple request channels to increase race probability + numChannels := 10 + uuids := make([]uuid.UUID, numChannels) + for i := range numChannels { + uuids[i] = uuid.New() + ch := make(chan *sdp.GatewayResponse, 1) + c.requestMapMu.Lock() + c.requestMap[uuids[i]] = ch + c.requestMapMu.Unlock() + } + + // Create a message to send + msg := &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: &sdp.Item{ + Type: "test", + UniqueAttribute: fmt.Sprintf("item-%d", iteration), + Scope: "test", + }, + }, + } + + // Use a wait group to coordinate concurrent operations + var wg sync.WaitGroup + panicChan := make(chan bool, numChannels*2) + + // Start a simulated receive() goroutine that will call postRequestChan + // This simulates the real receive() behavior where it processes messages + // and calls postRequestChan() until the context is cancelled + c.receiveDone.Add(1) + go func() { + defer c.receiveDone.Done() + // Simulate receive() processing messages and calling postRequestChan + // It will be cancelled by Close() and should stop before channels are closed + for i := range 1000 { + // Check context before processing each message (like real receive() does) + if c.receiveCtx.Err() != nil { + // Context cancelled - exit gracefully (simulating receive() behavior) + return + } + // Simulate receive() processing a message and calling postRequestChan + // Use a random UUID to simulate different queries + // Wrap in recover() to catch any panics (shouldn't happen with proper context handling) + func() { + defer func() { + if r := recover(); r != nil { + panicChan <- true + } + }() + u := uuids[i%numChannels] + c.postRequestChan(u, msg) + }() + time.Sleep(time.Nanosecond) + } + }() + + // Start a goroutine that calls Close() concurrently + // Close() will cancel the receive context, wait for receive() to finish, + // and then close channels. This ensures receive() stops before channels are closed. + wg.Add(1) + go func() { + defer wg.Done() + // Wait a tiny bit to let some postRequestChan calls start + time.Sleep(time.Microsecond * 10) + // Use Close() which properly cancels receive context and waits before closing channels + _ = c.Close(ctx) + }() + + // Wait for all goroutines to complete + wg.Wait() + close(panicChan) + + // Count panics + for range panicChan { + panics++ + } + }() + } + + t.Logf("Successfully completed all %d iterations", iterations) + if panics > 0 { + t.Errorf("Detected %d panics - with proper context handling, receive() should stop before channels are closed, preventing panics. Panics indicate the context handling is not working correctly.", panics) + } else { + t.Logf("No panics detected - context handling is working correctly. receive() stops before channels are closed, preventing 'send on closed channel' panics") + } +} + +// TestNoMessageDroppingDuringNormalOperation verifies that messages are not +// dropped during normal operation when items arrive faster than they can be read. +// This test sends many items rapidly and verifies that all items are received. +func TestNoMessageDroppingDuringNormalOperation(t *testing.T) { + defer goleak.VerifyNone(t) + ctx := t.Context() + + ts, closeFn := newTestServer(ctx, t) + defer closeFn() + + c, err := Dial(ctx, ts.url, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = c.Close(ctx) + }() + + u := uuid.New() + q := &sdp.Query{ + UUID: u[:], + Type: "test", + Method: sdp.QueryMethod_GET, + Query: "query", + RecursionBehaviour: &sdp.Query_RecursionBehaviour{}, + Scope: "test", + IgnoreCache: false, + } + + // Send many items rapidly - more than the channel buffer size (1) + // to test that blocking send works correctly and no messages are dropped + numItems := 100 + expectedItems := make([]*sdp.Item, numItems) + for i := range numItems { + expectedItems[i] = &sdp.Item{ + Type: "test", + UniqueAttribute: fmt.Sprintf("item-%d", i), + Scope: "test", + Metadata: &sdp.Metadata{ + SourceQuery: q, + }, + } + } + + // Inject all items rapidly, then the completion status + go func() { + time.Sleep(50 * time.Millisecond) + // Send all items as fast as possible + for i := range numItems { + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_NewItem{ + NewItem: expectedItems[i], + }, + }) + } + // Then send the completion status + time.Sleep(10 * time.Millisecond) + ts.inject(ctx, &sdp.GatewayResponse{ + ResponseType: &sdp.GatewayResponse_QueryStatus{ + QueryStatus: &sdp.QueryStatus{ + UUID: u[:], + Status: sdp.QueryStatus_FINISHED, + }, + }, + }) + }() + + // QueryOne should receive all items, not drop any + items, err := c.QueryOne(ctx, q) + if err != nil { + t.Fatalf("QueryOne failed: %v", err) + } + + if len(items) != numItems { + t.Errorf("Expected %d items, got %d. Messages were dropped!", numItems, len(items)) + } + + // Verify we got all the expected items + receivedAttrs := make(map[string]bool) + for _, item := range items { + receivedAttrs[item.GetUniqueAttribute()] = true + } + + for i := range numItems { + expectedAttr := fmt.Sprintf("item-%d", i) + if !receivedAttrs[expectedAttr] { + t.Errorf("Missing expected item: %s", expectedAttr) + } + } +} diff --git a/go/sdp-go/sdpws/messagehandler.go b/go/sdp-go/sdpws/messagehandler.go new file mode 100644 index 00000000..e25fffdc --- /dev/null +++ b/go/sdp-go/sdpws/messagehandler.go @@ -0,0 +1,191 @@ +package sdpws + +import ( + "context" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" +) + +// GatewayMessageHandler is an interface that can be implemented to handle +// messages from the gateway. The individual methods are called when the sdpws +// client receives a message from the gateway. Methods are called in the same +// order as the messages are received from the gateway. The sdpws client +// guarantees that the methods are called in a single thread, so no locking is +// needed. +type GatewayMessageHandler interface { + NewItem(context.Context, *sdp.Item) + NewEdge(context.Context, *sdp.Edge) + Status(context.Context, *sdp.GatewayRequestStatus) + Error(context.Context, string) + QueryError(context.Context, *sdp.QueryError) + DeleteItem(context.Context, *sdp.Reference) + DeleteEdge(context.Context, *sdp.Edge) + UpdateItem(context.Context, *sdp.Item) + SnapshotStoreResult(context.Context, *sdp.SnapshotStoreResult) + SnapshotLoadResult(context.Context, *sdp.SnapshotLoadResult) + BookmarkStoreResult(context.Context, *sdp.BookmarkStoreResult) + BookmarkLoadResult(context.Context, *sdp.BookmarkLoadResult) + QueryStatus(context.Context, *sdp.QueryStatus) + ChatResponse(context.Context, *sdp.ChatResponse) + ToolStart(context.Context, *sdp.ToolStart) + ToolFinish(context.Context, *sdp.ToolFinish) +} + +type LoggingGatewayMessageHandler struct { + Level log.Level +} + +// assert that LoggingGatewayMessageHandler implements GatewayMessageHandler +var _ GatewayMessageHandler = (*LoggingGatewayMessageHandler)(nil) + +func (l *LoggingGatewayMessageHandler) NewItem(ctx context.Context, item *sdp.Item) { + log.WithContext(ctx).WithField("item", item).Log(l.Level, "received new item") +} + +func (l *LoggingGatewayMessageHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { + log.WithContext(ctx).WithField("edge", edge).Log(l.Level, "received new edge") +} + +func (l *LoggingGatewayMessageHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { + log.WithContext(ctx).WithField("status", status.GetSummary()).Log(l.Level, "received status") +} + +func (l *LoggingGatewayMessageHandler) Error(ctx context.Context, errorMessage string) { + log.WithContext(ctx).WithField("errorMessage", errorMessage).Log(l.Level, "received error") +} + +func (l *LoggingGatewayMessageHandler) QueryError(ctx context.Context, queryError *sdp.QueryError) { + log.WithContext(ctx).WithField("queryError", queryError).Log(l.Level, "received query error") +} + +func (l *LoggingGatewayMessageHandler) DeleteItem(ctx context.Context, reference *sdp.Reference) { + log.WithContext(ctx).WithField("reference", reference).Log(l.Level, "received delete item") +} + +func (l *LoggingGatewayMessageHandler) DeleteEdge(ctx context.Context, edge *sdp.Edge) { + log.WithContext(ctx).WithField("edge", edge).Log(l.Level, "received delete edge") +} + +func (l *LoggingGatewayMessageHandler) UpdateItem(ctx context.Context, item *sdp.Item) { + log.WithContext(ctx).WithField("item", item).Log(l.Level, "received updated item") +} + +func (l *LoggingGatewayMessageHandler) SnapshotStoreResult(ctx context.Context, result *sdp.SnapshotStoreResult) { + log.WithContext(ctx).WithField("result", result).Log(l.Level, "received snapshot store result") +} + +func (l *LoggingGatewayMessageHandler) SnapshotLoadResult(ctx context.Context, result *sdp.SnapshotLoadResult) { + log.WithContext(ctx).WithField("result", result).Log(l.Level, "received snapshot load result") +} + +func (l *LoggingGatewayMessageHandler) BookmarkStoreResult(ctx context.Context, result *sdp.BookmarkStoreResult) { + log.WithContext(ctx).WithField("result", result).Log(l.Level, "received bookmark store result") +} + +func (l *LoggingGatewayMessageHandler) BookmarkLoadResult(ctx context.Context, result *sdp.BookmarkLoadResult) { + log.WithContext(ctx).WithField("result", result).Log(l.Level, "received bookmark load result") +} + +func (l *LoggingGatewayMessageHandler) QueryStatus(ctx context.Context, status *sdp.QueryStatus) { + log.WithContext(ctx).WithField("status", status).WithField("uuid", status.GetUUIDParsed()).Log(l.Level, "received query status") +} + +func (l *LoggingGatewayMessageHandler) ChatResponse(ctx context.Context, chatResponse *sdp.ChatResponse) { + log.WithContext(ctx).WithField("chatResponse", chatResponse).Log(l.Level, "received chat response") +} + +func (l *LoggingGatewayMessageHandler) ToolStart(ctx context.Context, toolStart *sdp.ToolStart) { + log.WithContext(ctx).WithField("toolStart", toolStart).Log(l.Level, "received tool start") +} + +func (l *LoggingGatewayMessageHandler) ToolFinish(ctx context.Context, toolFinish *sdp.ToolFinish) { + log.WithContext(ctx).WithField("toolFinish", toolFinish).Log(l.Level, "received tool finish") +} + +type NoopGatewayMessageHandler struct{} + +// assert that NoopGatewayMessageHandler implements GatewayMessageHandler +var _ GatewayMessageHandler = (*NoopGatewayMessageHandler)(nil) + +func (l *NoopGatewayMessageHandler) NewItem(ctx context.Context, item *sdp.Item) { +} + +func (l *NoopGatewayMessageHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { +} + +func (l *NoopGatewayMessageHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { +} + +func (l *NoopGatewayMessageHandler) Error(ctx context.Context, errorMessage string) { +} + +func (l *NoopGatewayMessageHandler) QueryError(ctx context.Context, queryError *sdp.QueryError) { +} + +func (l *NoopGatewayMessageHandler) DeleteItem(ctx context.Context, reference *sdp.Reference) { +} + +func (l *NoopGatewayMessageHandler) DeleteEdge(ctx context.Context, edge *sdp.Edge) { +} + +func (l *NoopGatewayMessageHandler) UpdateItem(ctx context.Context, item *sdp.Item) { +} + +func (l *NoopGatewayMessageHandler) SnapshotStoreResult(ctx context.Context, result *sdp.SnapshotStoreResult) { +} + +func (l *NoopGatewayMessageHandler) SnapshotLoadResult(ctx context.Context, result *sdp.SnapshotLoadResult) { +} + +func (l *NoopGatewayMessageHandler) BookmarkStoreResult(ctx context.Context, result *sdp.BookmarkStoreResult) { +} + +func (l *NoopGatewayMessageHandler) BookmarkLoadResult(ctx context.Context, result *sdp.BookmarkLoadResult) { +} + +func (l *NoopGatewayMessageHandler) QueryStatus(ctx context.Context, status *sdp.QueryStatus) { +} + +func (l *NoopGatewayMessageHandler) ChatResponse(ctx context.Context, chatMessageResult *sdp.ChatResponse) { +} + +func (l *NoopGatewayMessageHandler) ToolStart(ctx context.Context, toolStart *sdp.ToolStart) { +} + +func (l *NoopGatewayMessageHandler) ToolFinish(ctx context.Context, toolFinish *sdp.ToolFinish) { +} + +var _ GatewayMessageHandler = (*StoreEverythingHandler)(nil) + +// A handler that stores all the items and edges it receives +type StoreEverythingHandler struct { + Items []*sdp.Item + Edges []*sdp.Edge + + NoopGatewayMessageHandler +} + +func (s *StoreEverythingHandler) NewItem(ctx context.Context, item *sdp.Item) { + s.Items = append(s.Items, item) +} + +func (s *StoreEverythingHandler) NewEdge(ctx context.Context, edge *sdp.Edge) { + s.Edges = append(s.Edges, edge) +} + +var _ GatewayMessageHandler = (*WaitForAllQueriesHandler)(nil) + +// A Handler that waits for all queries to be done then calls a callback +type WaitForAllQueriesHandler struct { + // A callback that will be called when all queries are done + DoneCallback func() + + StoreEverythingHandler +} + +func (w *WaitForAllQueriesHandler) Status(ctx context.Context, status *sdp.GatewayRequestStatus) { + if status.Done() { + w.DoneCallback() + } +} diff --git a/go/sdp-go/sdpws/utils.go b/go/sdp-go/sdpws/utils.go new file mode 100644 index 00000000..adc82a9b --- /dev/null +++ b/go/sdp-go/sdpws/utils.go @@ -0,0 +1,343 @@ +package sdpws + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/durationpb" +) + +// Sends a query on the websocket connection without waiting for responses. Use +// the `Wait()` method to wait for completion of requests based on their UUID +func (c *Client) SendQuery(ctx context.Context, q *sdp.Query) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("query", q).Trace("writing query to websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_Query{ + Query: q, + }, + MinStatusInterval: durationpb.New(time.Second), + }) + if err != nil { + // c.send already aborts + // c.abort(ctx, err) + return fmt.Errorf("error sending query: %w", err) + } + return nil +} + +// QueryOne runs a query and waits for it to complete, returning only the items +// that were found as direct results to the top-level query. +func (c *Client) QueryOne(ctx context.Context, q *sdp.Query) ([]*sdp.Item, error) { + if c.Closed() { + return nil, errors.New("client closed") + } + + u := uuid.UUID(q.GetUUID()) + + r := c.createRequestChan(u) + defer c.finishRequestChan(u) + + err := c.SendQuery(ctx, q) + if err != nil { + // c.SendQuery already aborts + // c.abort(ctx, err) + return nil, err + } + + items := make([]*sdp.Item, 0) + + var otherErr *sdp.QueryError +readLoop: + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context canceled: %w", ctx.Err()) + case resp, more := <-r: + if !more { + break readLoop + } + switch resp.GetResponseType().(type) { + case *sdp.GatewayResponse_NewItem: + item := resp.GetNewItem() + log.WithContext(ctx).WithField("query", q).WithField("item", item).Trace("received item") + items = append(items, item) + case *sdp.GatewayResponse_QueryError: + qe := resp.GetQueryError() + log.WithContext(ctx).WithField("query", q).WithField("queryError", qe).Trace("received query error") + switch qe.GetErrorType() { + case sdp.QueryError_OTHER, sdp.QueryError_TIMEOUT, sdp.QueryError_NOSCOPE: + // record that we received an error, but continue reading + // if we receive any item, mapping was still successful + otherErr = qe + continue readLoop + case sdp.QueryError_NOTFOUND: + // never record not found as an error + continue readLoop + } + case *sdp.GatewayResponse_QueryStatus: + qs := resp.GetQueryStatus() + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.String("ovm.sdp.lastQueryStatus", qs.String())) + log.WithContext(ctx).WithField("query", q).WithField("queryStatus", qs).Trace("received query status") + switch qs.GetStatus() { //nolint:exhaustive // we dont care about sdp.QueryStatus_UNSPECIFIED, sdp.QueryStatus_STARTED + case sdp.QueryStatus_FINISHED: + break readLoop + case sdp.QueryStatus_CANCELLED: + return nil, errors.New("query cancelled") + case sdp.QueryStatus_ERRORED: + // if we already received items, we can ignore the error + if len(items) == 0 && otherErr != nil { + err = fmt.Errorf("query errored: %w", otherErr) + // query errors should not abort the connection + // c.abort(ctx, err) + return nil, err + } + break readLoop + } + default: + log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") + } + } + } + + return items, nil +} + +// TODO: CancelQuery +// TODO: Expand + +// Sends a LoadSnapshot request on the websocket connection without waiting for +// a response. +func (c *Client) SendLoadSnapshot(ctx context.Context, s *sdp.LoadSnapshot) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("snapshot", s).Trace("loading snapshot via websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_LoadSnapshot{ + LoadSnapshot: s, + }, + }) + if err != nil { + return fmt.Errorf("error sending load snapshot: %w", err) + } + return nil +} + +// Load a snapshot and wait for it to complete. This will return the +// SnapshotLoadResult from the gateway. A separate error is only returned when +// there is a communication error. Logic errors from the gateway are reported +// through the returned SnapshotLoadResult. +func (c *Client) LoadSnapshot(ctx context.Context, id uuid.UUID) (*sdp.SnapshotLoadResult, error) { + if c.Closed() { + return nil, errors.New("client closed") + } + + u := uuid.New() + s := &sdp.LoadSnapshot{ + UUID: id[:], + MsgID: u[:], + } + r := c.createRequestChan(u) + + err := c.SendLoadSnapshot(ctx, s) + if err != nil { + return nil, err + } + + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("context canceled: %w", ctx.Err()) + case resp, more := <-r: + if !more { + return nil, errors.New("request channel closed") + } + switch resp.GetResponseType().(type) { + case *sdp.GatewayResponse_SnapshotLoadResult: + slr := resp.GetSnapshotLoadResult() + log.WithContext(ctx).WithField("snapshot", s).WithField("snapshotLoadResult", slr).Trace("received snapshot load result") + return slr, nil + default: + log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") + return nil, errors.New("unexpected response") + } + } + } +} + +// Sends a StoreSnapshot request on the websocket connection without waiting for +// a response. +func (c *Client) SendStoreSnapshot(ctx context.Context, s *sdp.StoreSnapshot) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("snapshot", s).Trace("storing snapshot via websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_StoreSnapshot{ + StoreSnapshot: s, + }, + }) + if err != nil { + return fmt.Errorf("error sending store snapshot: %w", err) + } + return nil +} + +// Store a snapshot and wait for it to complete, returning the UUID of the +// snapshot that was created. +func (c *Client) StoreSnapshot(ctx context.Context, name, description string) (uuid.UUID, error) { + if c.Closed() { + return uuid.UUID{}, errors.New("client closed") + } + + u := uuid.New() + s := &sdp.StoreSnapshot{ + Name: name, + Description: description, + MsgID: u[:], + } + r := c.createRequestChan(u) + + err := c.SendStoreSnapshot(ctx, s) + if err != nil { + return uuid.UUID{}, err + } + + for { + select { + case <-ctx.Done(): + return uuid.UUID{}, fmt.Errorf("context canceled: %w", ctx.Err()) + case resp, more := <-r: + if !more { + return uuid.UUID{}, errors.New("request channel closed") + } + switch resp.GetResponseType().(type) { + case *sdp.GatewayResponse_SnapshotStoreResult: + ssr := resp.GetSnapshotStoreResult() + log.WithContext(ctx).WithField("Snapshot", s).WithField("snapshotStoreResult", ssr).Trace("received snapshot store result") + if ssr.GetSuccess() { + return uuid.UUID(ssr.GetSnapshotID()), nil + } + return uuid.UUID{}, fmt.Errorf("snapshot store failed: %v", ssr.GetErrorMessage()) + default: + log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") + return uuid.UUID{}, errors.New("unexpected response") + } + } + } +} + +func (c *Client) SendLoadBookmark(ctx context.Context, b *sdp.LoadBookmark) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("bookmark", b).Trace("loading bookmark via websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_LoadBookmark{ + LoadBookmark: b, + }, + }) + if err != nil { + return fmt.Errorf("error sending load bookmark: %w", err) + } + return nil +} + +// Sends a StoreBookmark request on the websocket connection without waiting for +// a response. +func (c *Client) SendStoreBookmark(ctx context.Context, b *sdp.StoreBookmark) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("bookmark", b).Trace("storing bookmark via websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_StoreBookmark{ + StoreBookmark: b, + }, + }) + if err != nil { + return fmt.Errorf("error sending store bookmark: %w", err) + } + return nil +} + +// Store a bookmark and wait for it to complete, returning the UUID of the +// bookmark that was created. +func (c *Client) StoreBookmark(ctx context.Context, name, description string, isSystem bool) (uuid.UUID, error) { + if c.Closed() { + return uuid.UUID{}, errors.New("client closed") + } + + u := uuid.New() + b := &sdp.StoreBookmark{ + Name: name, + Description: description, + MsgID: u[:], + IsSystem: true, + } + r := c.createRequestChan(u) + + err := c.SendStoreBookmark(ctx, b) + if err != nil { + return uuid.UUID{}, err + } + + for { + select { + case <-ctx.Done(): + return uuid.UUID{}, fmt.Errorf("context canceled: %w", ctx.Err()) + case resp, more := <-r: + if !more { + return uuid.UUID{}, errors.New("request channel closed") + } + switch resp.GetResponseType().(type) { + case *sdp.GatewayResponse_BookmarkStoreResult: + bsr := resp.GetBookmarkStoreResult() + log.WithContext(ctx).WithField("bookmark", b).WithField("bookmarkStoreResult", bsr).Trace("received bookmark store result") + if bsr.GetSuccess() { + return uuid.UUID(bsr.GetBookmarkID()), nil + } + return uuid.UUID{}, fmt.Errorf("bookmark store failed: %v", bsr.GetErrorMessage()) + default: + log.WithContext(ctx).WithField("response", resp).WithField("responseType", fmt.Sprintf("%T", resp.GetResponseType())).Warn("unexpected response") + return uuid.UUID{}, errors.New("unexpected response") + } + } + } +} + +// TODO: LoadBookmark + +// send chatMessage to the assistant +func (c *Client) SendChatMessage(ctx context.Context, m *sdp.ChatMessage) error { + if c.Closed() { + return errors.New("client closed") + } + + log.WithContext(ctx).WithField("message", m).Trace("sending chat message via websocket") + err := c.send(ctx, &sdp.GatewayRequest{ + RequestType: &sdp.GatewayRequest_ChatMessage{ + ChatMessage: m, + }, + }) + if err != nil { + return fmt.Errorf("error sending chat message: %w", err) + } + return nil +} diff --git a/go/sdp-go/signal.pb.go b/go/sdp-go/signal.pb.go new file mode 100644 index 00000000..aaa0e88b --- /dev/null +++ b/go/sdp-go/signal.pb.go @@ -0,0 +1,1286 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: signal.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AddSignalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user facing properties of the signal + Properties *SignalProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + // UUID of the change this signal is associated with + ChangeUUID []byte `protobuf:"bytes,2,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddSignalRequest) Reset() { + *x = AddSignalRequest{} + mi := &file_signal_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddSignalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddSignalRequest) ProtoMessage() {} + +func (x *AddSignalRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[0] + 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 AddSignalRequest.ProtoReflect.Descriptor instead. +func (*AddSignalRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{0} +} + +func (x *AddSignalRequest) GetProperties() *SignalProperties { + if x != nil { + return x.Properties + } + return nil +} + +func (x *AddSignalRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type AddSignalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signal *Signal `protobuf:"bytes,1,opt,name=signal,proto3" json:"signal,omitempty"` // The signal that was added + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddSignalResponse) Reset() { + *x = AddSignalResponse{} + mi := &file_signal_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddSignalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddSignalResponse) ProtoMessage() {} + +func (x *AddSignalResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[1] + 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 AddSignalResponse.ProtoReflect.Descriptor instead. +func (*AddSignalResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{1} +} + +func (x *AddSignalResponse) GetSignal() *Signal { + if x != nil { + return x.Signal + } + return nil +} + +type GetSignalsByChangeExternalIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` // UUID of the change + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSignalsByChangeExternalIDRequest) Reset() { + *x = GetSignalsByChangeExternalIDRequest{} + mi := &file_signal_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSignalsByChangeExternalIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignalsByChangeExternalIDRequest) ProtoMessage() {} + +func (x *GetSignalsByChangeExternalIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[2] + 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 GetSignalsByChangeExternalIDRequest.ProtoReflect.Descriptor instead. +func (*GetSignalsByChangeExternalIDRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{2} +} + +func (x *GetSignalsByChangeExternalIDRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetSignalsByChangeExternalIDResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // List of all signals associated with the change + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSignalsByChangeExternalIDResponse) Reset() { + *x = GetSignalsByChangeExternalIDResponse{} + mi := &file_signal_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSignalsByChangeExternalIDResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSignalsByChangeExternalIDResponse) ProtoMessage() {} + +func (x *GetSignalsByChangeExternalIDResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[3] + 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 GetSignalsByChangeExternalIDResponse.ProtoReflect.Descriptor instead. +func (*GetSignalsByChangeExternalIDResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{3} +} + +func (x *GetSignalsByChangeExternalIDResponse) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +type GetChangeOverviewSignalsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeOverviewSignalsRequest) Reset() { + *x = GetChangeOverviewSignalsRequest{} + mi := &file_signal_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeOverviewSignalsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeOverviewSignalsRequest) ProtoMessage() {} + +func (x *GetChangeOverviewSignalsRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[4] + 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 GetChangeOverviewSignalsRequest.ProtoReflect.Descriptor instead. +func (*GetChangeOverviewSignalsRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{4} +} + +func (x *GetChangeOverviewSignalsRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetChangeOverviewSignalsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` + // The aggregated value for all categories in the change, calculated by AggregateSignalScores. + Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetChangeOverviewSignalsResponse) Reset() { + *x = GetChangeOverviewSignalsResponse{} + mi := &file_signal_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetChangeOverviewSignalsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetChangeOverviewSignalsResponse) ProtoMessage() {} + +func (x *GetChangeOverviewSignalsResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[5] + 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 GetChangeOverviewSignalsResponse.ProtoReflect.Descriptor instead. +func (*GetChangeOverviewSignalsResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{5} +} + +func (x *GetChangeOverviewSignalsResponse) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +func (x *GetChangeOverviewSignalsResponse) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +type ItemAggregation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A sorted list of signals for this item, by category. + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // Corresponds to []*sdp.Signal + // The aggregated value for this item, calculated by AggregateSignalScores. + Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItemAggregation) Reset() { + *x = ItemAggregation{} + mi := &file_signal_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItemAggregation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemAggregation) ProtoMessage() {} + +func (x *ItemAggregation) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[6] + 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 ItemAggregation.ProtoReflect.Descriptor instead. +func (*ItemAggregation) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{6} +} + +func (x *ItemAggregation) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +func (x *ItemAggregation) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +type GetItemSignalsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalsRequest) Reset() { + *x = GetItemSignalsRequest{} + mi := &file_signal_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalsRequest) ProtoMessage() {} + +func (x *GetItemSignalsRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[7] + 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 GetItemSignalsRequest.ProtoReflect.Descriptor instead. +func (*GetItemSignalsRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{7} +} + +func (x *GetItemSignalsRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type GetItemSignalsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A map of Globally Unique Names (GUNs) of items to their aggregation of signals. + // These are by category, sorted by the signal value, ascending. + // We also include a value for this GUN, which is calculated by AggregateSignalScores + ItemAggregations map[string]*ItemAggregation `protobuf:"bytes,1,rep,name=itemAggregations,proto3" json:"itemAggregations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalsResponse) Reset() { + *x = GetItemSignalsResponse{} + mi := &file_signal_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalsResponse) ProtoMessage() {} + +func (x *GetItemSignalsResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[8] + 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 GetItemSignalsResponse.ProtoReflect.Descriptor instead. +func (*GetItemSignalsResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{8} +} + +func (x *GetItemSignalsResponse) GetItemAggregations() map[string]*ItemAggregation { + if x != nil { + return x.ItemAggregations + } + return nil +} + +type GetItemSignalsRequestV2 struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalsRequestV2) Reset() { + *x = GetItemSignalsRequestV2{} + mi := &file_signal_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalsRequestV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalsRequestV2) ProtoMessage() {} + +func (x *GetItemSignalsRequestV2) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[9] + 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 GetItemSignalsRequestV2.ProtoReflect.Descriptor instead. +func (*GetItemSignalsRequestV2) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{9} +} + +func (x *GetItemSignalsRequestV2) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +type ItemAggregationV2 struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A sorted list of signals for this item, by category. + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` // Corresponds to []*sdp.Signal + // The aggregated value for this item, calculated by AggregateSignalScores. + Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` // Corresponds to float64 + // It is the friendly item reference, taken from the resolve mapping queries. + // for handiness we wall back to the afterRef if the mappedRef is not available. + MappedRef *Reference `protobuf:"bytes,3,opt,name=mappedRef,proto3" json:"mappedRef,omitempty"` + // This it the item reference from after, it is used to look up the item in GetItemSignalDetailsRequest. + AfterRef *Reference `protobuf:"bytes,4,opt,name=afterRef,proto3" json:"afterRef,omitempty"` + // status is the status of the item, e.g. "added", "modified", "deleted". + Status ItemDiffStatus `protobuf:"varint,5,opt,name=status,proto3,enum=changes.ItemDiffStatus" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItemAggregationV2) Reset() { + *x = ItemAggregationV2{} + mi := &file_signal_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItemAggregationV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItemAggregationV2) ProtoMessage() {} + +func (x *ItemAggregationV2) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[10] + 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 ItemAggregationV2.ProtoReflect.Descriptor instead. +func (*ItemAggregationV2) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{10} +} + +func (x *ItemAggregationV2) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +func (x *ItemAggregationV2) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *ItemAggregationV2) GetMappedRef() *Reference { + if x != nil { + return x.MappedRef + } + return nil +} + +func (x *ItemAggregationV2) GetAfterRef() *Reference { + if x != nil { + return x.AfterRef + } + return nil +} + +func (x *ItemAggregationV2) GetStatus() ItemDiffStatus { + if x != nil { + return x.Status + } + return ItemDiffStatus_ITEM_DIFF_STATUS_UNSPECIFIED +} + +type GetItemSignalsResponseV2 struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A map of Globally Unique Names (GUNs) of items to their aggregation of signals. + // These are by category, sorted by the signal value, ascending. + // We also include a value for this GUN, which is calculated by AggregateSignalScores + ItemAggregations []*ItemAggregationV2 `protobuf:"bytes,1,rep,name=itemAggregations,proto3" json:"itemAggregations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalsResponseV2) Reset() { + *x = GetItemSignalsResponseV2{} + mi := &file_signal_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalsResponseV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalsResponseV2) ProtoMessage() {} + +func (x *GetItemSignalsResponseV2) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[11] + 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 GetItemSignalsResponseV2.ProtoReflect.Descriptor instead. +func (*GetItemSignalsResponseV2) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{11} +} + +func (x *GetItemSignalsResponseV2) GetItemAggregations() []*ItemAggregationV2 { + if x != nil { + return x.ItemAggregations + } + return nil +} + +// Get all custom signals for a change by its external ID and category. They are NOT associated with any item. +type GetCustomSignalsByCategoryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCustomSignalsByCategoryRequest) Reset() { + *x = GetCustomSignalsByCategoryRequest{} + mi := &file_signal_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCustomSignalsByCategoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCustomSignalsByCategoryRequest) ProtoMessage() {} + +func (x *GetCustomSignalsByCategoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[12] + 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 GetCustomSignalsByCategoryRequest.ProtoReflect.Descriptor instead. +func (*GetCustomSignalsByCategoryRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{12} +} + +func (x *GetCustomSignalsByCategoryRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +func (x *GetCustomSignalsByCategoryRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +// array of signals +type GetCustomSignalsByCategoryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCustomSignalsByCategoryResponse) Reset() { + *x = GetCustomSignalsByCategoryResponse{} + mi := &file_signal_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCustomSignalsByCategoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCustomSignalsByCategoryResponse) ProtoMessage() {} + +func (x *GetCustomSignalsByCategoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCustomSignalsByCategoryResponse.ProtoReflect.Descriptor instead. +func (*GetCustomSignalsByCategoryResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{13} +} + +func (x *GetCustomSignalsByCategoryResponse) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +type GetItemSignalDetailsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The item for which we want to get the details of the signals. + // it is the reference of the terraform item before/after. + // NB it is not the lookup item from resolve mapping queries. + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + // The UUID of the change this item is associated with. + ChangeUUID []byte `protobuf:"bytes,2,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` + // The category of the signals we want to get. This is used to filter the signals by category. + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalDetailsRequest) Reset() { + *x = GetItemSignalDetailsRequest{} + mi := &file_signal_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalDetailsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalDetailsRequest) ProtoMessage() {} + +func (x *GetItemSignalDetailsRequest) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetItemSignalDetailsRequest.ProtoReflect.Descriptor instead. +func (*GetItemSignalDetailsRequest) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{14} +} + +func (x *GetItemSignalDetailsRequest) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +func (x *GetItemSignalDetailsRequest) GetChangeUUID() []byte { + if x != nil { + return x.ChangeUUID + } + return nil +} + +func (x *GetItemSignalDetailsRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +type GetItemSignalDetailsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signals []*Signal `protobuf:"bytes,1,rep,name=signals,proto3" json:"signals,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetItemSignalDetailsResponse) Reset() { + *x = GetItemSignalDetailsResponse{} + mi := &file_signal_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetItemSignalDetailsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetItemSignalDetailsResponse) ProtoMessage() {} + +func (x *GetItemSignalDetailsResponse) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[15] + 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 GetItemSignalDetailsResponse.ProtoReflect.Descriptor instead. +func (*GetItemSignalDetailsResponse) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{15} +} + +func (x *GetItemSignalDetailsResponse) GetSignals() []*Signal { + if x != nil { + return x.Signals + } + return nil +} + +type SignalMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalMetadata) Reset() { + *x = SignalMetadata{} + mi := &file_signal_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalMetadata) ProtoMessage() {} + +func (x *SignalMetadata) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[16] + 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 SignalMetadata.ProtoReflect.Descriptor instead. +func (*SignalMetadata) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{16} +} + +type SignalProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // user-supplied properties of this signal + // A short name for the signal + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // This is float64 value, representing the signal's value. -5 to +5. +5 is very high / strong, 0 is neutral, -5 is very low / weak. + Value float64 `protobuf:"fixed64,2,opt,name=value,proto3" json:"value,omitempty"` + // A one sentence description of the signal, it could be activity in routineness, or another + // descriptive text + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // A category for the signal, e.g. "routineness", "anomaly", etc. How it will be grouped in the UI. + Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"` + // This is poorly named, this is the after item reference, equivalent to afterRef in ItemAggregationV2 + // in the signals table this is the afterRef column. It is not the mappedRef / friendly item reference. + Item *Reference `protobuf:"bytes,5,opt,name=item,proto3,oneof" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignalProperties) Reset() { + *x = SignalProperties{} + mi := &file_signal_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignalProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignalProperties) ProtoMessage() {} + +func (x *SignalProperties) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignalProperties.ProtoReflect.Descriptor instead. +func (*SignalProperties) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{17} +} + +func (x *SignalProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SignalProperties) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *SignalProperties) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SignalProperties) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *SignalProperties) GetItem() *Reference { + if x != nil { + return x.Item + } + return nil +} + +// we mimic the layout of the changes object here, because there are 2 parts +// to a signal: the machine-generated metadata and the user-supplied properties. +type Signal struct { + state protoimpl.MessageState `protogen:"open.v1"` + // machine-generated metadata of this signal + Metadata *SignalMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // user-supplied properties of this signal + Properties *SignalProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Signal) Reset() { + *x = Signal{} + mi := &file_signal_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Signal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Signal) ProtoMessage() {} + +func (x *Signal) ProtoReflect() protoreflect.Message { + mi := &file_signal_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Signal.ProtoReflect.Descriptor instead. +func (*Signal) Descriptor() ([]byte, []int) { + return file_signal_proto_rawDescGZIP(), []int{18} +} + +func (x *Signal) GetMetadata() *SignalMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Signal) GetProperties() *SignalProperties { + if x != nil { + return x.Properties + } + 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 = "" + + "\n" + + "\fsignal.proto\x12\x06signal\x1a\rchanges.proto\x1a\vitems.proto\"l\n" + + "\x10AddSignalRequest\x128\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x18.signal.SignalPropertiesR\n" + + "properties\x12\x1e\n" + + "\n" + + "changeUUID\x18\x02 \x01(\fR\n" + + "changeUUID\";\n" + + "\x11AddSignalResponse\x12&\n" + + "\x06signal\x18\x01 \x01(\v2\x0e.signal.SignalR\x06signal\"E\n" + + "#GetSignalsByChangeExternalIDRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"P\n" + + "$GetSignalsByChangeExternalIDResponse\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"A\n" + + "\x1fGetChangeOverviewSignalsRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"b\n" + + " GetChangeOverviewSignalsResponse\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + + "\x05value\x18\x02 \x01(\x01R\x05value\"Q\n" + + "\x0fItemAggregation\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + + "\x05value\x18\x02 \x01(\x01R\x05value\"7\n" + + "\x15GetItemSignalsRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"\xd8\x01\n" + + "\x16GetItemSignalsResponse\x12`\n" + + "\x10itemAggregations\x18\x01 \x03(\v24.signal.GetItemSignalsResponse.ItemAggregationsEntryR\x10itemAggregations\x1a\\\n" + + "\x15ItemAggregationsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12-\n" + + "\x05value\x18\x02 \x01(\v2\x17.signal.ItemAggregationR\x05value:\x028\x01\"9\n" + + "\x17GetItemSignalsRequestV2\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\"\xd6\x01\n" + + "\x11ItemAggregationV2\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\x12\x14\n" + + "\x05value\x18\x02 \x01(\x01R\x05value\x12(\n" + + "\tmappedRef\x18\x03 \x01(\v2\n" + + ".ReferenceR\tmappedRef\x12&\n" + + "\bafterRef\x18\x04 \x01(\v2\n" + + ".ReferenceR\bafterRef\x12/\n" + + "\x06status\x18\x05 \x01(\x0e2\x17.changes.ItemDiffStatusR\x06status\"a\n" + + "\x18GetItemSignalsResponseV2\x12E\n" + + "\x10itemAggregations\x18\x01 \x03(\v2\x19.signal.ItemAggregationV2R\x10itemAggregations\"_\n" + + "!GetCustomSignalsByCategoryRequest\x12\x1e\n" + + "\n" + + "changeUUID\x18\x01 \x01(\fR\n" + + "changeUUID\x12\x1a\n" + + "\bcategory\x18\x02 \x01(\tR\bcategory\"N\n" + + "\"GetCustomSignalsByCategoryResponse\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"y\n" + + "\x1bGetItemSignalDetailsRequest\x12\x1e\n" + + "\x04item\x18\x01 \x01(\v2\n" + + ".ReferenceR\x04item\x12\x1e\n" + + "\n" + + "changeUUID\x18\x02 \x01(\fR\n" + + "changeUUID\x12\x1a\n" + + "\bcategory\x18\x03 \x01(\tR\bcategory\"H\n" + + "\x1cGetItemSignalDetailsResponse\x12(\n" + + "\asignals\x18\x01 \x03(\v2\x0e.signal.SignalR\asignals\"\x10\n" + + "\x0eSignalMetadata\"\xa8\x01\n" + + "\x10SignalProperties\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\x01R\x05value\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1a\n" + + "\bcategory\x18\x04 \x01(\tR\bcategory\x12#\n" + + "\x04item\x18\x05 \x01(\v2\n" + + ".ReferenceH\x00R\x04item\x88\x01\x01B\a\n" + + "\x05_item\"v\n" + + "\x06Signal\x122\n" + + "\bmetadata\x18\x01 \x01(\v2\x16.signal.SignalMetadataR\bmetadata\x128\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x18.signal.SignalPropertiesR\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" + + "\x18GetChangeOverviewSignals\x12'.signal.GetChangeOverviewSignalsRequest\x1a(.signal.GetChangeOverviewSignalsResponse\x12O\n" + + "\x0eGetItemSignals\x12\x1d.signal.GetItemSignalsRequest\x1a\x1e.signal.GetItemSignalsResponse\x12U\n" + + "\x10GetItemSignalsV2\x12\x1f.signal.GetItemSignalsRequestV2\x1a .signal.GetItemSignalsResponseV2\x12s\n" + + "\x1aGetCustomSignalsByCategory\x12).signal.GetCustomSignalsByCategoryRequest\x1a*.signal.GetCustomSignalsByCategoryResponse\x12a\n" + + "\x14GetItemSignalDetails\x12#.signal.GetItemSignalDetailsRequest\x1a$.signal.GetItemSignalDetailsResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_signal_proto_rawDescOnce sync.Once + file_signal_proto_rawDescData []byte +) + +func file_signal_proto_rawDescGZIP() []byte { + file_signal_proto_rawDescOnce.Do(func() { + file_signal_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_signal_proto_rawDesc), len(file_signal_proto_rawDesc))) + }) + return file_signal_proto_rawDescData +} + +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 + (*GetSignalsByChangeExternalIDRequest)(nil), // 2: signal.GetSignalsByChangeExternalIDRequest + (*GetSignalsByChangeExternalIDResponse)(nil), // 3: signal.GetSignalsByChangeExternalIDResponse + (*GetChangeOverviewSignalsRequest)(nil), // 4: signal.GetChangeOverviewSignalsRequest + (*GetChangeOverviewSignalsResponse)(nil), // 5: signal.GetChangeOverviewSignalsResponse + (*ItemAggregation)(nil), // 6: signal.ItemAggregation + (*GetItemSignalsRequest)(nil), // 7: signal.GetItemSignalsRequest + (*GetItemSignalsResponse)(nil), // 8: signal.GetItemSignalsResponse + (*GetItemSignalsRequestV2)(nil), // 9: signal.GetItemSignalsRequestV2 + (*ItemAggregationV2)(nil), // 10: signal.ItemAggregationV2 + (*GetItemSignalsResponseV2)(nil), // 11: signal.GetItemSignalsResponseV2 + (*GetCustomSignalsByCategoryRequest)(nil), // 12: signal.GetCustomSignalsByCategoryRequest + (*GetCustomSignalsByCategoryResponse)(nil), // 13: signal.GetCustomSignalsByCategoryResponse + (*GetItemSignalDetailsRequest)(nil), // 14: signal.GetItemSignalDetailsRequest + (*GetItemSignalDetailsResponse)(nil), // 15: signal.GetItemSignalDetailsResponse + (*SignalMetadata)(nil), // 16: signal.SignalMetadata + (*SignalProperties)(nil), // 17: signal.SignalProperties + (*Signal)(nil), // 18: signal.Signal + (*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 + 18, // 1: signal.AddSignalResponse.signal:type_name -> signal.Signal + 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 + 20, // 5: signal.GetItemSignalsResponse.itemAggregations:type_name -> signal.GetItemSignalsResponse.ItemAggregationsEntry + 18, // 6: signal.ItemAggregationV2.signals:type_name -> signal.Signal + 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 + 21, // 12: signal.GetItemSignalDetailsRequest.item:type_name -> Reference + 18, // 13: signal.GetItemSignalDetailsResponse.signals:type_name -> signal.Signal + 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 + 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() } +func file_signal_proto_init() { + if File_signal_proto != nil { + return + } + file_changes_proto_init() + file_items_proto_init() + file_signal_proto_msgTypes[17].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_signal_proto_rawDesc), len(file_signal_proto_rawDesc)), + NumEnums: 0, + NumMessages: 21, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_signal_proto_goTypes, + DependencyIndexes: file_signal_proto_depIdxs, + MessageInfos: file_signal_proto_msgTypes, + }.Build() + File_signal_proto = out.File + file_signal_proto_goTypes = nil + file_signal_proto_depIdxs = nil +} diff --git a/go/sdp-go/signals.go b/go/sdp-go/signals.go new file mode 100644 index 00000000..bd884cd0 --- /dev/null +++ b/go/sdp-go/signals.go @@ -0,0 +1,10 @@ +package sdp + +type SignalCategoryName string + +// SignalCategoryName constants represent the predefined categories for signals. +// if you add a new category, please also update the cli command "submit-signal" @ cli/cmd/changes_submit_signal.go +const ( + SignalCategoryNameCustom SignalCategoryName = "Custom" + SignalCategoryNameRoutine SignalCategoryName = "Routine" +) diff --git a/go/sdp-go/snapshots.go b/go/sdp-go/snapshots.go new file mode 100644 index 00000000..8bbaf388 --- /dev/null +++ b/go/sdp-go/snapshots.go @@ -0,0 +1,41 @@ +package sdp + +import "github.com/google/uuid" + +// ToMap converts a Snapshot to a map for serialization. +func (s *Snapshot) ToMap() map[string]any { + return map[string]any{ + "metadata": s.GetMetadata().ToMap(), + "properties": s.GetProperties().ToMap(), + } +} + +// ToMap converts SnapshotMetadata to a map for serialization. +func (sm *SnapshotMetadata) ToMap() map[string]any { + return map[string]any{ + "UUID": stringFromUuidBytes(sm.GetUUID()), + "created": sm.GetCreated().AsTime(), + } +} + +// GetUUIDParsed returns the parsed UUID from the SnapshotMetadata, or nil if invalid. +func (sm *SnapshotMetadata) GetUUIDParsed() *uuid.UUID { + if sm == nil { + return nil + } + u, err := uuid.FromBytes(sm.GetUUID()) + if err != nil { + return nil + } + return &u +} + +// ToMap converts SnapshotProperties to a map for serialization. +func (sp *SnapshotProperties) ToMap() map[string]any { + return map[string]any{ + "name": sp.GetName(), + "description": sp.GetDescription(), + "queries": sp.GetQueries(), + "Items": sp.GetItems(), + } +} diff --git a/go/sdp-go/snapshots.pb.go b/go/sdp-go/snapshots.pb.go new file mode 100644 index 00000000..01509b98 --- /dev/null +++ b/go/sdp-go/snapshots.pb.go @@ -0,0 +1,1011 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: snapshots.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Snapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *SnapshotMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Properties *SnapshotProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Snapshot) Reset() { + *x = Snapshot{} + mi := &file_snapshots_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Snapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Snapshot) ProtoMessage() {} + +func (x *Snapshot) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[0] + 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 Snapshot.ProtoReflect.Descriptor instead. +func (*Snapshot) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{0} +} + +func (x *Snapshot) GetMetadata() *SnapshotMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Snapshot) GetProperties() *SnapshotProperties { + if x != nil { + return x.Properties + } + return nil +} + +type SnapshotProperties struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name of this snapshot + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // description of this snapshot + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // queries that make up the snapshot + Queries []*Query `protobuf:"bytes,3,rep,name=queries,proto3" json:"queries,omitempty"` + // items stored in the snapshot + Items []*Item `protobuf:"bytes,5,rep,name=items,proto3" json:"items,omitempty"` + // edges stored in the snapshot + Edges []*Edge `protobuf:"bytes,6,rep,name=edges,proto3" json:"edges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SnapshotProperties) Reset() { + *x = SnapshotProperties{} + mi := &file_snapshots_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SnapshotProperties) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotProperties) ProtoMessage() {} + +func (x *SnapshotProperties) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[1] + 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 SnapshotProperties.ProtoReflect.Descriptor instead. +func (*SnapshotProperties) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{1} +} + +func (x *SnapshotProperties) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SnapshotProperties) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SnapshotProperties) GetQueries() []*Query { + if x != nil { + return x.Queries + } + return nil +} + +func (x *SnapshotProperties) GetItems() []*Item { + if x != nil { + return x.Items + } + return nil +} + +func (x *SnapshotProperties) GetEdges() []*Edge { + if x != nil { + return x.Edges + } + return nil +} + +type SnapshotMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // unique id to identify this snapshot + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + // timestamp when this snapshot was created + Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SnapshotMetadata) Reset() { + *x = SnapshotMetadata{} + mi := &file_snapshots_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SnapshotMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnapshotMetadata) ProtoMessage() {} + +func (x *SnapshotMetadata) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[2] + 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 SnapshotMetadata.ProtoReflect.Descriptor instead. +func (*SnapshotMetadata) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{2} +} + +func (x *SnapshotMetadata) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *SnapshotMetadata) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + +// lists all snapshots +type ListSnapshotsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSnapshotsRequest) Reset() { + *x = ListSnapshotsRequest{} + mi := &file_snapshots_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotsRequest) ProtoMessage() {} + +func (x *ListSnapshotsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[3] + 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 ListSnapshotsRequest.ProtoReflect.Descriptor instead. +func (*ListSnapshotsRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{3} +} + +type ListSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the list of all snapshots + Snapshots []*Snapshot `protobuf:"bytes,1,rep,name=snapshots,proto3" json:"snapshots,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSnapshotResponse) Reset() { + *x = ListSnapshotResponse{} + mi := &file_snapshots_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotResponse) ProtoMessage() {} + +func (x *ListSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[4] + 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 ListSnapshotResponse.ProtoReflect.Descriptor instead. +func (*ListSnapshotResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{4} +} + +func (x *ListSnapshotResponse) GetSnapshots() []*Snapshot { + if x != nil { + return x.Snapshots + } + return nil +} + +// creates a new snapshot +type CreateSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // properties of the new snapshot + Properties *SnapshotProperties `protobuf:"bytes,1,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSnapshotRequest) Reset() { + *x = CreateSnapshotRequest{} + mi := &file_snapshots_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSnapshotRequest) ProtoMessage() {} + +func (x *CreateSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[5] + 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 CreateSnapshotRequest.ProtoReflect.Descriptor instead. +func (*CreateSnapshotRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateSnapshotRequest) GetProperties() *SnapshotProperties { + if x != nil { + return x.Properties + } + return nil +} + +type CreateSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the newly created snapshot + Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSnapshotResponse) Reset() { + *x = CreateSnapshotResponse{} + mi := &file_snapshots_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSnapshotResponse) ProtoMessage() {} + +func (x *CreateSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[6] + 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 CreateSnapshotResponse.ProtoReflect.Descriptor instead. +func (*CreateSnapshotResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateSnapshotResponse) GetSnapshot() *Snapshot { + if x != nil { + return x.Snapshot + } + return nil +} + +// get the details of a specific snapshot +type GetSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSnapshotRequest) Reset() { + *x = GetSnapshotRequest{} + mi := &file_snapshots_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSnapshotRequest) ProtoMessage() {} + +func (x *GetSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[7] + 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 GetSnapshotRequest.ProtoReflect.Descriptor instead. +func (*GetSnapshotRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{7} +} + +func (x *GetSnapshotRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type GetSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSnapshotResponse) Reset() { + *x = GetSnapshotResponse{} + mi := &file_snapshots_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSnapshotResponse) ProtoMessage() {} + +func (x *GetSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[8] + 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 GetSnapshotResponse.ProtoReflect.Descriptor instead. +func (*GetSnapshotResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{8} +} + +func (x *GetSnapshotResponse) GetSnapshot() *Snapshot { + if x != nil { + return x.Snapshot + } + return nil +} + +// updates the properties of an existing snapshot +type UpdateSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + Properties *SnapshotProperties `protobuf:"bytes,2,opt,name=properties,proto3" json:"properties,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSnapshotRequest) Reset() { + *x = UpdateSnapshotRequest{} + mi := &file_snapshots_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSnapshotRequest) ProtoMessage() {} + +func (x *UpdateSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[9] + 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 UpdateSnapshotRequest.ProtoReflect.Descriptor instead. +func (*UpdateSnapshotRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{9} +} + +func (x *UpdateSnapshotRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +func (x *UpdateSnapshotRequest) GetProperties() *SnapshotProperties { + if x != nil { + return x.Properties + } + return nil +} + +type UpdateSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // the updated version of the snapshot + Snapshot *Snapshot `protobuf:"bytes,1,opt,name=snapshot,proto3" json:"snapshot,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSnapshotResponse) Reset() { + *x = UpdateSnapshotResponse{} + mi := &file_snapshots_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSnapshotResponse) ProtoMessage() {} + +func (x *UpdateSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[10] + 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 UpdateSnapshotResponse.ProtoReflect.Descriptor instead. +func (*UpdateSnapshotResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateSnapshotResponse) GetSnapshot() *Snapshot { + if x != nil { + return x.Snapshot + } + return nil +} + +// deletes a given snapshot +type DeleteSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUID []byte `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSnapshotRequest) Reset() { + *x = DeleteSnapshotRequest{} + mi := &file_snapshots_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSnapshotRequest) ProtoMessage() {} + +func (x *DeleteSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[11] + 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 DeleteSnapshotRequest.ProtoReflect.Descriptor instead. +func (*DeleteSnapshotRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteSnapshotRequest) GetUUID() []byte { + if x != nil { + return x.UUID + } + return nil +} + +type DeleteSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSnapshotResponse) Reset() { + *x = DeleteSnapshotResponse{} + mi := &file_snapshots_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSnapshotResponse) ProtoMessage() {} + +func (x *DeleteSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[12] + 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 DeleteSnapshotResponse.ProtoReflect.Descriptor instead. +func (*DeleteSnapshotResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{12} +} + +// get the initial data +type GetInitialDataRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInitialDataRequest) Reset() { + *x = GetInitialDataRequest{} + mi := &file_snapshots_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInitialDataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInitialDataRequest) ProtoMessage() {} + +func (x *GetInitialDataRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInitialDataRequest.ProtoReflect.Descriptor instead. +func (*GetInitialDataRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{13} +} + +type GetInitialDataResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlastRadiusSnapshot *Snapshot `protobuf:"bytes,1,opt,name=blastRadiusSnapshot,proto3" json:"blastRadiusSnapshot,omitempty"` + ChangingItemsBookmark *Bookmark `protobuf:"bytes,2,opt,name=changingItemsBookmark,proto3" json:"changingItemsBookmark,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetInitialDataResponse) Reset() { + *x = GetInitialDataResponse{} + mi := &file_snapshots_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetInitialDataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetInitialDataResponse) ProtoMessage() {} + +func (x *GetInitialDataResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetInitialDataResponse.ProtoReflect.Descriptor instead. +func (*GetInitialDataResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{14} +} + +func (x *GetInitialDataResponse) GetBlastRadiusSnapshot() *Snapshot { + if x != nil { + return x.BlastRadiusSnapshot + } + return nil +} + +func (x *GetInitialDataResponse) GetChangingItemsBookmark() *Bookmark { + if x != nil { + return x.ChangingItemsBookmark + } + return nil +} + +type ListSnapshotsByGUNRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GloballyUniqueName string `protobuf:"bytes,1,opt,name=globallyUniqueName,proto3" json:"globallyUniqueName,omitempty"` + Pagination *PaginationRequest `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSnapshotsByGUNRequest) Reset() { + *x = ListSnapshotsByGUNRequest{} + mi := &file_snapshots_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotsByGUNRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotsByGUNRequest) ProtoMessage() {} + +func (x *ListSnapshotsByGUNRequest) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[15] + 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 ListSnapshotsByGUNRequest.ProtoReflect.Descriptor instead. +func (*ListSnapshotsByGUNRequest) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{15} +} + +func (x *ListSnapshotsByGUNRequest) GetGloballyUniqueName() string { + if x != nil { + return x.GloballyUniqueName + } + return "" +} + +func (x *ListSnapshotsByGUNRequest) GetPagination() *PaginationRequest { + if x != nil { + return x.Pagination + } + return nil +} + +type ListSnapshotsByGUNResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + UUIDs [][]byte `protobuf:"bytes,1,rep,name=UUIDs,proto3" json:"UUIDs,omitempty"` + Pagination *PaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSnapshotsByGUNResponse) Reset() { + *x = ListSnapshotsByGUNResponse{} + mi := &file_snapshots_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSnapshotsByGUNResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSnapshotsByGUNResponse) ProtoMessage() {} + +func (x *ListSnapshotsByGUNResponse) ProtoReflect() protoreflect.Message { + mi := &file_snapshots_proto_msgTypes[16] + 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 ListSnapshotsByGUNResponse.ProtoReflect.Descriptor instead. +func (*ListSnapshotsByGUNResponse) Descriptor() ([]byte, []int) { + return file_snapshots_proto_rawDescGZIP(), []int{16} +} + +func (x *ListSnapshotsByGUNResponse) GetUUIDs() [][]byte { + if x != nil { + return x.UUIDs + } + return nil +} + +func (x *ListSnapshotsByGUNResponse) GetPagination() *PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +var File_snapshots_proto protoreflect.FileDescriptor + +const file_snapshots_proto_rawDesc = "" + + "\n" + + "\x0fsnapshots.proto\x12\tsnapshots\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fbookmarks.proto\x1a\vitems.proto\x1a\n" + + "util.proto\"\x82\x01\n" + + "\bSnapshot\x127\n" + + "\bmetadata\x18\x01 \x01(\v2\x1b.snapshots.SnapshotMetadataR\bmetadata\x12=\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + + "properties\"\xac\x01\n" + + "\x12SnapshotProperties\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12 \n" + + "\aqueries\x18\x03 \x03(\v2\x06.QueryR\aqueries\x12\x1b\n" + + "\x05items\x18\x05 \x03(\v2\x05.ItemR\x05items\x12\x1b\n" + + "\x05edges\x18\x06 \x03(\v2\x05.EdgeR\x05edgesJ\x04\b\x04\x10\x05\"\\\n" + + "\x10SnapshotMetadata\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x124\n" + + "\acreated\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\acreated\"\x16\n" + + "\x14ListSnapshotsRequest\"I\n" + + "\x14ListSnapshotResponse\x121\n" + + "\tsnapshots\x18\x01 \x03(\v2\x13.snapshots.SnapshotR\tsnapshots\"V\n" + + "\x15CreateSnapshotRequest\x12=\n" + + "\n" + + "properties\x18\x01 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + + "properties\"I\n" + + "\x16CreateSnapshotResponse\x12/\n" + + "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"(\n" + + "\x12GetSnapshotRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"F\n" + + "\x13GetSnapshotResponse\x12/\n" + + "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"j\n" + + "\x15UpdateSnapshotRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\x12=\n" + + "\n" + + "properties\x18\x02 \x01(\v2\x1d.snapshots.SnapshotPropertiesR\n" + + "properties\"I\n" + + "\x16UpdateSnapshotResponse\x12/\n" + + "\bsnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\bsnapshot\"+\n" + + "\x15DeleteSnapshotRequest\x12\x12\n" + + "\x04UUID\x18\x01 \x01(\fR\x04UUID\"\x18\n" + + "\x16DeleteSnapshotResponse\"\x17\n" + + "\x15GetInitialDataRequest\"\xaa\x01\n" + + "\x16GetInitialDataResponse\x12E\n" + + "\x13blastRadiusSnapshot\x18\x01 \x01(\v2\x13.snapshots.SnapshotR\x13blastRadiusSnapshot\x12I\n" + + "\x15changingItemsBookmark\x18\x02 \x01(\v2\x13.bookmarks.BookmarkR\x15changingItemsBookmark\"\x7f\n" + + "\x19ListSnapshotsByGUNRequest\x12.\n" + + "\x12globallyUniqueName\x18\x01 \x01(\tR\x12globallyUniqueName\x122\n" + + "\n" + + "pagination\x18\x02 \x01(\v2\x12.PaginationRequestR\n" + + "pagination\"g\n" + + "\x1aListSnapshotsByGUNResponse\x12\x14\n" + + "\x05UUIDs\x18\x01 \x03(\fR\x05UUIDs\x123\n" + + "\n" + + "pagination\x18\x02 \x01(\v2\x13.PaginationResponseR\n" + + "pagination2\x9a\x04\n" + + "\x10SnapshotsService\x12Q\n" + + "\rListSnapshots\x12\x1f.snapshots.ListSnapshotsRequest\x1a\x1f.snapshots.ListSnapshotResponse\x12U\n" + + "\x0eCreateSnapshot\x12 .snapshots.CreateSnapshotRequest\x1a!.snapshots.CreateSnapshotResponse\x12L\n" + + "\vGetSnapshot\x12\x1d.snapshots.GetSnapshotRequest\x1a\x1e.snapshots.GetSnapshotResponse\x12U\n" + + "\x0eUpdateSnapshot\x12 .snapshots.UpdateSnapshotRequest\x1a!.snapshots.UpdateSnapshotResponse\x12U\n" + + "\x0eDeleteSnapshot\x12 .snapshots.DeleteSnapshotRequest\x1a!.snapshots.DeleteSnapshotResponse\x12`\n" + + "\x11ListSnapshotByGUN\x12$.snapshots.ListSnapshotsByGUNRequest\x1a%.snapshots.ListSnapshotsByGUNResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_snapshots_proto_rawDescOnce sync.Once + file_snapshots_proto_rawDescData []byte +) + +func file_snapshots_proto_rawDescGZIP() []byte { + file_snapshots_proto_rawDescOnce.Do(func() { + file_snapshots_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_snapshots_proto_rawDesc), len(file_snapshots_proto_rawDesc))) + }) + return file_snapshots_proto_rawDescData +} + +var file_snapshots_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_snapshots_proto_goTypes = []any{ + (*Snapshot)(nil), // 0: snapshots.Snapshot + (*SnapshotProperties)(nil), // 1: snapshots.SnapshotProperties + (*SnapshotMetadata)(nil), // 2: snapshots.SnapshotMetadata + (*ListSnapshotsRequest)(nil), // 3: snapshots.ListSnapshotsRequest + (*ListSnapshotResponse)(nil), // 4: snapshots.ListSnapshotResponse + (*CreateSnapshotRequest)(nil), // 5: snapshots.CreateSnapshotRequest + (*CreateSnapshotResponse)(nil), // 6: snapshots.CreateSnapshotResponse + (*GetSnapshotRequest)(nil), // 7: snapshots.GetSnapshotRequest + (*GetSnapshotResponse)(nil), // 8: snapshots.GetSnapshotResponse + (*UpdateSnapshotRequest)(nil), // 9: snapshots.UpdateSnapshotRequest + (*UpdateSnapshotResponse)(nil), // 10: snapshots.UpdateSnapshotResponse + (*DeleteSnapshotRequest)(nil), // 11: snapshots.DeleteSnapshotRequest + (*DeleteSnapshotResponse)(nil), // 12: snapshots.DeleteSnapshotResponse + (*GetInitialDataRequest)(nil), // 13: snapshots.GetInitialDataRequest + (*GetInitialDataResponse)(nil), // 14: snapshots.GetInitialDataResponse + (*ListSnapshotsByGUNRequest)(nil), // 15: snapshots.ListSnapshotsByGUNRequest + (*ListSnapshotsByGUNResponse)(nil), // 16: snapshots.ListSnapshotsByGUNResponse + (*Query)(nil), // 17: Query + (*Item)(nil), // 18: Item + (*Edge)(nil), // 19: Edge + (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp + (*Bookmark)(nil), // 21: bookmarks.Bookmark + (*PaginationRequest)(nil), // 22: PaginationRequest + (*PaginationResponse)(nil), // 23: PaginationResponse +} +var file_snapshots_proto_depIdxs = []int32{ + 2, // 0: snapshots.Snapshot.metadata:type_name -> snapshots.SnapshotMetadata + 1, // 1: snapshots.Snapshot.properties:type_name -> snapshots.SnapshotProperties + 17, // 2: snapshots.SnapshotProperties.queries:type_name -> Query + 18, // 3: snapshots.SnapshotProperties.items:type_name -> Item + 19, // 4: snapshots.SnapshotProperties.edges:type_name -> Edge + 20, // 5: snapshots.SnapshotMetadata.created:type_name -> google.protobuf.Timestamp + 0, // 6: snapshots.ListSnapshotResponse.snapshots:type_name -> snapshots.Snapshot + 1, // 7: snapshots.CreateSnapshotRequest.properties:type_name -> snapshots.SnapshotProperties + 0, // 8: snapshots.CreateSnapshotResponse.snapshot:type_name -> snapshots.Snapshot + 0, // 9: snapshots.GetSnapshotResponse.snapshot:type_name -> snapshots.Snapshot + 1, // 10: snapshots.UpdateSnapshotRequest.properties:type_name -> snapshots.SnapshotProperties + 0, // 11: snapshots.UpdateSnapshotResponse.snapshot:type_name -> snapshots.Snapshot + 0, // 12: snapshots.GetInitialDataResponse.blastRadiusSnapshot:type_name -> snapshots.Snapshot + 21, // 13: snapshots.GetInitialDataResponse.changingItemsBookmark:type_name -> bookmarks.Bookmark + 22, // 14: snapshots.ListSnapshotsByGUNRequest.pagination:type_name -> PaginationRequest + 23, // 15: snapshots.ListSnapshotsByGUNResponse.pagination:type_name -> PaginationResponse + 3, // 16: snapshots.SnapshotsService.ListSnapshots:input_type -> snapshots.ListSnapshotsRequest + 5, // 17: snapshots.SnapshotsService.CreateSnapshot:input_type -> snapshots.CreateSnapshotRequest + 7, // 18: snapshots.SnapshotsService.GetSnapshot:input_type -> snapshots.GetSnapshotRequest + 9, // 19: snapshots.SnapshotsService.UpdateSnapshot:input_type -> snapshots.UpdateSnapshotRequest + 11, // 20: snapshots.SnapshotsService.DeleteSnapshot:input_type -> snapshots.DeleteSnapshotRequest + 15, // 21: snapshots.SnapshotsService.ListSnapshotByGUN:input_type -> snapshots.ListSnapshotsByGUNRequest + 4, // 22: snapshots.SnapshotsService.ListSnapshots:output_type -> snapshots.ListSnapshotResponse + 6, // 23: snapshots.SnapshotsService.CreateSnapshot:output_type -> snapshots.CreateSnapshotResponse + 8, // 24: snapshots.SnapshotsService.GetSnapshot:output_type -> snapshots.GetSnapshotResponse + 10, // 25: snapshots.SnapshotsService.UpdateSnapshot:output_type -> snapshots.UpdateSnapshotResponse + 12, // 26: snapshots.SnapshotsService.DeleteSnapshot:output_type -> snapshots.DeleteSnapshotResponse + 16, // 27: snapshots.SnapshotsService.ListSnapshotByGUN:output_type -> snapshots.ListSnapshotsByGUNResponse + 22, // [22:28] is the sub-list for method output_type + 16, // [16:22] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name +} + +func init() { file_snapshots_proto_init() } +func file_snapshots_proto_init() { + if File_snapshots_proto != nil { + return + } + file_bookmarks_proto_init() + file_items_proto_init() + file_util_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_snapshots_proto_rawDesc), len(file_snapshots_proto_rawDesc)), + NumEnums: 0, + NumMessages: 17, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_snapshots_proto_goTypes, + DependencyIndexes: file_snapshots_proto_depIdxs, + MessageInfos: file_snapshots_proto_msgTypes, + }.Build() + File_snapshots_proto = out.File + file_snapshots_proto_goTypes = nil + file_snapshots_proto_depIdxs = nil +} diff --git a/go/sdp-go/test_utils.go b/go/sdp-go/test_utils.go new file mode 100644 index 00000000..0ca45d10 --- /dev/null +++ b/go/sdp-go/test_utils.go @@ -0,0 +1,240 @@ +package sdp + +import ( + "context" + "errors" + "fmt" + "math/rand" + "regexp" + "strings" + sync "sync" + + "github.com/nats-io/nats.go" + "google.golang.org/protobuf/proto" +) + +type ResponseMessage struct { + Subject string + V interface{} +} + +// TestConnection Used to mock a NATS connection for testing +type TestConnection struct { + Messages []ResponseMessage + MessagesMu sync.Mutex + + // If set, the test connection will not return ErrNoResponders if someone + // tries to publish a message to a subject with no responders + IgnoreNoResponders bool + + Subscriptions map[*regexp.Regexp][]nats.MsgHandler + subscriptionsMutex sync.RWMutex +} + +// assert interface implementation +var _ EncodedConnection = (*TestConnection)(nil) + +// Publish Test publish method, notes down the subject and the message +func (t *TestConnection) Publish(ctx context.Context, subj string, m proto.Message) error { + t.MessagesMu.Lock() + t.Messages = append(t.Messages, ResponseMessage{ + Subject: subj, + V: m, + }) + t.MessagesMu.Unlock() + + data, err := proto.Marshal(m) + if err != nil { + return err + } + msg := nats.Msg{ + Subject: subj, + Data: data, + } + return t.runHandlers(&msg) +} + +// PublishRequest Test publish method, notes down the subject and the message +func (t *TestConnection) PublishRequest(ctx context.Context, subj, replyTo string, m proto.Message) error { + t.MessagesMu.Lock() + t.Messages = append(t.Messages, ResponseMessage{ + Subject: subj, + V: m, + }) + t.MessagesMu.Unlock() + + data, err := proto.Marshal(m) + if err != nil { + return err + } + msg := nats.Msg{ + Subject: subj, + Data: data, + Header: nats.Header{}, + } + msg.Header.Add("reply-to", replyTo) + return t.runHandlers(&msg) +} + +// PublishMsg Test publish method, notes down the subject and the message +func (t *TestConnection) PublishMsg(ctx context.Context, msg *nats.Msg) error { + t.MessagesMu.Lock() + t.Messages = append(t.Messages, ResponseMessage{ + Subject: msg.Subject, + V: msg.Data, + }) + t.MessagesMu.Unlock() + + err := t.runHandlers(msg) + if err != nil { + return err + } + + return nil +} + +func (t *TestConnection) Subscribe(subj string, cb nats.MsgHandler) (*nats.Subscription, error) { + t.subscriptionsMutex.Lock() + defer t.subscriptionsMutex.Unlock() + + if t.Subscriptions == nil { + t.Subscriptions = make(map[*regexp.Regexp][]nats.MsgHandler) + } + + regex := t.subjectToRegexp(subj) + + t.Subscriptions[regex] = append(t.Subscriptions[regex], cb) + + return nil, nil +} + +func (t *TestConnection) QueueSubscribe(subj, queue string, cb nats.MsgHandler) (*nats.Subscription, error) { + // TODO: implement queue groups here + return t.Subscribe(subj, cb) +} + +func (r *TestConnection) subjectToRegexp(subject string) *regexp.Regexp { + // If the subject contains a > then handle this + if strings.Contains(subject, ">") { + // Escape regex to literal + quoted := regexp.QuoteMeta(subject) + + // Replace > with .*$ + return regexp.MustCompile(strings.ReplaceAll(quoted, ">", ".*$")) + } + + if strings.Contains(subject, "*") { + // Escape regex to literal + quoted := regexp.QuoteMeta(subject) + + // Replace \* with \w+ + return regexp.MustCompile(strings.ReplaceAll(quoted, `\*`, `\w+`)) + } + + return regexp.MustCompile(regexp.QuoteMeta(subject)) +} + +// RequestMsg Simulates a request on the given subject, assigns a random +// response subject then calls the handler on the given subject, we are +// expecting the handler to be in the format: func(msg *nats.Msg) +func (t *TestConnection) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { + replySubject := randSeq(10) + msg.Reply = replySubject + replies := make(chan interface{}, 128) + + // Subscribe to the reply subject + _, err := t.Subscribe(replySubject, func(msg *nats.Msg) { + replies <- msg + }) + if err != nil { + return nil, err + } + // Run the handlers + err = t.runHandlers(msg) + if err != nil { + return nil, err + } + + // Return the first result + select { + case reply, ok := <-replies: + if ok { + if m, ok := reply.(*nats.Msg); ok { + return &nats.Msg{ + Subject: replySubject, + Data: m.Data, + }, nil + } else { + return nil, fmt.Errorf("reply was not a *nats.Msg, but a %T", reply) + } + } else { + return nil, errors.New("no replies") + } + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// Status Always returns nats.CONNECTED +func (n *TestConnection) Status() nats.Status { + return nats.CONNECTED +} + +// Stats Always returns empty/zero nats.Statistics +func (n *TestConnection) Stats() nats.Statistics { + return nats.Statistics{} +} + +// LastError Always returns nil +func (n *TestConnection) LastError() error { + return nil +} + +// Drain Always returns nil +func (n *TestConnection) Drain() error { + return nil +} + +// Close Does nothing +func (n *TestConnection) Close() {} + +// Underlying Always returns nil +func (n *TestConnection) Underlying() *nats.Conn { + return &nats.Conn{} +} + +// Drop Does nothing +func (n *TestConnection) Drop() {} + +// runHandlers Runs the handlers for a given subject +func (t *TestConnection) runHandlers(msg *nats.Msg) error { + t.subscriptionsMutex.RLock() + defer t.subscriptionsMutex.RUnlock() + + var hasResponder bool + + for subjectRegex, handlers := range t.Subscriptions { + if subjectRegex.MatchString(msg.Subject) { + for _, handler := range handlers { + hasResponder = true + handler(msg) + } + } + } + + if hasResponder || t.IgnoreNoResponders { + return nil + } else { + return nats.ErrNoResponders + } +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] //nolint:gosec // This is not for security + } + return string(b) +} diff --git a/go/sdp-go/test_utils_test.go b/go/sdp-go/test_utils_test.go new file mode 100644 index 00000000..7eaf67a1 --- /dev/null +++ b/go/sdp-go/test_utils_test.go @@ -0,0 +1,143 @@ +package sdp + +import ( + "context" + "testing" + + "github.com/nats-io/nats.go" + "google.golang.org/protobuf/proto" +) + +func TestRequest(t *testing.T) { + tc := TestConnection{} + + t.Run("with a regular subject", func(t *testing.T) { + // Create the responder + _, err := tc.Subscribe("test", func(msg *nats.Msg) { + err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ + ResponseType: &GatewayResponse_Error{ + Error: "testing", + }, + }) + if err2 != nil { + t.Error(err2) + } + }) + if err != nil { + t.Fatal(err) + } + + request := &GatewayRequest{} + + data, err := proto.Marshal(request) + if err != nil { + t.Fatal(err) + } + + msg := nats.Msg{ + Subject: "test", + Data: data, + } + replyMsg, err := tc.RequestMsg(context.Background(), &msg) + if err != nil { + t.Fatal(err) + } + + response := &GatewayResponse{} + err = proto.Unmarshal(replyMsg.Data, response) + if err != nil { + t.Error(err) + } + + if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { + t.Errorf("expected error to be 'testing', got '%v'", response) + } + }) + + t.Run("with a > wildcard subject", func(t *testing.T) { + // Create the responder + _, err := tc.Subscribe("test.>", func(msg *nats.Msg) { + err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ + ResponseType: &GatewayResponse_Error{ + Error: "testing", + }, + }) + if err2 != nil { + t.Error(err2) + } + }) + if err != nil { + t.Fatal(err) + } + + request := &GatewayRequest{} + + data, err := proto.Marshal(request) + if err != nil { + t.Fatal(err) + } + + msg := nats.Msg{ + Subject: "test.foo.bar", + Data: data, + } + replyMsg, err := tc.RequestMsg(context.Background(), &msg) + if err != nil { + t.Fatal(err) + } + + response := &GatewayResponse{} + err = proto.Unmarshal(replyMsg.Data, response) + if err != nil { + t.Error(err) + } + + if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { + t.Errorf("expected error to be 'testing', got '%v'", response) + } + }) + + t.Run("with a * wildcard subject", func(t *testing.T) { + // Create the responder + _, err := tc.Subscribe("test.*.bar", func(msg *nats.Msg) { + err2 := tc.Publish(context.Background(), msg.Reply, &GatewayResponse{ + ResponseType: &GatewayResponse_Error{ + Error: "testing", + }, + }) + if err2 != nil { + t.Error(err2) + } + }) + if err != nil { + t.Fatal(err) + } + + request := &GatewayRequest{} + + data, err := proto.Marshal(request) + if err != nil { + t.Fatal(err) + } + + msg := nats.Msg{ + Subject: "test.foo.bar", + Data: data, + } + replyMsg, err := tc.RequestMsg(context.Background(), &msg) + if err != nil { + t.Fatal(err) + } + + response := &GatewayResponse{} + err = proto.Unmarshal(replyMsg.Data, response) + if err != nil { + t.Error(err) + } + + if response.GetResponseType().(*GatewayResponse_Error).Error != "testing" { + t.Errorf("expected error to be 'testing', got '%v'", response) + } + }) + +} diff --git a/go/sdp-go/tracing.go b/go/sdp-go/tracing.go new file mode 100644 index 00000000..16398f6c --- /dev/null +++ b/go/sdp-go/tracing.go @@ -0,0 +1,106 @@ +package sdp + +import ( + "context" + + "connectrpc.com/connect" + "github.com/getsentry/sentry-go" + "github.com/nats-io/nats.go" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type CtxMsgHandler func(ctx context.Context, msg *nats.Msg) + +func NewOtelExtractingHandler(spanName string, h CtxMsgHandler, t trace.Tracer, spanOpts ...trace.SpanStartOption) nats.MsgHandler { + if h == nil { + return nil + } + + return func(msg *nats.Msg) { + ctx := context.Background() + + ctx = otel.GetTextMapPropagator().Extract(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) + + // don't start a span when we have no spanName + if spanName != "" { + var span trace.Span + ctx, span = t.Start(ctx, spanName, spanOpts...) + defer span.End() + } + + h(ctx, msg) + } +} + +func NewAsyncOtelExtractingHandler(spanName string, h CtxMsgHandler, t trace.Tracer, spanOpts ...trace.SpanStartOption) nats.MsgHandler { + if h == nil { + return nil + } + + return func(msg *nats.Msg) { + go func() { + defer sentry.Recover() + + ctx := context.Background() + ctx = otel.GetTextMapPropagator().Extract(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) + + // don't start a span when we have no spanName + if spanName != "" { + var span trace.Span + ctx, span = t.Start(ctx, spanName, spanOpts...) + defer span.End() + } + + h(ctx, msg) + }() + } +} + +func InjectOtelTraceContext(ctx context.Context, msg *nats.Msg) { + if msg.Header == nil { + msg.Header = make(nats.Header) + } + + otel.GetTextMapPropagator().Inject(ctx, tracing.NewNatsHeaderCarrier(msg.Header)) +} + +type sentryInterceptor struct{} + +// NewSentryInterceptor pass this to connect handlers as `connect.WithInterceptors(NewSentryInterceptor())` to recover from panics in the handler and report them to sentry. Otherwise panics get recovered by connect-go itself and do not get reported to sentry. +func NewSentryInterceptor() connect.Interceptor { + return &sentryInterceptor{} +} + +func (i *sentryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + // Same as previous UnaryInterceptorFunc. + return connect.UnaryFunc(func( + ctx context.Context, + req connect.AnyRequest, + ) (connect.AnyResponse, error) { + defer sentry.Recover() + return next(ctx, req) + }) +} + +func (*sentryInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return connect.StreamingClientFunc(func( + ctx context.Context, + spec connect.Spec, + ) connect.StreamingClientConn { + defer sentry.Recover() + conn := next(ctx, spec) + return conn + }) +} + +func (i *sentryInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return connect.StreamingHandlerFunc(func( + ctx context.Context, + conn connect.StreamingHandlerConn, + ) error { + defer sentry.Recover() + return next(ctx, conn) + }) +} diff --git a/go/sdp-go/tracing_test.go b/go/sdp-go/tracing_test.go new file mode 100644 index 00000000..e3416629 --- /dev/null +++ b/go/sdp-go/tracing_test.go @@ -0,0 +1,72 @@ +package sdp + +import ( + "context" + "testing" + + "github.com/nats-io/nats.go" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func TestTraceContextPropagation(t *testing.T) { + tp := sdktrace.NewTracerProvider() + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + tc := TestConnection{ + Messages: make([]ResponseMessage, 0), + } + + outerCtx := context.Background() + outerCtx, outerSpan := tp.Tracer("outerTracer").Start(outerCtx, "outer span") + defer outerSpan.End() + // outerJson, err := outerSpan.SpanContext().MarshalJSON() + // if err != nil { + // t.Errorf("error marshalling outerSpan: %v", err) + // } else { + // if !bytes.Equal(outerJson, []byte("{\"TraceID\":\"00000000000000000000000000000000\",\"SpanID\":\"0000000000000000\",\"TraceFlags\":\"00\",\"TraceState\":\"\",\"Remote\":false}")) { + // t.Errorf("outer span has unexpected context: %v", string(outerJson)) + // } + // } + handlerCalled := make(chan struct{}) + _, err := tc.Subscribe("test.subject", NewOtelExtractingHandler("inner span", func(innerCtx context.Context, msg *nats.Msg) { + _, innerSpan := tp.Tracer("innerTracer").Start(innerCtx, "innerSpan") + // innerJson, err := innerSpan.SpanContext().MarshalJSON() + // if err != nil { + // t.Errorf("error marshalling innerSpan: %v", err) + // } else { + // if !bytes.Equal(innerJson, []byte("{\"TraceID\":\"00000000000000000000000000000000\",\"SpanID\":\"0000000000000000\",\"TraceFlags\":\"00\",\"TraceState\":\"\",\"Remote\":false}")) { + // t.Errorf("inner span has unexpected context: %v", string(innerJson)) + // } + // } + if innerSpan.SpanContext().TraceID() != outerSpan.SpanContext().TraceID() { + t.Error("inner span did not link up to outer span") + } + + // clean up + innerSpan.End() + + // finish the test + handlerCalled <- struct{}{} + }, tp.Tracer("providedTracer"))) + if err != nil { + t.Errorf("error subscribing: %v", err) + } + + m := &nats.Msg{ + Subject: "test.subject", + Data: make([]byte, 0), + } + + go func() { + InjectOtelTraceContext(outerCtx, m) + err = tc.PublishMsg(outerCtx, m) + if err != nil { + t.Errorf("error publishing message: %v", err) + } + }() + + // Wait for the handler to be called + <-handlerCalled +} diff --git a/go/sdp-go/util.go b/go/sdp-go/util.go new file mode 100644 index 00000000..aac366c3 --- /dev/null +++ b/go/sdp-go/util.go @@ -0,0 +1,156 @@ +package sdp + +import ( + "fmt" + "math" + "strings" + + "github.com/google/uuid" +) + +// CalculatePaginationOffsetLimit Calculates the offset and limit for pagination +// in SQL queries, along with the current page and total pages that should be +// included in the response +// +// This also sets sane defaults for the page size if pagination is not provided. +// These defaults are page 1 with a page size of 10 +// +// NOTE: If there are no items, then this will return 0 for all values +func CalculatePaginationOffsetLimit(pagination *PaginationRequest, totalItems int32) (offset, limit, page, totalPages int32) { + if totalItems == 0 { + // If there are no items, there are no pages + return 0, 0, 0, 0 + } + + var requestedPageSize int32 + var requestedPage int32 + + if pagination == nil { + // Set sane defaults + requestedPageSize = 10 + requestedPage = 1 + } else { + requestedPageSize = pagination.GetPageSize() + requestedPage = pagination.GetPage() + } + + // pagesize is at least 10, at most 100 + limit = min(100, max(10, requestedPageSize)) + // calculate the total number of pages + totalPages = int32(math.Ceil(float64(totalItems) / float64(limit))) + + // page has to be at least 1, and at most totalPages + page = min(totalPages, requestedPage) + page = max(1, page) + + // calculate the offset + if totalPages == 0 { + offset = 0 + } else { + offset = (page * limit) - limit + } + return offset, limit, page, totalPages +} + +// An object that returns all of the adapter metadata for a given source +type AdapterMetadataProvider interface { + AllAdapterMetadata() []*AdapterMetadata +} + +// A list of adapter metadata, this is used to store all the adapter metadata +// for a given source so that it can be retrieved later for the purposes of +// generating documentation and Terraform mappings +type AdapterMetadataList struct { + // The list of adapter metadata + list []*AdapterMetadata +} + +// AllAdapterMetadata returns all the adapter metadata +func (a *AdapterMetadataList) AllAdapterMetadata() []*AdapterMetadata { + return a.list +} + +// RegisterAdapterMetadata registers a new adapter metadata with the list and +// returns a pointer to that same metadata to be used elsewhere +func (a *AdapterMetadataList) Register(metadata *AdapterMetadata) *AdapterMetadata { + if a == nil { + return metadata + } + + a.list = append(a.list, metadata) + + return metadata +} + +type RoutineRollUp struct { + ChangeId uuid.UUID + Gun string + Attr string + Value string +} + +func (rr RoutineRollUp) String() string { + val := fmt.Sprintf("%v", rr.Value) + if len(val) > 100 { + val = val[:100] + } + val = strings.ReplaceAll(val, "\n", " ") + val = strings.ReplaceAll(val, "\t", " ") + return fmt.Sprintf("change:%v\tgun:%v\tattr:%v\tval:%v", rr.ChangeId, rr.Gun, rr.Attr, val) +} + +func WalkMapToRoutineRollUp(gun string, key string, data map[string]any) []RoutineRollUp { + results := []RoutineRollUp{} + + for k, v := range data { + attr := k + if key != "" { + attr = fmt.Sprintf("%v.%v", key, k) + } + switch val := v.(type) { + case map[string]any: + results = append(results, WalkMapToRoutineRollUp(gun, attr, val)...) + default: + results = append(results, RoutineRollUp{ + Gun: gun, + Attr: attr, + Value: fmt.Sprintf("%v", val), + }) + } + } + + return results +} + +// GcpSANameFromAccountName generates a GCP service account name from the given +// Service account must be 6-30 characters long, and must comply with the +// `^[a-zA-Z][a-zA-Z\d\-]*[a-zA-Z\d]$` regex. +// +// This regex returned from an error message when trying to create a service account. +// Unfortunately, we could not find any documentation on this. +// The account name is expected to be in the format of a UUID, which is 36 characters long, +// and contains dashes. +// The service account name must be 30 characters or less, +// and must start with a letter, end with a letter or digit, and can only contain +// letters, digits, and dashes. +// So we keep the SA name simple: Start with "C-" and take the first 28 characters of the account name. +func GcpSANameFromAccountName(accountName string) string { + if accountName == "" { + return "" + } + + accountName = strings.ReplaceAll(accountName, "-", "") + + if len(accountName) >= 6 { + // Ensure the account name is at most 30 characters long + // We will prefix it with "C-" to ensure it starts with a letter + // and truncate it to 28 characters after the prefix + if len(accountName) > 28 { + accountName = accountName[:28] + } + + return "C-" + accountName + } + + return "" +} diff --git a/go/sdp-go/util.pb.go b/go/sdp-go/util.pb.go new file mode 100644 index 00000000..bfdb6b5e --- /dev/null +++ b/go/sdp-go/util.pb.go @@ -0,0 +1,285 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: util.proto + +package sdp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SortOrder int32 + +const ( + SortOrder_ALPHABETICAL_ASCENDING SortOrder = 0 // A-Z + SortOrder_ALPHABETICAL_DESCENDING SortOrder = 1 // Z-A + SortOrder_DATE_ASCENDING SortOrder = 2 // Oldest first + SortOrder_DATE_DESCENDING SortOrder = 3 // Newest first +) + +// Enum value maps for SortOrder. +var ( + SortOrder_name = map[int32]string{ + 0: "ALPHABETICAL_ASCENDING", + 1: "ALPHABETICAL_DESCENDING", + 2: "DATE_ASCENDING", + 3: "DATE_DESCENDING", + } + SortOrder_value = map[string]int32{ + "ALPHABETICAL_ASCENDING": 0, + "ALPHABETICAL_DESCENDING": 1, + "DATE_ASCENDING": 2, + "DATE_DESCENDING": 3, + } +) + +func (x SortOrder) Enum() *SortOrder { + p := new(SortOrder) + *p = x + return p +} + +func (x SortOrder) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortOrder) Descriptor() protoreflect.EnumDescriptor { + return file_util_proto_enumTypes[0].Descriptor() +} + +func (SortOrder) Type() protoreflect.EnumType { + return &file_util_proto_enumTypes[0] +} + +func (x SortOrder) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortOrder.Descriptor instead. +func (SortOrder) EnumDescriptor() ([]byte, []int) { + return file_util_proto_rawDescGZIP(), []int{0} +} + +type PaginationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The number of items to return in a single page. The minimum is 10 and the + // maximum is 100. + PageSize int32 `protobuf:"varint,1,opt,name=pageSize,proto3" json:"pageSize,omitempty"` + // The page number to return. the first page is 1. If the page number is + // larger than the total number of pages, the last page is returned. If the + // page number is negative, the first page 1 is returned. + Page int32 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PaginationRequest) Reset() { + *x = PaginationRequest{} + mi := &file_util_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PaginationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginationRequest) ProtoMessage() {} + +func (x *PaginationRequest) ProtoReflect() protoreflect.Message { + mi := &file_util_proto_msgTypes[0] + 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 PaginationRequest.ProtoReflect.Descriptor instead. +func (*PaginationRequest) Descriptor() ([]byte, []int) { + return file_util_proto_rawDescGZIP(), []int{0} +} + +func (x *PaginationRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *PaginationRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +type PaginationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The number of items in the current page + PageSize int32 `protobuf:"varint,1,opt,name=pageSize,proto3" json:"pageSize,omitempty"` + // The total number of items available. Expensive to calculate + // https://www.cybertec-postgresql.com/en/pagination-problem-total-result-count/ + // this is done as a separate query + TotalItems int32 `protobuf:"varint,2,opt,name=totalItems,proto3" json:"totalItems,omitempty"` + // The current page number, NB if the user provided a negative page number, + // this will be 1, if the user provided a page number larger than the total + // number of pages, this will be the last page. If there are no results at + // all, this will be 0. + Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + // The total number of pages available. based on the totalItems and pageSize. + // If there are no results this will be zero. + TotalPages int32 `protobuf:"varint,4,opt,name=totalPages,proto3" json:"totalPages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PaginationResponse) Reset() { + *x = PaginationResponse{} + mi := &file_util_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PaginationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginationResponse) ProtoMessage() {} + +func (x *PaginationResponse) ProtoReflect() protoreflect.Message { + mi := &file_util_proto_msgTypes[1] + 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 PaginationResponse.ProtoReflect.Descriptor instead. +func (*PaginationResponse) Descriptor() ([]byte, []int) { + return file_util_proto_rawDescGZIP(), []int{1} +} + +func (x *PaginationResponse) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *PaginationResponse) GetTotalItems() int32 { + if x != nil { + return x.TotalItems + } + return 0 +} + +func (x *PaginationResponse) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *PaginationResponse) GetTotalPages() int32 { + if x != nil { + return x.TotalPages + } + return 0 +} + +var File_util_proto protoreflect.FileDescriptor + +const file_util_proto_rawDesc = "" + + "\n" + + "\n" + + "util.proto\"C\n" + + "\x11PaginationRequest\x12\x1a\n" + + "\bpageSize\x18\x01 \x01(\x05R\bpageSize\x12\x12\n" + + "\x04page\x18\x02 \x01(\x05R\x04page\"\x84\x01\n" + + "\x12PaginationResponse\x12\x1a\n" + + "\bpageSize\x18\x01 \x01(\x05R\bpageSize\x12\x1e\n" + + "\n" + + "totalItems\x18\x02 \x01(\x05R\n" + + "totalItems\x12\x12\n" + + "\x04page\x18\x03 \x01(\x05R\x04page\x12\x1e\n" + + "\n" + + "totalPages\x18\x04 \x01(\x05R\n" + + "totalPages*m\n" + + "\tSortOrder\x12\x1a\n" + + "\x16ALPHABETICAL_ASCENDING\x10\x00\x12\x1b\n" + + "\x17ALPHABETICAL_DESCENDING\x10\x01\x12\x12\n" + + "\x0eDATE_ASCENDING\x10\x02\x12\x13\n" + + "\x0fDATE_DESCENDING\x10\x03B1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + +var ( + file_util_proto_rawDescOnce sync.Once + file_util_proto_rawDescData []byte +) + +func file_util_proto_rawDescGZIP() []byte { + file_util_proto_rawDescOnce.Do(func() { + file_util_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_util_proto_rawDesc), len(file_util_proto_rawDesc))) + }) + return file_util_proto_rawDescData +} + +var file_util_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_util_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_util_proto_goTypes = []any{ + (SortOrder)(0), // 0: SortOrder + (*PaginationRequest)(nil), // 1: PaginationRequest + (*PaginationResponse)(nil), // 2: PaginationResponse +} +var file_util_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_util_proto_init() } +func file_util_proto_init() { + if File_util_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_util_proto_rawDesc), len(file_util_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_util_proto_goTypes, + DependencyIndexes: file_util_proto_depIdxs, + EnumInfos: file_util_proto_enumTypes, + MessageInfos: file_util_proto_msgTypes, + }.Build() + File_util_proto = out.File + file_util_proto_goTypes = nil + file_util_proto_depIdxs = nil +} diff --git a/go/sdp-go/util_test.go b/go/sdp-go/util_test.go new file mode 100644 index 00000000..40301926 --- /dev/null +++ b/go/sdp-go/util_test.go @@ -0,0 +1,113 @@ +package sdp + +import ( + "fmt" + "regexp" + "testing" +) + +func TestCalculatePaginationOffsetLimit(t *testing.T) { + testCases := []struct { + page int32 + pageSize int32 + totalItems int32 + expectedOffset int32 + expectedLimit int32 + expectedPage int32 + expectedTotalPages int32 + }{ + {page: 2, pageSize: 10, totalItems: 20, expectedOffset: 10, expectedPage: 2, expectedLimit: 10, expectedTotalPages: 2}, + {page: 3, pageSize: 10, totalItems: 25, expectedOffset: 20, expectedPage: 3, expectedLimit: 10, expectedTotalPages: 3}, + {page: 0, pageSize: 5, totalItems: 15, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 2}, + {page: 5, pageSize: 7, totalItems: 23, expectedOffset: 20, expectedPage: 3, expectedLimit: 10, expectedTotalPages: 3}, + {page: 1, pageSize: 10, totalItems: 3, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 1}, + {page: -1, pageSize: 10, totalItems: 1, expectedOffset: 0, expectedPage: 1, expectedLimit: 10, expectedTotalPages: 1}, + {page: 1, pageSize: 101, totalItems: 1, expectedOffset: 0, expectedPage: 1, expectedLimit: 100, expectedTotalPages: 1}, + {page: 1, pageSize: 10, totalItems: 0, expectedOffset: 0, expectedPage: 0, expectedLimit: 0, expectedTotalPages: 0}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("page%d pagesize%d totalItems%d", tc.page, tc.pageSize, tc.totalItems), func(t *testing.T) { + req := PaginationRequest{ + Page: tc.page, + PageSize: tc.pageSize, + } + offset, limit, correctedPage, pages := CalculatePaginationOffsetLimit(&req, tc.totalItems) + if offset != tc.expectedOffset { + t.Errorf("Expected offset %d, but got %d", tc.expectedOffset, offset) + } + if correctedPage != tc.expectedPage { + t.Errorf("Expected correctedPage %d, but got %d", tc.expectedPage, correctedPage) + } + if limit != tc.expectedLimit { + t.Errorf("Expected limit %d, but got %d", tc.expectedLimit, limit) + } + if pages != tc.expectedTotalPages { + t.Errorf("Expected pages %d, but got %d", tc.expectedTotalPages, pages) + } + }) + } + + t.Run("Default values", func(t *testing.T) { + offset, limit, correctedPage, pages := CalculatePaginationOffsetLimit(nil, 100) + if offset != 0 { + t.Errorf("Expected offset 0, but got %d", offset) + } + if correctedPage != 1 { + t.Errorf("Expected correctedPage 1, but got %d", correctedPage) + } + if limit != 10 { + t.Errorf("Expected limit 10, but got %d", limit) + } + if pages != 10 { + t.Errorf("Expected pages 10, but got %d", pages) + } + }) +} + +func TestGcpSANameFromAccountName(t *testing.T) { + t.Parallel() + + tests := []struct { + accountName string + expected string + }{ + // BEWARE!! If this test needs changing, all currently existing service + // accounts in GCP will need to be updated, which sounds like an unholy + // mess. + {"test-account", "C-testaccount"}, + {"", ""}, + {"6351cbb7-cb45-481a-99cd-909d04a58512", "C-6351cbb7cb45481a99cd909d04a5"}, + {"d408ea46-f4c9-487f-9bf4-b0bcb6843815", "C-d408ea46f4c9487f9bf4b0bcb684"}, + {"63d185c7141237978cfdbaa2", "C-63d185c7141237978cfdbaa2"}, + {"b6c1119a-b80b-4a7b-b8df-acb5348525ac", "C-b6c1119ab80b4a7bb8dfacb53485"}, + } + + pattern := `^[a-zA-Z][a-zA-Z\d\-]*[a-zA-Z\d]$` + + for _, test := range tests { + t.Run(test.accountName, func(t *testing.T) { + result := GcpSANameFromAccountName(test.accountName) + if result != test.expected { + t.Errorf("expected %s, got %s", test.expected, result) + } + + if test.expected != "" { + matched, err := regexp.MatchString(pattern, result) + if err != nil { + t.Fatalf("failed to compile regex: %v", err) + } + if !matched { + t.Errorf("result %q does not match regex %q", result, pattern) + } + + if len(result) > 30 { + t.Errorf("result %q exceeds 30 characters", result) + } + + if len(result) < 6 { + t.Errorf("result %q is less than 6 characters", result) + } + } + }) + } +} diff --git a/go/sdp-go/validation.go b/go/sdp-go/validation.go new file mode 100644 index 00000000..5f084bae --- /dev/null +++ b/go/sdp-go/validation.go @@ -0,0 +1,168 @@ +package sdp + +import ( + "errors" + "fmt" +) + +// Validate ensures that an Item contains all required fields: +// - Type: must be non-empty +// - UniqueAttribute: must be non-empty +// - Attributes: must not be nil +// - Scope: must be non-empty +// - UniqueAttributeValue: must be non-empty (derived from Attributes) +func (i *Item) Validate() error { + if i == nil { + return errors.New("Item is nil") + } + + if i.GetType() == "" { + return fmt.Errorf("item has empty Type: %v", i.GloballyUniqueName()) + } + + if i.GetUniqueAttribute() == "" { + return fmt.Errorf("item has empty UniqueAttribute: %v", i.GloballyUniqueName()) + } + + if i.GetAttributes() == nil { + return fmt.Errorf("item has nil Attributes: %v", i.GloballyUniqueName()) + } + + if i.GetScope() == "" { + return fmt.Errorf("item has empty Scope: %v", i.GloballyUniqueName()) + } + + if i.UniqueAttributeValue() == "" { + return fmt.Errorf("item has empty UniqueAttributeValue: %v", i.GloballyUniqueName()) + } + + return nil +} + +// Validate ensures a Reference contains all required fields: +// - Type: must be non-empty +// - UniqueAttributeValue: must be non-empty +// - Scope: must be non-empty +func (r *Reference) Validate() error { + if r == nil { + return errors.New("reference is nil") + } + if r.GetType() == "" { + return fmt.Errorf("reference has empty Type: %v", r) + } + if r.GetUniqueAttributeValue() == "" { + return fmt.Errorf("reference has empty UniqueAttributeValue: %v", r) + } + if r.GetScope() == "" { + return fmt.Errorf("reference has empty Scope: %v", r) + } + + return nil +} + +// Validate ensures an Edge is valid by validating both the From and To references. +func (e *Edge) Validate() error { + if e == nil { + return errors.New("edge is nil") + } + + var err error + + err = e.GetFrom().Validate() + if err != nil { + return err + } + + err = e.GetTo().Validate() + + return err +} + +// Validate ensures a Response contains all required fields: +// - Responder: must be non-empty +// - UUID: must be non-empty +func (r *Response) Validate() error { + if r == nil { + return errors.New("response is nil") + } + + if r.GetResponder() == "" { + return fmt.Errorf("response has empty Responder: %v", r) + } + + if len(r.GetUUID()) == 0 { + return fmt.Errorf("response has empty UUID: %v", r) + } + + return nil +} + +// Validate ensures a QueryError contains all required fields: +// - UUID: must be non-empty +// - ErrorString: must be non-empty +// - Scope: must be non-empty +// - SourceName: must be non-empty +// - ItemType: must be non-empty +// - ResponderName: must be non-empty +func (e *QueryError) Validate() error { + if e == nil { + return errors.New("queryError is nil") + } + + if len(e.GetUUID()) == 0 { + return fmt.Errorf("queryError has empty UUID: %w", e) + } + + if e.GetErrorString() == "" { + return fmt.Errorf("queryError has empty ErrorString: %w", e) + } + + if e.GetScope() == "" { + return fmt.Errorf("queryError has empty Scope: %w", e) + } + + if e.GetSourceName() == "" { + return fmt.Errorf("queryError has empty SourceName: %w", e) + } + + if e.GetItemType() == "" { + return fmt.Errorf("queryError has empty ItemType: %w", e) + } + + if e.GetResponderName() == "" { + return fmt.Errorf("queryError has empty ResponderName: %w", e) + } + + return nil +} + +// Validate ensures a Query contains all required fields: +// - Type: must be non-empty +// - Scope: must be non-empty +// - UUID: must be exactly 16 bytes +// - Query: must be non-empty when method is GET +func (q *Query) Validate() error { + if q == nil { + return errors.New("query is nil") + } + + if q.GetType() == "" { + return fmt.Errorf("query has empty Type: %v", q) + } + + if q.GetScope() == "" { + return fmt.Errorf("query has empty Scope: %v", q) + } + + if len(q.GetUUID()) != 16 { + return fmt.Errorf("query has invalid UUID: %v", q) + } + + if q.GetMethod() == QueryMethod_GET { + if q.GetQuery() == "" { + return fmt.Errorf("query cannot have empty Query when method is Get: %v", q) + } + } + + return nil +} diff --git a/go/sdp-go/validation_test.go b/go/sdp-go/validation_test.go new file mode 100644 index 00000000..fba7c509 --- /dev/null +++ b/go/sdp-go/validation_test.go @@ -0,0 +1,924 @@ +package sdp + +import ( + "errors" + "testing" + "time" + + "buf.build/go/protovalidate" + + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestValidateItem(t *testing.T) { + t.Run("item is fine", func(t *testing.T) { + err := newItem().Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("Item is nil", func(t *testing.T) { + var i *Item + err := i.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("item has empty Type", func(t *testing.T) { + i := newItem() + + i.Type = "" + + err := i.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("item has empty UniqueAttribute", func(t *testing.T) { + i := newItem() + + i.UniqueAttribute = "" + + err := i.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("item has nil Attributes", func(t *testing.T) { + i := newItem() + + i.Attributes = nil + + err := i.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("item has empty Scope", func(t *testing.T) { + i := newItem() + + i.Scope = "" + + err := i.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("item has empty UniqueAttributeValue", func(t *testing.T) { + i := newItem() + + err := i.GetAttributes().Set(i.GetUniqueAttribute(), "") + if err != nil { + t.Fatal(err) + } + + err = i.Validate() + if err == nil { + t.Error("expected error") + } + }) +} + +func TestValidateReference(t *testing.T) { + t.Run("Reference is fine", func(t *testing.T) { + r := newReference() + + err := r.Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("Reference is nil", func(t *testing.T) { + var r *Reference + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("reference has empty Type", func(t *testing.T) { + r := newReference() + + r.Type = "" + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("reference has empty UniqueAttributeValue", func(t *testing.T) { + r := newReference() + + r.UniqueAttributeValue = "" + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("reference has empty Scope", func(t *testing.T) { + r := newReference() + + r.Scope = "" + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) +} + +func TestValidateEdge(t *testing.T) { + t.Run("Edge is fine", func(t *testing.T) { + e := newEdge() + + err := e.Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("Edge has nil From", func(t *testing.T) { + e := newEdge() + + e.From = nil + + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Edge has nil To", func(t *testing.T) { + e := newEdge() + + e.To = nil + + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Edge has invalid From", func(t *testing.T) { + e := newEdge() + + e.From.Type = "" + + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Edge has invalid To", func(t *testing.T) { + e := newEdge() + + e.To.Scope = "" + + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) +} + +func TestValidateResponse(t *testing.T) { + t.Run("Response is fine", func(t *testing.T) { + r := newResponse() + + err := r.Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("Response is nil", func(t *testing.T) { + var r *Response + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Response has empty Responder", func(t *testing.T) { + r := newResponse() + r.Responder = "" + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("Response has empty UUID", func(t *testing.T) { + r := newResponse() + r.UUID = nil + + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) +} + +func TestValidateQueryError(t *testing.T) { + t.Run("QueryError is fine", func(t *testing.T) { + e := newQueryError() + + err := e.Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("QueryError is nil", func(t *testing.T) { + + }) + + t.Run("QueryError has empty UUID", func(t *testing.T) { + e := newQueryError() + e.UUID = nil + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("QueryError has empty ErrorString", func(t *testing.T) { + e := newQueryError() + e.ErrorString = "" + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("QueryError has empty Scope", func(t *testing.T) { + e := newQueryError() + e.Scope = "" + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("QueryError has empty SourceName", func(t *testing.T) { + e := newQueryError() + e.SourceName = "" + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("QueryError has empty ItemType", func(t *testing.T) { + e := newQueryError() + e.ItemType = "" + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) + + t.Run("QueryError has empty ResponderName", func(t *testing.T) { + e := newQueryError() + e.ResponderName = "" + err := e.Validate() + + if err == nil { + t.Error("expected error") + } + }) +} + +func TestValidateQuery(t *testing.T) { + t.Run("Query is fine", func(t *testing.T) { + r := newQuery() + + err := r.Validate() + + if err != nil { + t.Error(err) + } + }) + + t.Run("Query is nil", func(t *testing.T) { + + }) + + t.Run("Query has empty Type", func(t *testing.T) { + r := newQuery() + r.Type = "" + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + + }) + + t.Run("Query has empty Scope", func(t *testing.T) { + r := newQuery() + r.Scope = "" + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + + }) + + t.Run("Response has empty UUID", func(t *testing.T) { + r := newQuery() + r.UUID = nil + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + + }) + + t.Run("Query cannot have empty Query when method is Get", func(t *testing.T) { + r := newQuery() + r.Method = QueryMethod_GET + r.Query = "" + err := r.Validate() + + if err == nil { + t.Error("expected error") + } + }) + +} + +func newQuery() *Query { + u := uuid.New() + + return &Query{ + Type: "person", + Method: QueryMethod_GET, + Query: "Dylan", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 1, + }, + Scope: "global", + UUID: u[:], + Deadline: timestamppb.New(time.Now().Add(1 * time.Second)), + IgnoreCache: false, + } +} + +func newQueryError() *QueryError { + u := uuid.New() + + return &QueryError{ + UUID: u[:], + ErrorType: QueryError_OTHER, + ErrorString: "bad", + Scope: "global", + SourceName: "test-source", + ItemType: "test", + ResponderName: "test-responder", + } +} + +func newResponse() *Response { + u := uuid.New() + + ru := uuid.New() + + return &Response{ + Responder: "foo", + ResponderUUID: ru[:], + State: ResponderState_WORKING, + NextUpdateIn: durationpb.New(time.Second), + UUID: u[:], + } +} + +func newEdge() *Edge { + return &Edge{ + From: newReference(), + To: newReference(), + } +} + +func newReference() *Reference { + return &Reference{ + Type: "person", + UniqueAttributeValue: "Dylan", + Scope: "global", + } +} + +func newItem() *Item { + return &Item{ + Type: "user", + UniqueAttribute: "name", + Scope: "test", + // TODO(LIQs): delete empty data + LinkedItemQueries: []*LinkedItemQuery{}, + LinkedItems: []*LinkedItem{}, + Attributes: &ItemAttributes{ + AttrStruct: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "name": { + Kind: &structpb.Value_StringValue{ + StringValue: "bar", + }, + }, + }, + }, + }, + Metadata: &Metadata{ + SourceName: "users", + SourceQuery: &Query{ + Type: "user", + Method: QueryMethod_LIST, + Query: "*", + RecursionBehaviour: &Query_RecursionBehaviour{ + LinkDepth: 12, + }, + Scope: "testScope", + }, + Timestamp: timestamppb.Now(), + SourceDuration: &durationpb.Duration{ + Seconds: 1, + Nanos: 1, + }, + SourceDurationPerItem: &durationpb.Duration{ + Seconds: 0, + Nanos: 500, + }, + }, + } +} + +func TestAdapterMetadataValidation(t *testing.T) { + t.Run("Valid Metadata", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter.test_adapter", + }, + }, + } + + err := protovalidate.Validate(md) + if err != nil { + t.Errorf("expected no errors, got %v", err) + } + }) + t.Run("Empty Terraform mappings is OK", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + } + + err := protovalidate.Validate(md) + if err != nil { + t.Errorf("expected no errors, got %v", err) + } + }) + + t.Run("Empty strings in the potential links", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{""}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter.test_adapter", + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Undefined category", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: 9999, // Undefined category + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter.test_adapter", + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Undefined Terraform query method", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: 9999, // Undefined method + TerraformQueryMap: "aws_test_adapter.test_adapter", + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Malformed Terraform query map - no dots", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter_test_adapter", // no dots! + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Malformed Terraform query map - more than 2 items", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter.test_adapter_id.something_else", // expected 2 items, got 3 + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("With Nil Terraform mapping", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + nil, + { + TerraformMethod: QueryMethod_GET, + TerraformQueryMap: "aws_test_adapter.test_adapter_id", + }, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Missing get description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Missing search description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Missing list description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Empty string in the get description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Empty string in the search description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "", + List: true, + ListDescription: "List test adapters", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) + + t.Run("Empty string in the list description", func(t *testing.T) { + md := &AdapterMetadata{ + Type: "test-adapter", + DescriptiveName: "Test Adapter", + Category: AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + SupportedQueryMethods: &AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get a test adapter", + Search: true, + SearchDescription: "Search test adapters", + List: true, + ListDescription: "", + }, + PotentialLinks: []string{"test-link"}, + TerraformMappings: []*TerraformMapping{ + {TerraformQueryMap: "aws_test_adapter.test_adapter"}, + }, + } + + err := protovalidate.Validate(md) + if err == nil { + t.Errorf("expected error, got nil") + } + + var validationError *protovalidate.ValidationError + if !errors.As(err, &validationError) { + t.Errorf("expected validation error, got %T: %v", err, err) + } + }) +} diff --git a/go/sdpcache/bolt_cache.go b/go/sdpcache/bolt_cache.go new file mode 100644 index 00000000..9d740310 --- /dev/null +++ b/go/sdpcache/bolt_cache.go @@ -0,0 +1,1505 @@ +package sdpcache + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "syscall" + "time" + + "github.com/getsentry/sentry-go" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "go.etcd.io/bbolt" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" +) + +// Bucket names for bbolt +var ( + itemsBucketName = []byte("items") + expiryBucketName = []byte("expiry") + metaBucketName = []byte("meta") + deletedBytesKey = []byte("deletedBytes") +) + +// DefaultCompactThreshold is the default threshold for triggering compaction (100MB) +const DefaultCompactThreshold = 100 * 1024 * 1024 + +// isDiskFullError checks if an error is due to disk being full (ENOSPC) +func isDiskFullError(err error) bool { + if err == nil { + return false + } + // Check if it wraps ENOSPC + var errno syscall.Errno + if errors.As(err, &errno) && errno == syscall.ENOSPC { + return true + } + // Check using errors.Is for wrapped errors + return errors.Is(err, syscall.ENOSPC) +} + +// encodeCachedEntry serializes a CachedEntry to bytes using protobuf +func encodeCachedEntry(e *sdp.CachedEntry) ([]byte, error) { + return proto.Marshal(e) +} + +// decodeCachedEntry deserializes bytes to a CachedEntry using protobuf +func decodeCachedEntry(data []byte) (*sdp.CachedEntry, error) { + e := &sdp.CachedEntry{} + if err := proto.Unmarshal(data, e); err != nil { + return nil, fmt.Errorf("failed to unmarshal cached entry: %w", err) + } + return e, nil +} + +// toCachedResult converts a CachedEntry to a CachedResult +func cachedEntryToCachedResult(e *sdp.CachedEntry) *CachedResult { + result := &CachedResult{ + Item: e.GetItem(), + Expiry: time.Unix(0, e.GetExpiryUnixNano()), + IndexValues: IndexValues{ + SSTHash: SSTHash(e.GetSstHash()), + UniqueAttributeValue: e.GetUniqueAttributeValue(), + Method: e.GetMethod(), + Query: e.GetQuery(), + }, + } + // Only set Error if it's actually meaningful (not nil and not zero-value) + err := e.GetError() + if err != nil && (err.GetErrorType() != 0 || err.GetErrorString() != "" || err.GetScope() != "" || err.GetSourceName() != "" || err.GetItemType() != "") { + result.Error = err + } + return result +} + +// fromCachedResult creates a CachedEntry from a CachedResult +func fromCachedResult(cr *CachedResult) (*sdp.CachedEntry, error) { + e := &sdp.CachedEntry{ + Item: cr.Item, + ExpiryUnixNano: cr.Expiry.UnixNano(), + UniqueAttributeValue: cr.IndexValues.UniqueAttributeValue, + Method: cr.IndexValues.Method, + Query: cr.IndexValues.Query, + SstHash: string(cr.IndexValues.SSTHash), + } + + if cr.Error != nil { + // Try to cast to QueryError for protobuf serialization + var qErr *sdp.QueryError + if errors.As(cr.Error, &qErr) { + e.Error = qErr + } else { + // For non-QueryError errors, wrap in a QueryError + e.Error = &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: cr.Error.Error(), + } + } + } + + return e, nil +} + +// makeEntryKey creates a key for storing an entry in the items bucket +// Format: {method}|{query}|{uniqueAttributeValue}|{globallyUniqueName} +func makeEntryKey(iv IndexValues, item *sdp.Item) []byte { + var gun string + if item != nil { + gun = item.GloballyUniqueName() + } + key := fmt.Sprintf("%d|%s|%s|%s", iv.Method, iv.Query, iv.UniqueAttributeValue, gun) + return []byte(key) +} + +// makeExpiryKey creates a key for the expiry index +// Format: {expiryNano}|{sstHash}|{entryKey} +func makeExpiryKey(expiry time.Time, sstHash SSTHash, entryKey []byte) []byte { + // Use big-endian encoding for expiry so keys sort chronologically + buf := make([]byte, 8+1+len(sstHash)+1+len(entryKey)) + expiryNano := expiry.UnixNano() + var expiryNanoUint uint64 + if expiryNano < 0 { + expiryNanoUint = 0 + } else { + expiryNanoUint = uint64(expiryNano) + } + binary.BigEndian.PutUint64(buf[0:8], expiryNanoUint) + buf[8] = '|' + copy(buf[9:], []byte(sstHash)) + buf[9+len(sstHash)] = '|' + copy(buf[10+len(sstHash):], entryKey) + return buf +} + +// parseExpiryKey extracts the expiry time, sst hash, and entry key from an expiry key +func parseExpiryKey(key []byte) (time.Time, SSTHash, []byte, error) { + if len(key) < 10 { + return time.Time{}, "", nil, errors.New("expiry key too short") + } + + expiryNanoUint := binary.BigEndian.Uint64(key[0:8]) + expiryNano := int64(expiryNanoUint) + // Check for overflow when converting uint64 to int64 + if expiryNano < 0 && expiryNanoUint > 0 { + expiryNano = 0 + } + expiry := time.Unix(0, expiryNano) + + // Find the separators + rest := key[9:] // skip the first separator + sepIdx := bytes.IndexByte(rest, '|') + if sepIdx < 0 { + return time.Time{}, "", nil, errors.New("invalid expiry key format") + } + + sstHash := SSTHash(rest[:sepIdx]) + entryKey := rest[sepIdx+1:] + + return expiry, sstHash, entryKey, nil +} + +// BoltCache implements the Cache interface using bbolt for persistent storage +type BoltCache struct { + db *bbolt.DB + path string + + // Minimum amount of time to wait between cache purges + MinWaitTime time.Duration + + // CompactThreshold is the number of deleted bytes before triggering compaction + CompactThreshold int64 + + // The timer that is used to trigger the next purge + purgeTimer *time.Timer + + // The time that the purger will run next + nextPurge time.Time + + // Ensures that purge stats like `purgeTimer` and `nextPurge` aren't being + // modified concurrently + purgeMutex sync.Mutex + + // Track deleted bytes for compaction + deletedBytes int64 + deletedMu sync.Mutex + + // Ensures that compaction operations aren't running concurrently + // Read operations use RLock, write operations and compaction use Lock + compactMutex sync.RWMutex + + // Tracks in-flight lookups to prevent duplicate work when multiple + // goroutines request the same cache key simultaneously + pending *pendingWork +} + +// assert interface +var _ Cache = (*BoltCache)(nil) + +// BoltCacheOption is a functional option for configuring BoltCache +type BoltCacheOption func(*BoltCache) + +// WithMinWaitTime sets the minimum wait time between purges +func WithMinWaitTime(d time.Duration) BoltCacheOption { + return func(c *BoltCache) { + c.MinWaitTime = d + } +} + +// WithCompactThreshold sets the threshold for triggering compaction +func WithCompactThreshold(bytes int64) BoltCacheOption { + return func(c *BoltCache) { + c.CompactThreshold = bytes + } +} + +// NewBoltCache creates a new BoltCache at the specified path. +// If a cache file already exists at the path, it will be opened and used. +// The existing file will be automatically handled by the purge process, +// which removes expired items. No explicit cleanup is needed on startup. +func NewBoltCache(path string, opts ...BoltCacheOption) (*BoltCache, error) { + // Ensure the directory exists + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + // bbolt.Open will open an existing file if present, or create a new one + db, err := bbolt.Open(path, 0o600, &bbolt.Options{ + Timeout: 5 * time.Second, + }) + if err != nil { + return nil, fmt.Errorf("failed to open bolt database: %w", err) + } + + c := &BoltCache{ + db: db, + path: path, + CompactThreshold: DefaultCompactThreshold, + pending: newPendingWork(), + } + + for _, opt := range opts { + opt(c) + } + + // Initialize buckets + if err := c.initBuckets(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to initialize buckets: %w", err) + } + + // Load deleted bytes from meta + if err := c.loadDeletedBytes(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to load deleted bytes: %w", err) + } + + return c, nil +} + +// initBuckets creates the required buckets if they don't exist +func (c *BoltCache) initBuckets() error { + return c.db.Update(func(tx *bbolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists(itemsBucketName); err != nil { + return fmt.Errorf("failed to create items bucket: %w", err) + } + if _, err := tx.CreateBucketIfNotExists(expiryBucketName); err != nil { + return fmt.Errorf("failed to create expiry bucket: %w", err) + } + if _, err := tx.CreateBucketIfNotExists(metaBucketName); err != nil { + return fmt.Errorf("failed to create meta bucket: %w", err) + } + return nil + }) +} + +// loadDeletedBytes loads the deleted bytes counter from the meta bucket +func (c *BoltCache) loadDeletedBytes() error { + return c.db.View(func(tx *bbolt.Tx) error { + meta := tx.Bucket(metaBucketName) + if meta == nil { + return nil + } + + data := meta.Get(deletedBytesKey) + if len(data) == 8 { + deletedBytesUint := binary.BigEndian.Uint64(data) + deletedBytes := int64(deletedBytesUint) + // Check for overflow when converting uint64 to int64 + if deletedBytes < 0 && deletedBytesUint > 0 { + deletedBytes = 0 + } + c.deletedBytes = deletedBytes + } + return nil + }) +} + +// saveDeletedBytes saves the deleted bytes counter to the meta bucket +func (c *BoltCache) saveDeletedBytes(tx *bbolt.Tx) error { + meta := tx.Bucket(metaBucketName) + if meta == nil { + return errors.New("meta bucket not found") + } + + buf := make([]byte, 8) + deletedBytes := c.deletedBytes + var deletedBytesUint uint64 + if deletedBytes < 0 { + deletedBytesUint = 0 + } else { + deletedBytesUint = uint64(deletedBytes) + } + binary.BigEndian.PutUint64(buf, deletedBytesUint) + return meta.Put(deletedBytesKey, buf) +} + +// addDeletedBytes adds to the deleted bytes counter (thread-safe) +func (c *BoltCache) addDeletedBytes(n int64) { + c.deletedMu.Lock() + c.deletedBytes += n + c.deletedMu.Unlock() +} + +// getDeletedBytes returns the current deleted bytes count (thread-safe) +func (c *BoltCache) getDeletedBytes() int64 { + c.deletedMu.Lock() + defer c.deletedMu.Unlock() + return c.deletedBytes +} + +// resetDeletedBytes resets the deleted bytes counter (thread-safe) +func (c *BoltCache) resetDeletedBytes() { + c.deletedMu.Lock() + c.deletedBytes = 0 + c.deletedMu.Unlock() +} + +// getFileSize returns the size of the BoltDB file, logging any errors +func (c *BoltCache) getFileSize() int64 { + if c == nil || c.path == "" { + return 0 + } + + stat, err := os.Stat(c.path) + if err != nil { + if os.IsNotExist(err) { + log.Warnf("BoltDB cache file does not exist: %s", c.path) + } else { + log.WithError(err).Warnf("Failed to stat BoltDB cache file: %s", c.path) + } + return 0 + } + + return stat.Size() +} + +// getDiskUsageMetrics returns disk usage metrics for the BoltDB file +func (c *BoltCache) getDiskUsageMetrics() (fileSize int64, deletedBytes int64) { + if c == nil || c.path == "" { + return 0, 0 + } + + fileSize = c.getFileSize() + deletedBytes = c.getDeletedBytes() + + return fileSize, deletedBytes +} + +// setDiskUsageAttributes sets disk usage attributes on a span +func (c *BoltCache) setDiskUsageAttributes(span trace.Span) { + if c == nil { + return + } + + fileSize, deletedBytes := c.getDiskUsageMetrics() + span.SetAttributes( + attribute.Int64("ovm.boltdb.fileSizeBytes", fileSize), + attribute.Int64("ovm.boltdb.deletedBytes", deletedBytes), + attribute.Int64("ovm.boltdb.compactThresholdBytes", c.CompactThreshold), + ) +} + +// CloseAndDestroy closes the database and deletes the cache file. +// This method makes the destructive behavior explicit. +func (c *BoltCache) CloseAndDestroy() error { + if c == nil { + return nil + } + // Acquire write lock to prevent compaction from interfering + c.compactMutex.Lock() + defer c.compactMutex.Unlock() + + // Get the file path before closing + path := c.db.Path() + + // Close the database + if err := c.db.Close(); err != nil { + return err + } + + // Delete the cache file + return os.Remove(path) +} + +// deleteCacheFile removes the cache file entirely. This is used as a last resort +// when the disk is full and cleanup doesn't help. It closes the database, +// removes the file, and resets internal state. +func (c *BoltCache) deleteCacheFile(ctx context.Context) error { + if c == nil { + return nil + } + + // Create a span for this operation + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFile", trace.WithAttributes( + attribute.String("ovm.cache.path", c.path), + )) + defer span.End() + + // Acquire write lock to prevent compaction from interfering + c.compactMutex.Lock() + defer c.compactMutex.Unlock() + + return c.deleteCacheFileLocked(ctx, span) +} + +// deleteCacheFileLocked is the internal version that assumes the caller already holds compactMutex.Lock() +func (c *BoltCache) deleteCacheFileLocked(ctx context.Context, span trace.Span) error { + // Close the database if it's open + if err := c.db.Close(); err != nil { + span.RecordError(err) + sentry.CaptureException(err) + log.WithContext(ctx).WithError(err).Error("Failed to close database during cache file deletion") + } + + // Remove the cache file + if c.path != "" { + if err := os.Remove(c.path); err != nil && !os.IsNotExist(err) { + span.RecordError(err) + sentry.CaptureException(err) + log.WithContext(ctx).WithError(err).Error("Failed to remove cache file") + return fmt.Errorf("failed to remove cache file: %w", err) + } + span.SetAttributes(attribute.Bool("ovm.cache.file_deleted", true)) + } + + // Reset internal state + c.resetDeletedBytes() + + // Reopen the database + db, err := bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to reopen database") + return fmt.Errorf("failed to reopen database: %w", err) + } + + c.db = db + + // Initialize buckets + if err := c.initBuckets(); err != nil { + _ = db.Close() + return fmt.Errorf("failed to initialize buckets after cache file deletion: %w", err) + } + + return nil +} + +// createDoneFunc returns a done function that calls pending.Complete for the given cache key. +// The done function releases resources and unblocks waiting goroutines. +// The done function is safe to call multiple times (idempotent via sync.Once). +func (c *BoltCache) createDoneFunc(ck CacheKey) func() { + if c == nil || c.pending == nil { + return noopDone + } + key := ck.String() + var once sync.Once + return func() { + once.Do(func() { + c.pending.Complete(key) + }) + } +} + +// Lookup performs a cache lookup for the given query parameters. +func (c *BoltCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.Lookup", + trace.WithAttributes( + attribute.String("ovm.cache.sourceName", srcName), + attribute.String("ovm.cache.method", method.String()), + attribute.String("ovm.cache.scope", scope), + attribute.String("ovm.cache.type", typ), + attribute.String("ovm.cache.query", query), + attribute.Bool("ovm.cache.ignoreCache", ignoreCache), + ), + ) + defer span.End() + + ck := CacheKeyFromParts(srcName, method, scope, typ, query) + + // Set disk usage metrics + c.setDiskUsageAttributes(span) + + if c == nil { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache not initialised"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "cache has not been initialised", + Scope: scope, + SourceName: srcName, + ItemType: typ, + }, noopDone + } + + if ignoreCache { + span.SetAttributes( + attribute.String("ovm.cache.result", "ignore cache"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + // Search already has RLock, so we don't need to add another one here + initialSearchStart := time.Now() + items, err := c.search(ctx, ck) + initialSearchDuration := time.Since(initialSearchStart) + span.SetAttributes( + attribute.Float64("ovm.cache.initialSearchDuration_ms", float64(initialSearchDuration.Milliseconds())), + ) + + if err != nil { + var qErr *sdp.QueryError + if errors.Is(err, ErrCacheNotFound) { + // Cache miss - check if another goroutine is already fetching this data + shouldWork, entry := c.pending.StartWork(ck.String()) + if shouldWork { + // We're the first caller, return miss so caller does the work + span.SetAttributes( + attribute.String("ovm.cache.result", "cache miss"), + attribute.Bool("ovm.cache.hit", false), + attribute.Bool("ovm.cache.workPending", false), + ) + return false, ck, nil, nil, c.createDoneFunc(ck) + } + + // Another goroutine is fetching this data, wait for it to complete + pendingWaitStart := time.Now() + ok := c.pending.Wait(ctx, entry) + pendingWaitDuration := time.Since(pendingWaitStart) + + span.SetAttributes( + attribute.Float64("ovm.cache.pendingWaitDuration_ms", float64(pendingWaitDuration.Milliseconds())), + attribute.Bool("ovm.cache.pendingWaitSuccess", ok), + ) + + if !ok { + // Context was cancelled or work was cancelled, return miss + span.SetAttributes( + attribute.String("ovm.cache.result", "pending work cancelled or timeout"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + // Work is complete, re-check the cache for results + recheckSearchStart := time.Now() + items, recheckErr := c.search(ctx, ck) + recheckSearchDuration := time.Since(recheckSearchStart) + span.SetAttributes( + attribute.Float64("ovm.cache.recheckSearchDuration_ms", float64(recheckSearchDuration.Milliseconds())), + ) + if recheckErr != nil { + if errors.Is(recheckErr, ErrCacheNotFound) { + // Cache still empty after pending work completed + // This is valid - worker may have found nothing or cancelled + span.SetAttributes( + attribute.String("ovm.cache.result", "pending work completed but cache still empty"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + var recheckQErr *sdp.QueryError + if errors.As(recheckErr, &recheckQErr) { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit from pending work: error"), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, nil, recheckQErr, noopDone + } + // Truly unexpected error - return miss + span.SetAttributes( + attribute.String("ovm.cache.result", "unexpected error on re-check"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit from pending work"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, items, nil, noopDone + } else if errors.As(err, &qErr) { + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + span.SetAttributes(attribute.String("ovm.cache.result", "cache hit: item not found")) + } else { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: QueryError"), + attribute.String("ovm.cache.error", err.Error()), + ) + } + + span.SetAttributes(attribute.Bool("ovm.cache.hit", true)) + return true, ck, nil, qErr, noopDone + } else { + qErr = &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + Scope: scope, + SourceName: srcName, + ItemType: typ, + } + + span.SetAttributes( + attribute.String("ovm.cache.error", err.Error()), + attribute.String("ovm.cache.result", "cache hit: unknown QueryError"), + attribute.Bool("ovm.cache.hit", true), + ) + + return true, ck, nil, qErr, noopDone + } + } + + if method == sdp.QueryMethod_GET { + if len(items) < 2 { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: 1 item"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, items, nil, noopDone + } else { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache returned >1 value, purging and continuing"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", false), + ) + // Delete already has Lock(), so we can call it directly + c.Delete(ck) + return false, ck, nil, nil, noopDone + } + } + + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: multiple items"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + + // RLock already released above + return true, ck, items, nil, noopDone +} + +// Search performs a lower-level search using a CacheKey. +// If ctx contains a span, detailed timing metrics will be added as span attributes. +func (c *BoltCache) search(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { + if c == nil { + return nil, nil + } + + // Get span from context if available + span := trace.SpanFromContext(ctx) + + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + lockAcquireStart := time.Now() + c.compactMutex.RLock() + lockAcquireDuration := time.Since(lockAcquireStart) + defer c.compactMutex.RUnlock() + + results := make([]*CachedResult, 0) + var itemsScanned int + + txStart := time.Now() + err := c.db.View(func(tx *bbolt.Tx) error { + items := tx.Bucket(itemsBucketName) + if items == nil { + return nil + } + + sstHash := ck.SST.Hash() + sstBucket := items.Bucket([]byte(sstHash)) + if sstBucket == nil { + return nil + } + + now := time.Now() + + // Scan through all entries in this SST bucket + cursor := sstBucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + itemsScanned++ + entry, err := decodeCachedEntry(v) + if err != nil { + continue // Skip corrupted entries + } + + // Check if expired + expiry := time.Unix(0, entry.GetExpiryUnixNano()) + if expiry.Before(now) { + continue + } + + // Check if matches the cache key + entryIV := IndexValues{ + SSTHash: SSTHash(entry.GetSstHash()), + UniqueAttributeValue: entry.GetUniqueAttributeValue(), + Method: entry.GetMethod(), + Query: entry.GetQuery(), + } + if !ck.Matches(entryIV) { + continue + } + + result := cachedEntryToCachedResult(entry) + results = append(results, result) + } + + return nil + }) + txDuration := time.Since(txStart) + + // Add detailed search metrics to span if available + if span.IsRecording() { + span.SetAttributes( + attribute.Int64("ovm.cache.lockAcquireDuration_ms", lockAcquireDuration.Milliseconds()), + attribute.Int64("ovm.cache.txDuration_ms", txDuration.Milliseconds()), + attribute.Int("ovm.cache.itemsScanned", itemsScanned), + attribute.Int("ovm.cache.itemsReturned", len(results)), + ) + } + + if err != nil { + return nil, fmt.Errorf("search failed: %w", err) + } + + if len(results) == 0 { + return nil, ErrCacheNotFound + } + + // Check for errors first + items := make([]*sdp.Item, 0, len(results)) + for _, res := range results { + if res.Error != nil { + return nil, res.Error + } + + if res.Item != nil { + items = append(items, res.Item) + } + } + + return items, nil +} + +// StoreItem stores an item in the cache with the specified duration. +func (c *BoltCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { + if item == nil || c == nil { + return + } + + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + c.compactMutex.RLock() + defer c.compactMutex.RUnlock() + + methodStr := "" + if ck.Method != nil { + methodStr = ck.Method.String() + } + + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.StoreItem", + trace.WithAttributes( + attribute.String("ovm.cache.method", methodStr), + attribute.String("ovm.cache.scope", ck.SST.Scope), + attribute.String("ovm.cache.type", ck.SST.Type), + attribute.String("ovm.cache.sourceName", ck.SST.SourceName), + attribute.String("ovm.cache.itemType", item.GetType()), + attribute.String("ovm.cache.itemScope", item.GetScope()), + attribute.String("ovm.cache.duration", duration.String()), + ), + ) + defer span.End() + + // Set disk usage metrics + c.setDiskUsageAttributes(span) + + // Ensure minimum duration to avoid items expiring immediately + // This handles cases where time.Until() returns 0 or negative due to timing + // Use 100ms to account for race detector overhead and slow CI environments + if duration <= 100*time.Millisecond { + duration = 100 * time.Millisecond + } + + res := CachedResult{ + Item: item, + Error: nil, + Expiry: time.Now().Add(duration), + IndexValues: IndexValues{ + UniqueAttributeValue: item.UniqueAttributeValue(), + }, + } + + if ck.Method != nil { + res.IndexValues.Method = *ck.Method + } + if ck.Query != nil { + res.IndexValues.Query = *ck.Query + } + + res.IndexValues.SSTHash = ck.SST.Hash() + + c.storeResult(ctx, res) +} + +// StoreError stores an error in the cache with the specified duration. +func (c *BoltCache) StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) { + if c == nil || err == nil { + return + } + + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + c.compactMutex.RLock() + defer c.compactMutex.RUnlock() + + methodStr := "" + if ck.Method != nil { + methodStr = ck.Method.String() + } + + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.StoreError", + trace.WithAttributes( + attribute.String("ovm.cache.method", methodStr), + attribute.String("ovm.cache.scope", ck.SST.Scope), + attribute.String("ovm.cache.type", ck.SST.Type), + attribute.String("ovm.cache.sourceName", ck.SST.SourceName), + attribute.String("ovm.cache.error", err.Error()), + attribute.String("ovm.cache.duration", duration.String()), + ), + ) + defer span.End() + + // Set disk usage metrics + c.setDiskUsageAttributes(span) + + // Ensure minimum duration to avoid items expiring immediately + // Use 100ms to account for race detector overhead and slow CI environments + if duration <= 100*time.Millisecond { + duration = 100 * time.Millisecond + } + + res := CachedResult{ + Item: nil, + Error: err, + Expiry: time.Now().Add(duration), + IndexValues: ck.ToIndexValues(), + } + + c.storeResult(ctx, res) +} + +// storeResult stores a CachedResult in the database +func (c *BoltCache) storeResult(ctx context.Context, res CachedResult) { + span := trace.SpanFromContext(ctx) + + entry, err := fromCachedResult(&res) + if err != nil { + return // Silently fail on serialization errors + } + + entryBytes, err := encodeCachedEntry(entry) + if err != nil { + return + } + + entryKey := makeEntryKey(res.IndexValues, res.Item) + expiryKey := makeExpiryKey(res.Expiry, res.IndexValues.SSTHash, entryKey) + + overwritten := false + entrySize := int64(len(entryBytes)) + + // Helper function to perform the actual database update + performUpdate := func() error { + return c.db.Update(func(tx *bbolt.Tx) error { + items := tx.Bucket(itemsBucketName) + if items == nil { + return errors.New("items bucket not found") + } + + // Get or create the SST sub-bucket + sstBucket, err := items.CreateBucketIfNotExists([]byte(res.IndexValues.SSTHash)) + if err != nil { + return fmt.Errorf("failed to create sst bucket: %w", err) + } + + // Check if we're overwriting an unexpired entry + existingData := sstBucket.Get(entryKey) + if existingData != nil { + existingEntry, err := decodeCachedEntry(existingData) + if err == nil { + existingExpiry := time.Unix(0, existingEntry.GetExpiryUnixNano()) + now := time.Now() + if existingExpiry.After(now) { + overwritten = true + timeUntilExpiry := existingExpiry.Sub(now) + + attrs := []attribute.KeyValue{ + attribute.Bool("ovm.cache.unexpired_overwrite", true), + attribute.String("ovm.cache.time_until_expiry", timeUntilExpiry.String()), + attribute.String("ovm.cache.sst_hash", string(res.IndexValues.SSTHash)), + attribute.String("ovm.cache.query_method", res.IndexValues.Method.String()), + } + + if res.Item != nil { + attrs = append(attrs, + attribute.String("ovm.cache.item_type", res.Item.GetType()), + attribute.String("ovm.cache.item_scope", res.Item.GetScope()), + ) + } + + if res.IndexValues.Query != "" { + attrs = append(attrs, attribute.String("ovm.cache.query", res.IndexValues.Query)) + } + + if res.IndexValues.UniqueAttributeValue != "" { + attrs = append(attrs, attribute.String("ovm.cache.unique_attribute", res.IndexValues.UniqueAttributeValue)) + } + + span.SetAttributes(attrs...) + + // Delete old expiry key + expiry := tx.Bucket(expiryBucketName) + if expiry != nil { + oldExpiryKey := makeExpiryKey(existingExpiry, res.IndexValues.SSTHash, entryKey) + _ = expiry.Delete(oldExpiryKey) + } + } + } + } + + // Store the entry + if err := sstBucket.Put(entryKey, entryBytes); err != nil { + return fmt.Errorf("failed to store entry: %w", err) + } + + // Store in expiry index + expiry := tx.Bucket(expiryBucketName) + if expiry == nil { + return errors.New("expiry bucket not found") + } + if err := expiry.Put(expiryKey, nil); err != nil { + return fmt.Errorf("failed to store expiry: %w", err) + } + + return nil + }) + } + + err = performUpdate() + + // Handle disk full errors + // Note: storeResult is called from StoreItem/StoreError which already holds compactMutex.RLock() + // so we use the locked versions to avoid deadlock + if err != nil && isDiskFullError(err) { + // Attempt cleanup by purging expired items - needs to happen in a + // goroutine to avoid deadlocks and get a fresh write lock + go func() { + // we need a fresh write lock to block concurrent compaction and + // deleteCacheFileLocked operations. Retrying performUpdate under + // the write lock will ensure that only one instance of this + // goroutine will actually perform the deleteCacheFileLocked. + c.compactMutex.Lock() + defer c.compactMutex.Unlock() + + ctx, purgeSpan := tracing.Tracer().Start(ctx, "BoltCache.purgeLocked") + defer purgeSpan.End() + c.purgeLocked(ctx, time.Now()) + + // Retry the write operation once + err = performUpdate() + + // If still failing with disk full, delete the cache entirely - use locked version + if err != nil && isDiskFullError(err) { + deleteCtx, deleteSpan := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFileLocked", trace.WithAttributes( + attribute.String("ovm.cache.path", c.path), + )) + defer deleteSpan.End() + _ = c.deleteCacheFileLocked(deleteCtx, deleteSpan) + // After deleting the cache, we can't store the result, so just return + return + } + }() + // now return to release the read lock and allow the goroutine above to run + return + } + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to store result") + // Update disk usage metrics even on error + c.setDiskUsageAttributes(span) + return + } + + if !overwritten { + span.SetAttributes(attribute.Bool("ovm.cache.unexpired_overwrite", false)) + } + + // Add entry size and update disk usage metrics + span.SetAttributes( + attribute.Int64("ovm.boltdb.entrySizeBytes", entrySize), + ) + c.setDiskUsageAttributes(span) + + // Update the purge time if required + c.setNextPurgeIfEarlier(res.Expiry) +} + +// Delete removes all entries matching the given cache key. +func (c *BoltCache) Delete(ck CacheKey) { + if c == nil { + return + } + + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + c.compactMutex.RLock() + defer c.compactMutex.RUnlock() + + var totalDeleted int64 + + _ = c.db.Update(func(tx *bbolt.Tx) error { + items := tx.Bucket(itemsBucketName) + if items == nil { + return nil + } + + sstHash := ck.SST.Hash() + sstBucket := items.Bucket([]byte(sstHash)) + if sstBucket == nil { + return nil + } + + expiry := tx.Bucket(expiryBucketName) + + // Collect keys to delete + keysToDelete := make([][]byte, 0) + cursor := sstBucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + entry, err := decodeCachedEntry(v) + if err != nil { + continue + } + + entryIV := IndexValues{ + SSTHash: SSTHash(entry.GetSstHash()), + UniqueAttributeValue: entry.GetUniqueAttributeValue(), + Method: entry.GetMethod(), + Query: entry.GetQuery(), + } + if ck.Matches(entryIV) { + keysToDelete = append(keysToDelete, append([]byte(nil), k...)) + totalDeleted += int64(len(k) + len(v)) + + // Delete from expiry index + if expiry != nil { + expiryTime := time.Unix(0, entry.GetExpiryUnixNano()) + expiryKey := makeExpiryKey(expiryTime, SSTHash(entry.GetSstHash()), k) + _ = expiry.Delete(expiryKey) + } + } + } + + // Delete the entries + for _, k := range keysToDelete { + _ = sstBucket.Delete(k) + } + + return nil + }) + + if totalDeleted > 0 { + c.addDeletedBytes(totalDeleted) + } +} + +// Clear removes all entries from the cache. +func (c *BoltCache) Clear() { + if c == nil { + return + } + + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + c.compactMutex.RLock() + defer c.compactMutex.RUnlock() + + _ = c.db.Update(func(tx *bbolt.Tx) error { + // Delete and recreate buckets + _ = tx.DeleteBucket(itemsBucketName) + _ = tx.DeleteBucket(expiryBucketName) + + _, _ = tx.CreateBucketIfNotExists(itemsBucketName) + _, _ = tx.CreateBucketIfNotExists(expiryBucketName) + + // Reset deleted bytes in meta + meta := tx.Bucket(metaBucketName) + if meta != nil { + buf := make([]byte, 8) + _ = meta.Put(deletedBytesKey, buf) + } + + return nil + }) + + c.resetDeletedBytes() +} + +// Purge removes all expired items from the cache. +func (c *BoltCache) Purge(ctx context.Context, before time.Time) PurgeStats { + if c == nil { + return PurgeStats{} + } + + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.Purge", + trace.WithAttributes( + attribute.String("ovm.boltdb.purgeBefore", before.Format(time.RFC3339)), + ), + ) + defer span.End() + + stats := func() PurgeStats { + // Acquire read lock to prevent compaction from closing the database, but do not lock out other bbolt operations + c.compactMutex.RLock() + defer c.compactMutex.RUnlock() + + return c.purgeLocked(ctx, before) + }() + + // Check if compaction is needed + deletedBytesBeforeCompact := c.getDeletedBytes() + compactionTriggered := deletedBytesBeforeCompact >= c.CompactThreshold + + if compactionTriggered { + span.SetAttributes( + attribute.Bool("ovm.boltdb.compactionTriggered", true), + attribute.Int64("ovm.boltdb.deletedBytesBeforeCompact", deletedBytesBeforeCompact), + ) + if err := c.compact(ctx); err == nil { + span.SetAttributes(attribute.Bool("ovm.boltdb.compactionSuccess", true)) + } else { + span.RecordError(err) + span.SetAttributes(attribute.Bool("ovm.boltdb.compactionSuccess", false)) + } + } else { + span.SetAttributes(attribute.Bool("ovm.boltdb.compactionTriggered", false)) + } + + return stats +} + +// purgeLocked is the internal version that assumes the caller already holds compactMutex.Lock() +// It performs the actual purging work and returns the stats, but does not handle compaction. +func (c *BoltCache) purgeLocked(ctx context.Context, before time.Time) PurgeStats { + span := trace.SpanFromContext(ctx) + + // Set initial disk usage metrics + c.setDiskUsageAttributes(span) + + start := time.Now() + var nextExpiry *time.Time + numPurged := 0 + var totalDeleted int64 + + // Collect expired entries + type expiredEntry struct { + sstHash SSTHash + entryKey []byte + size int64 + } + expired := make([]expiredEntry, 0) + + _ = c.db.View(func(tx *bbolt.Tx) error { + expiry := tx.Bucket(expiryBucketName) + if expiry == nil { + return nil + } + + items := tx.Bucket(itemsBucketName) + + cursor := expiry.Cursor() + for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { + expiryTime, sstHash, entryKey, err := parseExpiryKey(k) + if err != nil { + continue + } + + if expiryTime.Before(before) { + // Calculate size for deleted bytes tracking + var size int64 + if items != nil { + if sstBucket := items.Bucket([]byte(sstHash)); sstBucket != nil { + if v := sstBucket.Get(entryKey); v != nil { + size = int64(len(k) + len(entryKey) + len(v)) + } + } + } + expired = append(expired, expiredEntry{ + sstHash: sstHash, + entryKey: append([]byte(nil), entryKey...), + size: size, + }) + } else { + // Found first non-expired entry + nextExpiry = &expiryTime + break + } + } + + return nil + }) + + // Delete expired entries + if len(expired) > 0 { + _ = c.db.Update(func(tx *bbolt.Tx) error { + items := tx.Bucket(itemsBucketName) + expiry := tx.Bucket(expiryBucketName) + + for _, e := range expired { + // Delete from items + if items != nil { + if sstBucket := items.Bucket([]byte(e.sstHash)); sstBucket != nil { + _ = sstBucket.Delete(e.entryKey) + } + } + + // Delete from expiry index + if expiry != nil { + // We need to reconstruct the expiry key + cursor := expiry.Cursor() + for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { + _, sstHash, entryKey, err := parseExpiryKey(k) + if err != nil { + continue + } + if sstHash == e.sstHash && bytes.Equal(entryKey, e.entryKey) { + _ = expiry.Delete(k) + break + } + } + } + + totalDeleted += e.size + numPurged++ + } + + // Save deleted bytes + c.addDeletedBytes(totalDeleted) + return c.saveDeletedBytes(tx) + }) + } + + // Update final disk usage metrics + c.setDiskUsageAttributes(span) + + span.SetAttributes( + attribute.Int("ovm.boltdb.numPurged", numPurged), + attribute.Int64("ovm.boltdb.totalDeletedBytes", totalDeleted), + ) + if nextExpiry != nil { + span.SetAttributes(attribute.String("ovm.boltdb.nextExpiry", nextExpiry.Format(time.RFC3339))) + } + + return PurgeStats{ + NumPurged: numPurged, + TimeTaken: time.Since(start), + NextExpiry: nextExpiry, + } +} + +// compact performs database compaction to reclaim disk space +func (c *BoltCache) compact(ctx context.Context) error { + // Acquire global lock to prevent any concurrent bbolt operations + c.compactMutex.Lock() + defer c.compactMutex.Unlock() + + ctx, span := tracing.Tracer().Start(ctx, "BoltCache.compact") + defer span.End() + + // Set initial disk usage metrics + c.setDiskUsageAttributes(span) + + fileSizeBefore := c.getFileSize() + if fileSizeBefore > 0 { + span.SetAttributes(attribute.Int64("ovm.boltdb.fileSizeBeforeBytes", fileSizeBefore)) + } + + // Create a temporary file for the compacted database + tempPath := c.path + ".compact" + + // Helper to handle disk full errors during file operations + // Note: We already hold compactMutex.Lock(), so we use the locked versions + handleDiskFull := func(err error, operation string) error { + if isDiskFullError(err) { + // Attempt cleanup first - use locked version since we already hold the lock + c.purgeLocked(ctx, time.Now()) + // If cleanup didn't help, delete the cache - use locked version + deleteCtx, deleteSpan := tracing.Tracer().Start(ctx, "BoltCache.deleteCacheFileLocked", trace.WithAttributes( + attribute.String("ovm.cache.path", c.path), + )) + defer deleteSpan.End() + _ = c.deleteCacheFileLocked(deleteCtx, deleteSpan) + return fmt.Errorf("disk full during %s, cache deleted: %w", operation, err) + } + return err + } + + // Open the destination database + dstDB, err := bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + if err != nil { + if isDiskFullError(err) { + // Attempt cleanup first - use locked version since we already hold the lock + c.purgeLocked(ctx, time.Now()) + // Try again + dstDB, err = bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + if err != nil { + return handleDiskFull(err, "temp database creation") + } + } else { + return fmt.Errorf("failed to create temp database: %w", err) + } + } + + // Compact from source to destination + if err := bbolt.Compact(dstDB, c.db, 0); err != nil { + dstDB.Close() + os.Remove(tempPath) + if isDiskFullError(err) { + // Attempt cleanup first - use locked version since we already hold the lock + c.purgeLocked(ctx, time.Now()) + // Try compaction again + dstDB2, retryErr := bbolt.Open(tempPath, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + if retryErr != nil { + return handleDiskFull(retryErr, "temp database creation after cleanup") + } + if compactErr := bbolt.Compact(dstDB2, c.db, 0); compactErr != nil { + dstDB2.Close() + os.Remove(tempPath) + return handleDiskFull(compactErr, "compaction after cleanup") + } + // Success on retry, continue with dstDB2 + dstDB = dstDB2 + } else { + return fmt.Errorf("compaction failed: %w", err) + } + } + + // Close the destination database + if err := dstDB.Close(); err != nil { + os.Remove(tempPath) + return fmt.Errorf("failed to close temp database: %w", err) + } + + // Close the current database + if err := c.db.Close(); err != nil { + os.Remove(tempPath) + return fmt.Errorf("failed to close database: %w", err) + } + + // Replace the old file with the compacted one + if err := os.Rename(tempPath, c.path); err != nil { + // Try to reopen the original database + c.db, _ = bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + return handleDiskFull(err, "rename") + } + + // Reopen the database + db, err := bbolt.Open(c.path, 0o600, &bbolt.Options{Timeout: 5 * time.Second}) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to reopen database") + return fmt.Errorf("failed to reopen database: %w", err) + } + + c.db = db + + // Set final disk usage metrics and compaction results + fileSizeAfter := c.getFileSize() + spaceReclaimed := fileSizeBefore - fileSizeAfter + + span.SetAttributes( + attribute.Int64("ovm.boltdb.fileSizeAfterBytes", fileSizeAfter), + attribute.Int64("ovm.boltdb.spaceReclaimedBytes", spaceReclaimed), + ) + c.setDiskUsageAttributes(span) + + // update deleted bytes after compaction + c.resetDeletedBytes() + _ = c.db.Update(func(tx *bbolt.Tx) error { + return c.saveDeletedBytes(tx) + }) + + return nil +} + +// GetMinWaitTime returns the minimum time between purge operations +func (c *BoltCache) GetMinWaitTime() time.Duration { + if c == nil { + return 0 + } + + if c.MinWaitTime == 0 { + return MinWaitDefault + } + + return c.MinWaitTime +} + +// StartPurger starts a background goroutine that automatically purges expired items. +func (c *BoltCache) StartPurger(ctx context.Context) { + if c == nil { + return + } + + c.purgeMutex.Lock() + if c.purgeTimer == nil { + c.purgeTimer = time.NewTimer(0) + c.purgeMutex.Unlock() + } else { + c.purgeMutex.Unlock() + log.WithContext(ctx).Info("Purger already running") + return // the purger is already running, so we don't need to start it again + } + + go func(ctx context.Context) { + for { + select { + case <-c.purgeTimer.C: + stats := c.Purge(ctx, time.Now()) + c.setNextPurgeFromStats(stats) + case <-ctx.Done(): + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + c.purgeTimer.Stop() + c.purgeTimer = nil + return + } + } + }(ctx) +} + +// setNextPurgeFromStats sets when the next purge should run based on the stats +func (c *BoltCache) setNextPurgeFromStats(stats PurgeStats) { + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + if stats.NextExpiry == nil { + c.purgeTimer.Reset(1000 * time.Hour) + c.nextPurge = time.Now().Add(1000 * time.Hour) + } else { + if time.Until(*stats.NextExpiry) < c.GetMinWaitTime() { + c.purgeTimer.Reset(c.GetMinWaitTime()) + c.nextPurge = time.Now().Add(c.GetMinWaitTime()) + } else { + c.purgeTimer.Reset(time.Until(*stats.NextExpiry)) + c.nextPurge = *stats.NextExpiry + } + } +} + +// setNextPurgeIfEarlier sets the next purge time if the provided time is sooner +func (c *BoltCache) setNextPurgeIfEarlier(t time.Time) { + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + if t.Before(c.nextPurge) { + if c.purgeTimer == nil { + return + } + + c.purgeTimer.Stop() + c.nextPurge = t + c.purgeTimer.Reset(time.Until(t)) + } +} diff --git a/go/sdpcache/cache.go b/go/sdpcache/cache.go new file mode 100644 index 00000000..d6089394 --- /dev/null +++ b/go/sdpcache/cache.go @@ -0,0 +1,1009 @@ +package sdpcache + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/getsentry/sentry-go" + "github.com/google/btree" + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" +) + +// noopDone is a reusable no-op done function returned when no cleanup is needed +var noopDone = func() {} + +type IndexValues struct { + SSTHash SSTHash + UniqueAttributeValue string + Method sdp.QueryMethod + Query string +} + +type CacheKey struct { + SST SST // *required + UniqueAttributeValue *string + Method *sdp.QueryMethod + Query *string +} + +func CacheKeyFromParts(srcName string, method sdp.QueryMethod, scope string, typ string, query string) CacheKey { + ck := CacheKey{ + SST: SST{ + SourceName: srcName, + Scope: scope, + Type: typ, + }, + } + + switch method { + case sdp.QueryMethod_GET: + // With a Get query we need just the one specific item, so also + // filter on uniqueAttributeValue + ck.UniqueAttributeValue = &query + case sdp.QueryMethod_LIST: + // In the case of a find, we just want everything that was found in + // the last find, so we only care about the method + ck.Method = &method + case sdp.QueryMethod_SEARCH: + // For a search, we only want to get from the cache items that were + // found using search, and with the exact same query + ck.Method = &method + ck.Query = &query + } + + return ck +} + +func CacheKeyFromQuery(q *sdp.Query, srcName string) CacheKey { + return CacheKeyFromParts(srcName, q.GetMethod(), q.GetScope(), q.GetType(), q.GetQuery()) +} + +func (ck CacheKey) String() string { + fields := []string{ + ("SourceName=" + ck.SST.SourceName), + ("Scope=" + ck.SST.Scope), + ("Type=" + ck.SST.Type), + } + + if ck.UniqueAttributeValue != nil { + fields = append(fields, ("UniqueAttributeValue=" + *ck.UniqueAttributeValue)) + } + + if ck.Method != nil { + fields = append(fields, ("Method=" + ck.Method.String())) + } + + if ck.Query != nil { + fields = append(fields, ("Query=" + *ck.Query)) + } + + return strings.Join(fields, ", ") +} + +// ToIndexValues Converts a cache query to a set of index values +func (ck CacheKey) ToIndexValues() IndexValues { + iv := IndexValues{ + SSTHash: ck.SST.Hash(), + } + + if ck.Method != nil { + iv.Method = *ck.Method + } + + if ck.Query != nil { + iv.Query = *ck.Query + } + + if ck.UniqueAttributeValue != nil { + iv.UniqueAttributeValue = *ck.UniqueAttributeValue + } + + return iv +} + +// Matches Returns whether or not the supplied index values match the +// CacheQuery, excluding the SST since this will have already been validated. +// Note that this only checks values that ave actually been set in the +// CacheQuery +func (ck CacheKey) Matches(i IndexValues) bool { + // Check for any mismatches on the values that are set + if ck.Method != nil { + if *ck.Method != i.Method { + return false + } + } + + if ck.Query != nil { + if *ck.Query != i.Query { + return false + } + } + + if ck.UniqueAttributeValue != nil { + if *ck.UniqueAttributeValue != i.UniqueAttributeValue { + return false + } + } + + return true +} + +var ErrCacheNotFound = errors.New("not found in cache") + +// SST A combination of SourceName, Scope and Type, all of which must be +// provided +type SST struct { + SourceName string + Scope string + Type string +} + +// Hash Creates a new SST hash from a given SST +func (s SST) Hash() SSTHash { + h := sha256.New() + h.Write([]byte(s.SourceName)) + h.Write([]byte(s.Scope)) + h.Write([]byte(s.Type)) + + sum := make([]byte, 0) + sum = h.Sum(sum) + + return SSTHash(fmt.Sprintf("%x", sum)) +} + +// CachedResult An item including cache metadata +type CachedResult struct { + // Item is the actual cached item + Item *sdp.Item + + // Error is the error that we want + Error error + + // The time at which this item expires + Expiry time.Time + + // Values that we use for calculating indexes + IndexValues IndexValues +} + +// SSTHash Represents the hash of `SourceName`, `Scope` and `Type` +type SSTHash string + +// Cache provides operations for caching SDP items and errors +type Cache interface { + // Lookup performs a cache lookup for the given query parameters. + // Returns: (cache hit, cache key, items, query error, done function) + // + // If hit=false, you MUST call the returned done function when finished, after storing all + // items/errors. The done function releases resources and unblocks waiting goroutines. + // You should defer the done function immediately after calling Lookup to ensure it's called. + // + // Example usage for cache miss: + // hit, ck, _, _, done := cache.Lookup(...) + // defer done() // MUST be called when finished + // if !hit { + // // Store all items first + // for _, item := range items { + // cache.StoreItem(ctx, item, duration, ck) + // } + // // done() is called via defer, releasing waiters + // } + // + // The done function is safe to call multiple times (idempotent). + // For cache hits or waiting goroutines, the done function is a no-op. + Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) + + // StoreItem stores an item in the cache with the specified duration. + StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) + + // StoreError stores an error in the cache with the specified duration. + StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) + + // Delete removes all entries matching the given cache key. + Delete(ck CacheKey) + + // Clear removes all entries from the cache. + Clear() + + // Purge removes all expired items from the cache. + // Returns statistics about the purge operation. + Purge(ctx context.Context, before time.Time) PurgeStats + + // GetMinWaitTime returns the minimum time between purge operations + GetMinWaitTime() time.Duration + + // StartPurger starts a background goroutine that automatically purges expired items. + // The purger will stop when the context is cancelled. + StartPurger(ctx context.Context) +} + +// NoOpCache is a cache implementation that does nothing. +// It can be used in tests or when caching is not desired, avoiding nil checks. +type NoOpCache struct{} + +// NewNoOpCache creates a new no-op cache that implements the Cache interface +// but performs no operations. Useful for testing or when caching is disabled. +func NewNoOpCache() Cache { + return &NoOpCache{} +} + +// Lookup always returns a cache miss +func (n *NoOpCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { + ck := CacheKeyFromParts(srcName, method, scope, typ, query) + return false, ck, nil, nil, noopDone +} + +// StoreItem does nothing +func (n *NoOpCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { + // No-op +} + +// StoreError does nothing +func (n *NoOpCache) StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) { + // No-op +} + +// Delete does nothing +func (n *NoOpCache) Delete(ck CacheKey) { + // No-op +} + +// Clear does nothing +func (n *NoOpCache) Clear() { + // No-op +} + +// Purge returns empty stats +func (n *NoOpCache) Purge(ctx context.Context, before time.Time) PurgeStats { + return PurgeStats{} +} + +// GetMinWaitTime returns 0 +func (n *NoOpCache) GetMinWaitTime() time.Duration { + return 0 +} + +// StartPurger does nothing +func (n *NoOpCache) StartPurger(ctx context.Context) { +} + +type MemoryCache struct { + // Minimum amount of time to wait between cache purges + MinWaitTime time.Duration + + // The timer that is used to trigger the next purge + purgeTimer *time.Timer + + // The time that the purger will run next + nextPurge time.Time + + indexes map[SSTHash]*indexSet + + // This index is used to track item expiries, since items can have different + // expiry durations we need to use a btree here rather than just appending + // to a slice or something. The purge process uses this to determine what + // needs deleting, then calls into each specific index to delete as required + expiryIndex *btree.BTreeG[*CachedResult] + + // Mutex for reading caches + indexMutex sync.RWMutex + + // Ensures that purge stats like `purgeTimer` and `nextPurge` aren't being + // modified concurrently + purgeMutex sync.Mutex + + // Tracks in-flight lookups to prevent duplicate work when multiple + // goroutines request the same cache key simultaneously + pending *pendingWork +} + +// NewMemoryCache creates a new in-memory cache implementation +func NewMemoryCache() *MemoryCache { + return &MemoryCache{ + indexes: make(map[SSTHash]*indexSet), + expiryIndex: newExpiryIndex(), + pending: newPendingWork(), + } +} + +// NewCache creates a new cache. This function returns a Cache interface. +// Currently, it returns a file-based implementation. The passed context will be +// used to start the purger. +func NewCache(ctx context.Context) Cache { + tmpFile, err := os.CreateTemp("", "sdpcache-*.db") + // close the file so bbolt can open it, but keep the file on disk. We don't + // need to check for errors since we're not using the file + _ = tmpFile.Close() + + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Failed to create temporary file for BoltCache, using memory cache instead") + cache := NewMemoryCache() + cache.StartPurger(ctx) + return cache + } + cache, err := NewBoltCache( + tmpFile.Name(), + WithMinWaitTime(30*time.Second), + // allocate 1GB of disk space for the cache (with 1GB additional for compaction temp file) + WithCompactThreshold(1*1024*1024*1024), + ) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Failed to create BoltCache, using memory cache instead") + _ = os.Remove(tmpFile.Name()) + cache := NewMemoryCache() + cache.StartPurger(ctx) + return cache + } + cache.StartPurger(ctx) + return cache +} + +func newExpiryIndex() *btree.BTreeG[*CachedResult] { + return btree.NewG(2, func(a, b *CachedResult) bool { + return a.Expiry.Before(b.Expiry) + }) +} + +type indexSet struct { + uniqueAttributeValueIndex *btree.BTreeG[*CachedResult] + methodIndex *btree.BTreeG[*CachedResult] + queryIndex *btree.BTreeG[*CachedResult] +} + +func newIndexSet() *indexSet { + return &indexSet{ + uniqueAttributeValueIndex: btree.NewG(2, func(a, b *CachedResult) bool { + return sortString(a.IndexValues.UniqueAttributeValue, a.Item) < sortString(b.IndexValues.UniqueAttributeValue, b.Item) + }), + methodIndex: btree.NewG(2, func(a, b *CachedResult) bool { + return sortString(a.IndexValues.Method.String(), a.Item) < sortString(b.IndexValues.Method.String(), b.Item) + }), + queryIndex: btree.NewG(2, func(a, b *CachedResult) bool { + return sortString(a.IndexValues.Query, a.Item) < sortString(b.IndexValues.Query, b.Item) + }), + } +} + +// createDoneFunc returns a done function that calls pending.Complete for the given cache key. +// The done function releases resources and unblocks waiting goroutines. +// The done function is safe to call multiple times (idempotent via sync.Once). +func (c *MemoryCache) createDoneFunc(ck CacheKey) func() { + if c == nil || c.pending == nil { + return noopDone + } + key := ck.String() + var once sync.Once + return func() { + once.Do(func() { + c.pending.Complete(key) + }) + } +} + +// Lookup returns true/false whether or not the cache has a result for the given +// query. If there are results, they will be returned as slice of `sdp.Item`s or +// an `*sdp.QueryError`. +// The CacheKey is always returned, even if the lookup otherwise fails or errors +func (c *MemoryCache) Lookup(ctx context.Context, srcName string, method sdp.QueryMethod, scope string, typ string, query string, ignoreCache bool) (bool, CacheKey, []*sdp.Item, *sdp.QueryError, func()) { + span := trace.SpanFromContext(ctx) + ck := CacheKeyFromParts(srcName, method, scope, typ, query) + + if c == nil { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache not initialised"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "cache has not been initialised", + Scope: scope, + SourceName: srcName, + ItemType: typ, + }, noopDone + } + + if ignoreCache { + span.SetAttributes( + attribute.String("ovm.cache.result", "ignore cache"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + items, err := c.search(ctx, ck) + if err != nil { + var qErr *sdp.QueryError + if errors.Is(err, ErrCacheNotFound) { + // Cache miss - check if another goroutine is already fetching this data + shouldWork, entry := c.pending.StartWork(ck.String()) + if shouldWork { + // We're the first caller, return miss so caller does the work + span.SetAttributes( + attribute.String("ovm.cache.result", "cache miss"), + attribute.Bool("ovm.cache.hit", false), + attribute.Bool("ovm.cache.workPending", false), + ) + return false, ck, nil, nil, c.createDoneFunc(ck) + } + + // Another goroutine is fetching this data, wait for it to complete + ok := c.pending.Wait(ctx, entry) + if !ok { + // Context was cancelled or work was cancelled, return miss + span.SetAttributes( + attribute.String("ovm.cache.result", "pending work cancelled or timeout"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + // Work is complete, re-check the cache for results + items, recheckErr := c.search(ctx, ck) + if recheckErr != nil { + if errors.Is(recheckErr, ErrCacheNotFound) { + // Cache still empty after pending work completed + // This is valid - worker may have found nothing or cancelled + span.SetAttributes( + attribute.String("ovm.cache.result", "pending work completed but cache still empty"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + var recheckQErr *sdp.QueryError + if errors.As(recheckErr, &recheckQErr) { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit from pending work: error"), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, nil, recheckQErr, noopDone + } + // Truly unexpected error - return miss + span.SetAttributes( + attribute.String("ovm.cache.result", "unexpected error on re-check"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit from pending work"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, items, nil, noopDone + } else if errors.As(err, &qErr) { + if qErr.GetErrorType() == sdp.QueryError_NOTFOUND { + span.SetAttributes(attribute.String("ovm.cache.result", "cache hit: item not found")) + } else { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: QueryError"), + attribute.String("ovm.cache.error", err.Error()), + ) + } + + span.SetAttributes(attribute.Bool("ovm.cache.hit", true)) + return true, ck, nil, qErr, noopDone + } else { + // If it's an unknown error, convert it to SDP and skip this source + qErr = &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + Scope: scope, + SourceName: srcName, + ItemType: typ, + } + + span.SetAttributes( + attribute.String("ovm.cache.error", err.Error()), + attribute.String("ovm.cache.result", "cache hit: unknown QueryError"), + attribute.Bool("ovm.cache.hit", true), + ) + + return true, ck, nil, qErr, noopDone + } + } + + if method == sdp.QueryMethod_GET { + // If the method was Get we should validate that we have + // only pulled one thing from the cache + + if len(items) < 2 { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: 1 item"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + return true, ck, items, nil, noopDone + } else { + span.SetAttributes( + attribute.String("ovm.cache.result", "cache returned >1 value, purging and continuing"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", false), + ) + c.Delete(ck) + return false, ck, nil, nil, noopDone + } + } + + span.SetAttributes( + attribute.String("ovm.cache.result", "cache hit: multiple items"), + attribute.Int("ovm.cache.numItems", len(items)), + attribute.Bool("ovm.cache.hit", true), + ) + + return true, ck, items, nil, noopDone +} + +// Search Runs a given query against the cache. If a cached error is found it +// will be returned immediately, if nothing is found a ErrCacheNotFound will +// be returned. Otherwise this will return items that match ALL of the given +// query parameters. Context is accepted for tracing but not currently used +// by MemoryCache (no I/O operations). +func (c *MemoryCache) search(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { + if c == nil { + return nil, nil + } + + items := make([]*sdp.Item, 0) + + results := c.getResults(ck) + + if len(results) == 0 { + return nil, ErrCacheNotFound + } + + now := time.Now() + + // If there is an error we want to return that, so we need to range over the + // results and separate items and errors. This is computationally less + // efficient than extracting errors inside of `getResults()` but logically + // it's a lot less complicated since `Delete()` uses the same method but + // applies different logic + for _, res := range results { + // Check if the cached result has expired + if res.Expiry.Before(now) { + // Skip expired results + continue + } + + if res.Error != nil { + return nil, res.Error + } + + // Return a copy of the item so the user can do whatever they want with + // it + itemCopy := proto.Clone(res.Item).(*sdp.Item) + + items = append(items, itemCopy) + } + + // If all results were expired, return cache not found + if len(items) == 0 { + return nil, ErrCacheNotFound + } + + return items, nil +} + +// Delete Deletes anything that matches the given cache query +func (c *MemoryCache) Delete(ck CacheKey) { + if c == nil { + return + } + + c.deleteResults(c.getResults(ck)) +} + +// getResults Searches indexes for cached results, doing no other logic. If +// nothing is found an empty slice will be returned. +func (c *MemoryCache) getResults(ck CacheKey) []*CachedResult { + c.indexMutex.RLock() + defer c.indexMutex.RUnlock() + + results := make([]*CachedResult, 0) + + // Get the relevant set of indexes based on the SST Hash + sstHash := ck.SST.Hash() + indexes, exists := c.indexes[sstHash] + pivot := CachedResult{ + IndexValues: IndexValues{ + SSTHash: sstHash, + }, + } + + if !exists { + // If we don't have a set of indexes then it definitely doesn't exist + return results + } + + // Start with the most specific index and fall back to the least specific. + // Checking all matching items and returning. These is no need to check all + // indexes since they all have the same content + if ck.UniqueAttributeValue != nil { + pivot.IndexValues.UniqueAttributeValue = *ck.UniqueAttributeValue + + indexes.uniqueAttributeValueIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { + if *ck.UniqueAttributeValue == result.IndexValues.UniqueAttributeValue { + if ck.Matches(result.IndexValues) { + results = append(results, result) + } + + // Always return true so that we continue to iterate + return true + } + + return false + }) + + return results + } + + if ck.Query != nil { + pivot.IndexValues.Query = *ck.Query + + indexes.queryIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { + if *ck.Query == result.IndexValues.Query { + if ck.Matches(result.IndexValues) { + results = append(results, result) + } + + // Always return true so that we continue to iterate + return true + } + + return false + }) + + return results + } + + if ck.Method != nil { + pivot.IndexValues.Method = *ck.Method + + indexes.methodIndex.AscendGreaterOrEqual(&pivot, func(result *CachedResult) bool { + if *ck.Method == result.IndexValues.Method { + // If the methods match, check the rest + if ck.Matches(result.IndexValues) { + results = append(results, result) + } + + // Always return true so that we continue to iterate + return true + } + + return false + }) + + return results + } + + // If nothing other than SST has been set then return everything + indexes.methodIndex.Ascend(func(result *CachedResult) bool { + results = append(results, result) + + return true + }) + + return results +} + +// StoreItem Stores an item in the cache. Note that this item must be fully +// populated (including metadata) for indexing to work correctly +func (c *MemoryCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { + if item == nil || c == nil { + return + } + + itemCopy := proto.Clone(item).(*sdp.Item) + + res := CachedResult{ + Item: itemCopy, + Error: nil, + Expiry: time.Now().Add(duration), + IndexValues: IndexValues{ + UniqueAttributeValue: itemCopy.UniqueAttributeValue(), + }, + } + + if ck.Method != nil { + res.IndexValues.Method = *ck.Method + } + if ck.Query != nil { + res.IndexValues.Query = *ck.Query + } + + res.IndexValues.SSTHash = ck.SST.Hash() + + c.storeResult(ctx, res) +} + +// StoreError Stores an error for the given duration. +func (c *MemoryCache) StoreError(ctx context.Context, err error, duration time.Duration, cacheQuery CacheKey) { + if c == nil || err == nil { + return + } + + res := CachedResult{ + Item: nil, + Error: err, + Expiry: time.Now().Add(duration), + IndexValues: cacheQuery.ToIndexValues(), + } + + c.storeResult(ctx, res) +} + +// Clear Delete all data in cache +func (c *MemoryCache) Clear() { + if c == nil { + return + } + + c.indexMutex.Lock() + defer c.indexMutex.Unlock() + + c.indexes = make(map[SSTHash]*indexSet) + c.expiryIndex = newExpiryIndex() +} + +func (c *MemoryCache) storeResult(ctx context.Context, res CachedResult) { + c.indexMutex.Lock() + defer c.indexMutex.Unlock() + + // Create the index if it doesn't exist + indexes, ok := c.indexes[res.IndexValues.SSTHash] + + if !ok { + indexes = newIndexSet() + c.indexes[res.IndexValues.SSTHash] = indexes + } + + // Add the item to the indexes and check if we're overwriting an unexpired entry + // We only need to check one index since they all reference the same CachedResult + oldResult, replaced := indexes.methodIndex.ReplaceOrInsert(&res) + indexes.queryIndex.ReplaceOrInsert(&res) + indexes.uniqueAttributeValueIndex.ReplaceOrInsert(&res) + + // Get the current span to add attributes + span := trace.SpanFromContext(ctx) + + // Check if we overwrote an entry that hasn't expired yet + // This indicates potential thundering-herd issues where multiple identical + // queries are executed concurrently instead of waiting for the first result + overwritten := false + if replaced && oldResult != nil { + now := time.Now() + if oldResult.Expiry.After(now) { + overwritten = true + timeUntilExpiry := oldResult.Expiry.Sub(now) + + // Build attributes for the overwrite event + attrs := []attribute.KeyValue{ + attribute.Bool("ovm.cache.unexpired_overwrite", true), + attribute.String("ovm.cache.time_until_expiry", timeUntilExpiry.String()), + attribute.String("ovm.cache.sst_hash", string(res.IndexValues.SSTHash)), + attribute.String("ovm.cache.query_method", res.IndexValues.Method.String()), + } + + if res.Item != nil { + attrs = append(attrs, + attribute.String("ovm.cache.item_type", res.Item.GetType()), + attribute.String("ovm.cache.item_scope", res.Item.GetScope()), + ) + } + + if res.IndexValues.Query != "" { + attrs = append(attrs, attribute.String("ovm.cache.query", res.IndexValues.Query)) + } + + if res.IndexValues.UniqueAttributeValue != "" { + attrs = append(attrs, attribute.String("ovm.cache.unique_attribute", res.IndexValues.UniqueAttributeValue)) + } + + span.SetAttributes(attrs...) + } + } + + // Always set the overwrite attribute, even if false, for consistent tracking + if !overwritten { + span.SetAttributes(attribute.Bool("ovm.cache.unexpired_overwrite", false)) + } + + // Add the item to the expiry index + c.expiryIndex.ReplaceOrInsert(&res) + + // Update the purge time if required + c.setNextPurgeIfEarlier(res.Expiry) +} + +// sortString Returns the string that the cached result should be sorted on. +// This has a prefix of the index value and suffix of the GloballyUniqueName if +// relevant +func sortString(indexValue string, item *sdp.Item) string { + if item == nil { + return indexValue + } else { + return indexValue + item.GloballyUniqueName() + } +} + +// PurgeStats Stats about the Purge +type PurgeStats struct { + // How many items were timed out of the cache + NumPurged int + // How long purging took overall + TimeTaken time.Duration + // The expiry time of the next item to expire. If there are no more items in + // the cache, this will be nil + NextExpiry *time.Time +} + +// deleteResults Deletes many cached results at once +func (c *MemoryCache) deleteResults(results []*CachedResult) { + c.indexMutex.Lock() + defer c.indexMutex.Unlock() + + for _, res := range results { + if indexSet, ok := c.indexes[res.IndexValues.SSTHash]; ok { + // For each expired item, delete it from all of the indexes that it will be in + if indexSet.methodIndex != nil { + indexSet.methodIndex.Delete(res) + } + if indexSet.queryIndex != nil { + indexSet.queryIndex.Delete(res) + } + if indexSet.uniqueAttributeValueIndex != nil { + indexSet.uniqueAttributeValueIndex.Delete(res) + } + } + + c.expiryIndex.Delete(res) + } +} + +// Purge Purges all expired items from the cache. The user must pass in the +// `before` time. All items that expired before this will be purged. Usually +// this would be just `time.Now()` however it could be overridden for testing +func (c *MemoryCache) Purge(ctx context.Context, before time.Time) PurgeStats { + if c == nil { + return PurgeStats{} + } + + // Store the current time rather than calling it a million times + start := time.Now() + + var nextExpiry *time.Time + + expired := make([]*CachedResult, 0) + + // Look through the expiry cache and work out what has expired + c.indexMutex.RLock() + c.expiryIndex.Ascend(func(res *CachedResult) bool { + if res.Expiry.Before(before) { + expired = append(expired, res) + + return true + } + + // Take note of the next expiry so we can schedule the next run + nextExpiry = &res.Expiry + + // As soon as hit this we'll stop ascending + return false + }) + c.indexMutex.RUnlock() + + c.deleteResults(expired) + + return PurgeStats{ + NumPurged: len(expired), + TimeTaken: time.Since(start), + NextExpiry: nextExpiry, + } +} + +// MinWaitDefault The default minimum wait time +const MinWaitDefault = (5 * time.Second) + +// GetMinWaitTime Returns the minimum wait time or the default if not set +func (c *MemoryCache) GetMinWaitTime() time.Duration { + if c == nil { + return 0 + } + + if c.MinWaitTime == 0 { + return MinWaitDefault + } + + return c.MinWaitTime +} + +// StartPurger Starts the purge process in the background, it will be cancelled +// when the context is cancelled. The cache will be purged initially, at which +// point the process will sleep until the next time an item expires +func (c *MemoryCache) StartPurger(ctx context.Context) { + if c == nil { + return + } + + c.purgeMutex.Lock() + if c.purgeTimer == nil { + c.purgeTimer = time.NewTimer(0) + c.purgeMutex.Unlock() + } else { + c.purgeMutex.Unlock() + log.WithContext(ctx).Info("Purger already running") + return // the purger is already running, so we don't need to start it again + } + + go func(ctx context.Context) { + for { + select { + case <-c.purgeTimer.C: + stats := c.Purge(ctx, time.Now()) + + c.setNextPurgeFromStats(stats) + case <-ctx.Done(): + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + c.purgeTimer.Stop() + c.purgeTimer = nil + return + } + } + }(ctx) +} + +// setNextPurgeFromStats Sets when the next purge should run based on the stats of the +// previous purge +func (c *MemoryCache) setNextPurgeFromStats(stats PurgeStats) { + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + if stats.NextExpiry == nil { + // If there is nothing else in the cache, wait basically + // forever + c.purgeTimer.Reset(1000 * time.Hour) + c.nextPurge = time.Now().Add(1000 * time.Hour) + } else { + if time.Until(*stats.NextExpiry) < c.GetMinWaitTime() { + c.purgeTimer.Reset(c.GetMinWaitTime()) + c.nextPurge = time.Now().Add(c.GetMinWaitTime()) + } else { + c.purgeTimer.Reset(time.Until(*stats.NextExpiry)) + c.nextPurge = *stats.NextExpiry + } + } +} + +// setNextPurgeIfEarlier Sets the next time the purger will run, if the provided +// time is sooner than the current scheduled purge time. While the purger is +// active this will be constantly updated, however if the purger is sleeping and +// new items are added this method ensures that the purger is woken up +func (c *MemoryCache) setNextPurgeIfEarlier(t time.Time) { + c.purgeMutex.Lock() + defer c.purgeMutex.Unlock() + + if t.Before(c.nextPurge) { + if c.purgeTimer == nil { + return + } + + c.purgeTimer.Stop() + c.nextPurge = t + c.purgeTimer.Reset(time.Until(t)) + } +} diff --git a/go/sdpcache/cache_benchmark_test.go b/go/sdpcache/cache_benchmark_test.go new file mode 100644 index 00000000..893fda93 --- /dev/null +++ b/go/sdpcache/cache_benchmark_test.go @@ -0,0 +1,774 @@ +package sdpcache + +import ( + "context" + "fmt" + "math/rand" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" +) + +const CacheDuration = 10 * time.Second + +// NewPopulatedCache Returns a newly populated cache and the CacheQuery that +// matches a randomly selected item in that cache +func NewPopulatedCache(ctx context.Context, numberItems int) (Cache, CacheKey) { + // Populate the cache + c := NewCache(ctx) + + var item *sdp.Item + var exampleCk CacheKey + exampleIndex := rand.Intn(numberItems) + + for i := range numberItems { + item = GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + if i == exampleIndex { + exampleCk = ck + } + + c.StoreItem(ctx, item, CacheDuration, ck) + } + + return c, exampleCk +} + +// NewPopulatedCacheWithListItems populates a cache with items that share the same +// SST (Source, Scope, Type) for LIST query benchmarking. All items will be returned +// when searching with a LIST query for the given SST. +func NewPopulatedCacheWithListItems(cache Cache, numberItems int, sst SST) CacheKey { + listMethod := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &listMethod} + + for i := range numberItems { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + // Ensure each item has a unique attribute value to prevent overwrites + // Format: "item-{index}" to guarantee uniqueness + uniqueValue := fmt.Sprintf("item-%d", i) + item.GetAttributes().Set("name", uniqueValue) + + cache.StoreItem(context.Background(), item, CacheDuration, ck) + } + + return ck +} + +// NewPopulatedCacheWithMultipleBuckets creates a cache with multiple SST buckets +// to enable realistic concurrent access patterns where different goroutines hit +// different buckets. +func NewPopulatedCacheWithMultipleBuckets(cache Cache, itemsPerBucket, numBuckets int) []CacheKey { + keys := make([]CacheKey, numBuckets) + listMethod := sdp.QueryMethod_LIST + + for bucketIdx := range numBuckets { + sst := SST{ + SourceName: "test-source", + Scope: fmt.Sprintf("scope-%d", bucketIdx), + Type: "test-type", + } + + keys[bucketIdx] = CacheKey{SST: sst, Method: &listMethod} + + for i := range itemsPerBucket { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + uniqueValue := fmt.Sprintf("bucket-%d-item-%d", bucketIdx, i) + item.GetAttributes().Set("name", uniqueValue) + + cache.StoreItem(context.Background(), item, CacheDuration, keys[bucketIdx]) + } + } + + return keys +} + +func BenchmarkCache1SingleItem(b *testing.B) { + c, query := NewPopulatedCache(b.Context(), 1) + + var err error + + b.ResetTimer() + + for range b.N { + // Search for a single item + _, err = testSearch(context.Background(), c, query) + + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCache10SingleItem(b *testing.B) { + c, query := NewPopulatedCache(b.Context(), 10) + + var err error + + b.ResetTimer() + + for range b.N { + // Search for a single item + _, err = testSearch(context.Background(), c, query) + + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCache100SingleItem(b *testing.B) { + c, query := NewPopulatedCache(b.Context(), 100) + + var err error + + b.ResetTimer() + + for range b.N { + // Search for a single item + _, err = testSearch(context.Background(), c, query) + + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCache1000SingleItem(b *testing.B) { + c, query := NewPopulatedCache(b.Context(), 1000) + + var err error + + b.ResetTimer() + + for range b.N { + // Search for a single item + _, err = testSearch(context.Background(), c, query) + + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkCache10_000SingleItem(b *testing.B) { + c, query := NewPopulatedCache(b.Context(), 10_000) + + var err error + + b.ResetTimer() + + for range b.N { + // Search for a single item + _, err = testSearch(context.Background(), c, query) + + if err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkListQueryLookup benchmarks LIST query performance using the Lookup method, +// which includes the full production path with pending work deduplication logic. +// This provides a more realistic benchmark of end-to-end LIST query performance. +func BenchmarkListQueryLookup(b *testing.B) { + implementations := cacheImplementations(b) + cacheSizes := []int{10, 100, 1_000, 10_000} + + for _, impl := range implementations { + b.Run(impl.name, func(b *testing.B) { + for _, size := range cacheSizes { + b.Run(fmt.Sprintf("%d_items", size), func(b *testing.B) { + // Setup + cache := impl.factory() + sst := SST{ + SourceName: "test-source", + Scope: "test-scope", + Type: "test-type", + } + _ = NewPopulatedCacheWithListItems(cache, size, sst) + + b.ResetTimer() + b.ReportAllocs() + + // Benchmark + for range b.N { + hit, _, items, qErr, done := cache.Lookup( + b.Context(), + sst.SourceName, + sdp.QueryMethod_LIST, + sst.Scope, + sst.Type, + "", + false, // ignoreCache + ) + done() // Clean up immediately + if qErr != nil { + b.Fatalf("unexpected query error: %v", qErr) + } + if !hit { + b.Fatal("expected cache hit, got miss") + } + if len(items) != size { + b.Fatalf("expected %d items, got %d", size, len(items)) + } + } + }) + } + }) + } +} + +// BenchmarkListQueryConcurrent benchmarks LIST query performance under high concurrency. +// This simulates production scenarios where hundreds of goroutines hit the cache simultaneously. +func BenchmarkListQueryConcurrent(b *testing.B) { + implementations := cacheImplementations(b) + + // Test configuration similar to production + cacheSize := 5_000 // Similar to production's largest bucket + concurrencyLevels := []int{10, 50, 100, 250, 500} + + for _, impl := range implementations { + b.Run(impl.name, func(b *testing.B) { + for _, concurrency := range concurrencyLevels { + b.Run(fmt.Sprintf("%d_concurrent", concurrency), func(b *testing.B) { + // Setup: Create cache with multiple buckets for realistic access patterns + cache := impl.factory() + numBuckets := 10 // Multiple buckets to spread queries + itemsPerBucket := cacheSize / numBuckets + cacheKeys := NewPopulatedCacheWithMultipleBuckets(cache, itemsPerBucket, numBuckets) + + b.ResetTimer() + b.ReportAllocs() + b.SetParallelism(concurrency / runtime.GOMAXPROCS(0)) // Scale to desired concurrency + + // Benchmark: Each goroutine randomly queries one of the buckets + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + // Randomly select a bucket to query + bucketIdx := rand.Intn(numBuckets) + ck := cacheKeys[bucketIdx] + + // Use Lookup() to match production behavior + hit, _, items, qErr, done := cache.Lookup( + b.Context(), + ck.SST.SourceName, + sdp.QueryMethod_LIST, + ck.SST.Scope, + ck.SST.Type, + "", + false, // ignoreCache + ) + done() // Clean up immediately + if qErr != nil { + b.Errorf("unexpected query error: %v", qErr) + return + } + if !hit { + b.Error("expected cache hit, got miss") + return + } + if len(items) != itemsPerBucket { + b.Errorf("expected %d items, got %d", itemsPerBucket, len(items)) + return + } + } + }) + }) + } + }) + } +} + +// BenchmarkListQueryConcurrentSameKey benchmarks worst-case contention where all +// goroutines query the same cache key simultaneously. This tests pending work +// deduplication and maximum lock contention. +func BenchmarkListQueryConcurrentSameKey(b *testing.B) { + implementations := cacheImplementations(b) + + cacheSize := 5_000 + concurrencyLevels := []int{10, 50, 100, 250, 500} + + for _, impl := range implementations { + b.Run(impl.name, func(b *testing.B) { + for _, concurrency := range concurrencyLevels { + b.Run(fmt.Sprintf("%d_concurrent", concurrency), func(b *testing.B) { + // Setup: Single SST bucket that all goroutines will hit + cache := impl.factory() + sst := SST{ + SourceName: "test-source", + Scope: "test-scope", + Type: "test-type", + } + _ = NewPopulatedCacheWithListItems(cache, cacheSize, sst) + + b.ResetTimer() + b.ReportAllocs() + b.SetParallelism(concurrency / runtime.GOMAXPROCS(0)) + + // Benchmark: All goroutines hit the same key + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + // Use Lookup() to match production behavior + hit, _, items, qErr, done := cache.Lookup( + b.Context(), + sst.SourceName, + sdp.QueryMethod_LIST, + sst.Scope, + sst.Type, + "", + false, // ignoreCache + ) + done() // Clean up immediately + if qErr != nil { + b.Errorf("unexpected query error: %v", qErr) + return + } + if !hit { + b.Error("expected cache hit, got miss") + return + } + if len(items) != cacheSize { + b.Errorf("expected %d items, got %d", cacheSize, len(items)) + return + } + } + }) + }) + } + }) + } +} + +// BenchmarkPendingWorkContention tests cache behavior when many concurrent goroutines +// all call Lookup() for the same cache key simultaneously. This simulates the production +// scenario where hundreds of goroutines wait in pending.Wait() for a single slow +// aggregatedList operation to complete. +func BenchmarkPendingWorkContention(b *testing.B) { + // Test parameters matching production scenarios + concurrencyLevels := []int{100, 200, 400, 500} + fetchDurations := []time.Duration{1 * time.Second, 5 * time.Second, 10 * time.Second} + resultSizes := []int{100, 1000, 5000} + + for _, impl := range cacheImplementations(b) { + b.Run(impl.name, func(b *testing.B) { + for _, concurrency := range concurrencyLevels { + b.Run(fmt.Sprintf("concurrency=%d", concurrency), func(b *testing.B) { + for _, fetchDuration := range fetchDurations { + b.Run(fmt.Sprintf("fetchDuration=%s", fetchDuration), func(b *testing.B) { + for _, resultSize := range resultSizes { + b.Run(fmt.Sprintf("resultSize=%d", resultSize), func(b *testing.B) { + // Run the actual benchmark + benchmarkPendingWorkContentionScenario( + b, + impl.factory, + concurrency, + fetchDuration, + resultSize, + ) + }) + } + }) + } + }) + } + }) + } +} + +// benchmarkPendingWorkContentionScenario runs a single pending work contention scenario +func benchmarkPendingWorkContentionScenario( + b *testing.B, + cacheFactory func() Cache, + concurrency int, + fetchDuration time.Duration, + resultSize int, +) { + b.ReportAllocs() + + // Create a fresh cache for this test + cache := cacheFactory() + defer func() { + if closer, ok := cache.(interface{ Close() error }); ok { + closer.Close() + } + }() + + // Define the shared cache key that all goroutines will use + sst := SST{ + SourceName: "test-source", + Scope: "test-scope-*", + Type: "test-type", + } + listMethod := sdp.QueryMethod_LIST + sharedCacheKey := CacheKey{SST: sst, Method: &listMethod} + + // Track timing metrics across all goroutines + var ( + firstStartTime time.Time + firstCompleteTime time.Time + lastCompleteTime time.Time + timingMutex sync.Mutex + ) + + // Atomic flag to detect the first goroutine (the one that does the work) + var firstGoroutine atomic.Bool + + // Use a start barrier to ensure all goroutines begin simultaneously + startBarrier := make(chan struct{}) + + b.ResetTimer() + + for range b.N { + // Clear cache between iterations + cache.Clear() + + // Reset state + firstGoroutine.Store(false) + firstStartTime = time.Time{} + firstCompleteTime = time.Time{} + lastCompleteTime = time.Time{} + + var wg sync.WaitGroup + wg.Add(concurrency) + + // Spawn all goroutines + for range concurrency { + go func() { + defer wg.Done() + + // Wait for start signal to ensure simultaneous execution + <-startBarrier + + startTime := time.Now() + + // Call Lookup - this is where the contention happens + hit, _, items, qErr, done := cache.Lookup( + b.Context(), + sst.SourceName, + sdp.QueryMethod_LIST, + sst.Scope, + sst.Type, + "", + false, // ignoreCache + ) + + endTime := time.Now() + + // Check if this goroutine was the first one (the worker) + isFirst := firstGoroutine.CompareAndSwap(false, true) + + if isFirst { + // This goroutine got the cache miss and needs to do the work + if hit { + b.Errorf("First goroutine should get cache miss, got hit") + done() + return + } + + // Record when work started + timingMutex.Lock() + firstStartTime = startTime + timingMutex.Unlock() + + // Simulate slow fetch operation (like aggregatedList) + time.Sleep(fetchDuration) + + // Store items in cache (simulating results from aggregatedList) + for itemIdx := range resultSize { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + item.GetAttributes().Set("name", fmt.Sprintf("item-%d", itemIdx)) + + cache.StoreItem(b.Context(), item, CacheDuration, sharedCacheKey) + } + + // Record when work completed + timingMutex.Lock() + firstCompleteTime = time.Now() + timingMutex.Unlock() + + // Call done() to complete pending work and release waiting goroutines + done() + } else { + // This goroutine should have waited in pending.Wait() and then got a cache hit + // Note: It might get partial results if it wakes up while the first goroutine + // is still storing items (released when the first goroutine calls done()) + done() // No-op for waiters, but good practice + if !hit { + b.Errorf("Waiting goroutine should get cache hit after pending work completes, got miss") + return + } + if qErr != nil { + b.Errorf("Waiting goroutine got error: %v", qErr) + return + } + if len(items) == 0 { + b.Errorf("Waiting goroutine got cache hit but no items") + return + } + // Don't check exact count - waiters may get partial results + } + + // Track when each goroutine completes + timingMutex.Lock() + if lastCompleteTime.IsZero() || endTime.After(lastCompleteTime) { + lastCompleteTime = endTime + } + timingMutex.Unlock() + }() + } + + // Release all goroutines simultaneously + close(startBarrier) + + // Wait for all goroutines to complete + wg.Wait() + + // Calculate and report metrics for this iteration + if !firstStartTime.IsZero() && !firstCompleteTime.IsZero() && !lastCompleteTime.IsZero() { + workDuration := firstCompleteTime.Sub(firstStartTime) + totalDuration := lastCompleteTime.Sub(firstStartTime) + maxWaitTime := lastCompleteTime.Sub(firstCompleteTime) + + // Report metrics + b.ReportMetric(workDuration.Seconds(), "work_duration_sec") + b.ReportMetric(totalDuration.Seconds(), "total_duration_sec") + b.ReportMetric(maxWaitTime.Seconds(), "max_wait_sec") + b.ReportMetric(float64(concurrency-1), "waiting_goroutines") + + // Calculate efficiency: ideally, waiters should return immediately after work completes + // A ratio close to 1.0 means waiters waited approximately the work duration + waitToWorkRatio := totalDuration.Seconds() / workDuration.Seconds() + b.ReportMetric(waitToWorkRatio, "wait_to_work_ratio") + } + + // Recreate start barrier for next iteration + startBarrier = make(chan struct{}) + } + + b.StopTimer() +} + +// BenchmarkConcurrentMultiKeyWrites tests cache behavior when many concurrent goroutines +// call Lookup() with DIFFERENT cache keys, all get cache misses, and all write results +// concurrently to the same BoltDB file. This simulates the production scenario where +// a wildcard query is expanded into 620+ separate queries with different scopes. +func BenchmarkConcurrentMultiKeyWrites(b *testing.B) { + // Test parameters matching production scenarios + concurrencyLevels := []int{100, 200, 400, 600} + itemsPerGoroutine := []int{10, 100, 500} + fetchDurations := []time.Duration{100 * time.Millisecond, 1 * time.Second, 5 * time.Second} + + for _, impl := range cacheImplementations(b) { + b.Run(impl.name, func(b *testing.B) { + for _, concurrency := range concurrencyLevels { + b.Run(fmt.Sprintf("concurrency=%d", concurrency), func(b *testing.B) { + for _, itemsPerGoroutine := range itemsPerGoroutine { + b.Run(fmt.Sprintf("itemsPerGoroutine=%d", itemsPerGoroutine), func(b *testing.B) { + for _, fetchDuration := range fetchDurations { + b.Run(fmt.Sprintf("fetchDuration=%s", fetchDuration), func(b *testing.B) { + // Run the actual benchmark + benchmarkConcurrentMultiKeyWritesScenario( + b, + impl.factory, + concurrency, + itemsPerGoroutine, + fetchDuration, + ) + }) + } + }) + } + }) + } + }) + } +} + +// benchmarkConcurrentMultiKeyWritesScenario runs a single concurrent multi-key write scenario +func benchmarkConcurrentMultiKeyWritesScenario( + b *testing.B, + cacheFactory func() Cache, + concurrency int, + itemsPerGoroutine int, + fetchDuration time.Duration, +) { + b.ReportAllocs() + + // Create a fresh cache for this test + cache := cacheFactory() + defer func() { + if closer, ok := cache.(interface{ Close() error }); ok { + closer.Close() + } + }() + + // Generate unique cache keys for each goroutine (different scopes) + cacheKeys := make([]CacheKey, concurrency) + listMethod := sdp.QueryMethod_LIST + for i := range concurrency { + cacheKeys[i] = CacheKey{ + SST: SST{ + SourceName: "test-source", + Scope: fmt.Sprintf("scope-%d", i), // Different scope = different cache key + Type: "test-type", + }, + Method: &listMethod, + } + } + + // Track timing metrics + var ( + goroutineStartTimes []time.Time + goroutineEndTimes []time.Time + timesMutex sync.Mutex + totalStoreItemCalls atomic.Int64 + ) + + // Use a start barrier to ensure all goroutines begin simultaneously + startBarrier := make(chan struct{}) + + b.ResetTimer() + + for range b.N { + // Clear cache between iterations + cache.Clear() + + // Reset metrics + goroutineStartTimes = make([]time.Time, 0, concurrency) + goroutineEndTimes = make([]time.Time, 0, concurrency) + totalStoreItemCalls.Store(0) + + var wg sync.WaitGroup + wg.Add(concurrency) + + // Spawn all goroutines + for g := range concurrency { + goroutineIdx := g + go func() { + defer wg.Done() + + // Wait for start signal to ensure simultaneous execution + <-startBarrier + + startTime := time.Now() + + // Track start time + timesMutex.Lock() + goroutineStartTimes = append(goroutineStartTimes, startTime) + timesMutex.Unlock() + + // Call Lookup with unique cache key - should be a cache miss + myCacheKey := cacheKeys[goroutineIdx] + hit, _, _, qErr, done := cache.Lookup( + b.Context(), + myCacheKey.SST.SourceName, + sdp.QueryMethod_LIST, + myCacheKey.SST.Scope, + myCacheKey.SST.Type, + "", + false, // ignoreCache + ) + + if hit { + b.Errorf("Expected cache miss for goroutine %d, got hit", goroutineIdx) + done() + return + } + if qErr != nil { + b.Errorf("Unexpected error for goroutine %d: %v", goroutineIdx, qErr) + done() + return + } + + // Simulate slow fetch operation (like aggregatedList API call) + time.Sleep(fetchDuration) + + // Store multiple items (simulating API results) + for itemIdx := range itemsPerGoroutine { + item := GenerateRandomItem() + item.Scope = myCacheKey.SST.Scope + item.Type = myCacheKey.SST.Type + item.Metadata.SourceName = myCacheKey.SST.SourceName + item.GetAttributes().Set("name", fmt.Sprintf("goroutine-%d-item-%d", goroutineIdx, itemIdx)) + + cache.StoreItem(b.Context(), item, CacheDuration, myCacheKey) + totalStoreItemCalls.Add(1) + } + + // Call done() to complete pending work + done() + + endTime := time.Now() + + // Track end time + timesMutex.Lock() + goroutineEndTimes = append(goroutineEndTimes, endTime) + timesMutex.Unlock() + }() + } + + // Release all goroutines simultaneously + close(startBarrier) + + // Wait for all goroutines to complete + wg.Wait() + + // Calculate and report metrics for this iteration + if len(goroutineStartTimes) > 0 && len(goroutineEndTimes) > 0 { + // Find earliest start and latest end + earliestStart := goroutineStartTimes[0] + latestEnd := goroutineEndTimes[0] + + for _, t := range goroutineStartTimes { + if t.Before(earliestStart) { + earliestStart = t + } + } + for _, t := range goroutineEndTimes { + if t.After(latestEnd) { + latestEnd = t + } + } + + totalDuration := latestEnd.Sub(earliestStart) + totalWrites := totalStoreItemCalls.Load() + writeThroughput := float64(totalWrites) / totalDuration.Seconds() + + // Calculate average goroutine duration + var totalGoroutineDuration time.Duration + for idx := range goroutineStartTimes { + if idx < len(goroutineEndTimes) { + totalGoroutineDuration += goroutineEndTimes[idx].Sub(goroutineStartTimes[idx]) + } + } + avgGoroutineDuration := totalGoroutineDuration / time.Duration(len(goroutineStartTimes)) + + // Report metrics + b.ReportMetric(totalDuration.Seconds(), "total_duration_sec") + b.ReportMetric(avgGoroutineDuration.Seconds(), "avg_goroutine_sec") + b.ReportMetric(float64(concurrency), "concurrent_writers") + b.ReportMetric(float64(totalWrites), "total_store_calls") + b.ReportMetric(writeThroughput, "writes_per_sec") + } + + // Recreate start barrier for next iteration + startBarrier = make(chan struct{}) + } + + b.StopTimer() +} diff --git a/go/sdpcache/cache_stuck_test.go b/go/sdpcache/cache_stuck_test.go new file mode 100644 index 00000000..b5118e18 --- /dev/null +++ b/go/sdpcache/cache_stuck_test.go @@ -0,0 +1,377 @@ +package sdpcache + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" +) + +// TestListErrorWithProperCleanup tests the correct behavior where: +// 1. A LIST operation is performed and gets a cache miss +// 2. The caller starts the work +// 3. The query encounters an error +// 4. The caller properly calls StoreError to cache the error +// 5. Subsequent requests get the cached error immediately (don't block) +// +// This test documents the fix for the cache timeout bug. +func TestListErrorWithProperCleanup(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // Track timing + var secondCallDuration time.Duration + + // First goroutine: Gets cache miss, simulates work that errors, + // and properly calls StoreError to cache the error + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that takes time and then errors + time.Sleep(50 * time.Millisecond) + + // CORRECT BEHAVIOR: Worker encounters an error and properly caches it + err := &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "simulated list error", + } + cache.StoreError(ctx, err, 1*time.Hour, ck) + t.Log("First goroutine: properly called StoreError") + }() + + // Second goroutine: Should get cached error immediately + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + // Use a short timeout to detect blocking + timeoutCtx, done := context.WithTimeout(ctx, 500*time.Millisecond) + defer done() + + start := time.Now() + hit, _, _, qErr, done := cache.Lookup(timeoutCtx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + secondCallDuration = time.Since(start) + + if !hit { + t.Error("second goroutine: expected cache hit (cached error)") + } + if qErr == nil { + t.Error("second goroutine: expected cached error") + } + t.Logf("Second goroutine: got cached error after %v", secondCallDuration) + }() + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // Verify the second call got the result quickly (didn't block) + if secondCallDuration > 200*time.Millisecond { + t.Fatalf("Second call took too long (%v), possibly blocked waiting for pending work", secondCallDuration) + } + + t.Logf("✓ Second call returned quickly (%v) with cached error - proper cleanup is working", secondCallDuration) + }) + } +} + +// TestListErrorWithProperCancellation tests the CORRECT behavior where: +// 1. A LIST operation is performed and gets a cache miss +// 2. The query encounters an error +// 3. The caller properly calls the done function +// 4. Subsequent requests should get a cache miss immediately (not block) +func TestListErrorWithProperDone(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // Track timing + var secondCallDuration time.Duration + + // First goroutine: Gets cache miss, simulates work that errors, + // and PROPERLY calls the done function + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + + if hit { + t.Error("first goroutine: expected cache miss") + done() // Clean up even on error + return + } + + // Simulate work that takes time and then errors + time.Sleep(100 * time.Millisecond) + + // CORRECT BEHAVIOR: Call done to release resources + done() + t.Log("First goroutine: properly called done()") + }() + + // Second goroutine: Should receive cache miss quickly (not block) + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + start := time.Now() + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + secondCallDuration = time.Since(start) + + if hit { + t.Error("second goroutine: expected cache miss") + } + + t.Logf("Second goroutine: got cache miss after %v", secondCallDuration) + }() + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // The second call should NOT block for long + // It should get a cache miss shortly after the first call done() (~100ms) + if secondCallDuration > 300*time.Millisecond { + t.Errorf("Expected second call to return quickly after cancellation, but it took %v", secondCallDuration) + } + + t.Logf("Test demonstrates correct behavior: second call returned in %v", secondCallDuration) + }) + } +} + +// TestListErrorWithStoreError tests the CORRECT behavior where: +// 1. A LIST operation is performed and gets a cache miss +// 2. The query encounters an error +// 3. The caller properly calls StoreError +// 4. Subsequent requests should get the cached error immediately +func TestListErrorWithStoreError(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + expectedError := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "list returned error", + Scope: sst.Scope, + SourceName: sst.SourceName, + ItemType: sst.Type, + } + + // Track results + var secondCallHit bool + var secondCallError *sdp.QueryError + var secondCallDuration time.Duration + + // First goroutine: Gets cache miss, simulates work that errors, + // and PROPERLY calls StoreError + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that takes time and then errors + time.Sleep(100 * time.Millisecond) + + // CORRECT BEHAVIOR: Store the error so other callers can get it + cache.StoreError(ctx, expectedError, 10*time.Second, ck) + t.Log("First goroutine: properly called StoreError") + }() + + // Second goroutine: Should receive the cached error + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + start := time.Now() + var items []*sdp.Item + var done func() + secondCallHit, _, items, secondCallError, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + secondCallDuration = time.Since(start) + + if items != nil { + t.Error("second goroutine: expected nil items with error") + } + + t.Logf("Second goroutine: got result after %v", secondCallDuration) + }() + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // The second call should get the cached error + if !secondCallHit { + t.Error("Expected cache hit with error") + } + + if secondCallError == nil { + t.Error("Expected error to be returned") + } + + if secondCallError != nil && secondCallError.GetErrorType() != expectedError.GetErrorType() { + t.Errorf("Expected error type %v, got %v", expectedError.GetErrorType(), secondCallError.GetErrorType()) + } + + // Should return relatively quickly (~100ms for first goroutine work) + if secondCallDuration > 300*time.Millisecond { + t.Errorf("Expected second call to return quickly with cached error, but it took %v", secondCallDuration) + } + + t.Logf("Test demonstrates correct behavior: second call got cached error in %v", secondCallDuration) + }) + } +} + +// TestListReturnsEmptyButNoStore tests the scenario where: +// 1. A LIST operation completes successfully but finds no items +// 2. The caller calls Complete() but doesn't store anything +// 3. Subsequent requests should get cache miss (not error) +func TestListReturnsEmptyButNoStore(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + var secondCallHit bool + var secondCallDuration time.Duration + + // First goroutine: LIST returns 0 items, completes without storing + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that completes and finds no items + time.Sleep(100 * time.Millisecond) + + // Complete without storing anything (LIST found 0 items) + // This is handled by the underlying pending work mechanism + switch c := cache.(type) { + case *MemoryCache: + c.pending.Complete(ck.String()) + case *BoltCache: + c.pending.Complete(ck.String()) + } + + t.Log("First goroutine: completed work but stored nothing") + }() + + // Second goroutine: Should get cache miss + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + start := time.Now() + secondCallHit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + secondCallDuration = time.Since(start) + + t.Logf("Second goroutine: hit=%v, duration=%v", secondCallHit, secondCallDuration) + }() + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // Second call should get cache miss (not error) + if secondCallHit { + t.Error("Expected cache miss when first caller completed without storing") + } + + // Should return relatively quickly (~100ms for first goroutine work) + if secondCallDuration > 300*time.Millisecond { + t.Errorf("Expected second call to return quickly, but it took %v", secondCallDuration) + } + }) + } +} diff --git a/go/sdpcache/cache_test.go b/go/sdpcache/cache_test.go new file mode 100644 index 00000000..8003557f --- /dev/null +++ b/go/sdpcache/cache_test.go @@ -0,0 +1,2156 @@ +package sdpcache + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" +) + +// testSearch is a helper function that calls the internal search method +// on either MemoryCache or BoltCache implementations for testing purposes +func testSearch(ctx context.Context, cache Cache, ck CacheKey) ([]*sdp.Item, error) { + switch c := cache.(type) { + case *MemoryCache: + return c.search(ctx, ck) + case *BoltCache: + return c.search(ctx, ck) + default: + return nil, fmt.Errorf("unsupported cache type for search: %T", cache) + } +} + +// cacheImplementations returns the list of cache implementations to test +// Accepts testing.TB so it can be used by both tests and benchmarks +func cacheImplementations(tb testing.TB) []struct { + name string + factory func() Cache +} { + return []struct { + name string + factory func() Cache + }{ + {"MemoryCache", func() Cache { return NewMemoryCache() }}, + {"BoltCache", func() Cache { + c, err := NewBoltCache(filepath.Join(tb.TempDir(), "cache.db")) + if err != nil { + tb.Fatalf("failed to create BoltCache: %v", err) + } + tb.Cleanup(func() { + _ = c.CloseAndDestroy() + }) + return c + }}, + } +} + +func TestStoreItem(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(t.Context(), item, 10*time.Second, ck) + + results, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + + if len(results) != 1 { + t.Errorf("expected 1 result, got %v", len(results)) + } + + // Test another match + item = GenerateRandomItem() + ck = CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(t.Context(), item, 10*time.Second, ck) + + results, err = testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + + if len(results) != 1 { + t.Errorf("expected 1 result, got %v", len(results)) + } + + // Test different scope + item = GenerateRandomItem() + ck = CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(t.Context(), item, 10*time.Second, ck) + + ck.SST.Scope = fmt.Sprintf("new scope %v", ck.SST.Scope) + + results, err = testSearch(t.Context(), cache, ck) + if err != nil { + if !errors.Is(err, ErrCacheNotFound) { + t.Error(err) + } else { + t.Log("expected cache miss") + } + } + + if len(results) != 0 { + t.Errorf("expected 0 result, got %v", results) + } + }) + } +} + +func TestStoreError(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + // Test with just an error + sst := SST{ + SourceName: "foo", + Scope: "foo", + Type: "foo", + } + + uav := "foo" + + cache.StoreError(t.Context(), errors.New("arse"), 10*time.Second, CacheKey{ + SST: sst, + Method: sdp.QueryMethod_GET.Enum(), + Query: &uav, + }) + + items, err := testSearch(t.Context(), cache, CacheKey{ + SST: sst, + Method: sdp.QueryMethod_GET.Enum(), + Query: &uav, + }) + + if len(items) > 0 { + t.Errorf("expected 0 items, got %v", len(items)) + } + + if err == nil { + t.Error("expected error, got nil") + } + + // Test with items and an error for the same query + // Add an item with the same details as above + item := GenerateRandomItem() + item.Metadata.SourceQuery.Method = sdp.QueryMethod_GET + item.Metadata.SourceQuery.Query = "foo" + item.Metadata.SourceName = "foo" + item.Scope = "foo" + item.Type = "foo" + + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + items, err = testSearch(t.Context(), cache, ck) + + if len(items) > 0 { + t.Errorf("expected 0 items, got %v", len(items)) + } + + if err == nil { + t.Error("expected error, got nil") + } + + // Test with multiple errors + cache.StoreError(t.Context(), errors.New("nope"), 10*time.Second, CacheKey{ + SST: sst, + Method: sdp.QueryMethod_GET.Enum(), + Query: &uav, + }) + + items, err = testSearch(t.Context(), cache, CacheKey{ + SST: sst, + Method: sdp.QueryMethod_GET.Enum(), + Query: &uav, + }) + + if len(items) > 0 { + t.Errorf("expected 0 items, got %v", len(items)) + } + + if err == nil { + t.Error("expected error, got nil") + } + }) + } +} + +func TestPurge(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + cachedItems := []struct { + Item *sdp.Item + Expiry time.Time + }{ + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(50 * time.Millisecond), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(1 * time.Second), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(2 * time.Second), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(3 * time.Second), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(4 * time.Second), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(5 * time.Second), + }, + } + + for _, i := range cachedItems { + ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) + cache.StoreItem(t.Context(), i.Item, time.Until(i.Expiry), ck) + } + + // Make sure all the items are in the cache + for _, i := range cachedItems { + ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) + items, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + + if len(items) != 1 { + t.Errorf("expected 1 item, got %v", len(items)) + } + } + + // Purge just the first one + stats := cache.Purge(t.Context(), cachedItems[0].Expiry.Add(500*time.Millisecond)) + + if stats.NumPurged != 1 { + t.Errorf("expected 1 item purged, got %v", stats.NumPurged) + } + + // The times won't be exactly equal because we're checking it against + // time.Now more than once. So I need to check that they are *almost* the + // same, but not exactly + nextExpiryString := stats.NextExpiry.Format(time.RFC3339) + expectedNextExpiryString := cachedItems[1].Expiry.Format(time.RFC3339) + + if nextExpiryString != expectedNextExpiryString { + t.Errorf("expected next expiry to be %v, got %v", expectedNextExpiryString, nextExpiryString) + } + + // Purge all but the last one + stats = cache.Purge(t.Context(), cachedItems[4].Expiry.Add(500*time.Millisecond)) + + if stats.NumPurged != 4 { + t.Errorf("expected 4 item purged, got %v", stats.NumPurged) + } + + // Purge the last one + stats = cache.Purge(t.Context(), cachedItems[5].Expiry.Add(500*time.Millisecond)) + + if stats.NumPurged != 1 { + t.Errorf("expected 1 item purged, got %v", stats.NumPurged) + } + + if stats.NextExpiry != nil { + t.Errorf("expected expiry to be nil, got %v", stats.NextExpiry) + } + }) + } +} + +func TestDelete(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + // Insert an item + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(t.Context(), item, time.Millisecond, ck) + sst := SST{ + SourceName: item.GetMetadata().GetSourceName(), + Scope: item.GetScope(), + Type: item.GetType(), + } + + // It should be there + items, err := testSearch(t.Context(), cache, CacheKey{ + SST: sst, + }) + if err != nil { + t.Error(err) + } + + if len(items) != 1 { + t.Errorf("expected 1 item, got %v", len(items)) + } + + // Delete it + cache.Delete(CacheKey{ + SST: sst, + }) + + // It should be gone + items, err = testSearch(t.Context(), cache, CacheKey{ + SST: sst, + }) + + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("expected ErrCacheNotFound, got %v", err) + } + + if len(items) != 0 { + t.Errorf("expected 0 item, got %v", len(items)) + } + }) + } +} + +func TestPointers(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(t.Context(), item, time.Minute, ck) + + item.Type = "bad" + + items, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + + if len(items) != 1 { + t.Errorf("expected 1 item, got %v", len(items)) + } + + if items[0].GetType() == "bad" { + t.Error("item was changed in cache") + } + }) + } +} + +func TestCacheClear(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + cache.Clear() + + // Populate the cache + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(ctx, item, 500*time.Millisecond, ck) + + // Start purging just to make sure it doesn't break + ctx, done := context.WithCancel(ctx) + defer done() + cache.StartPurger(ctx) + + // Make sure the cache is populated + _, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + + // Clear the cache + cache.Clear() + + // Make sure the cache is empty + _, err = testSearch(t.Context(), cache, ck) + + if err == nil { + t.Error("expected error, cache not cleared") + } + + // Make sure we can populate it again + cache.StoreItem(ctx, item, 500*time.Millisecond, ck) + _, err = testSearch(t.Context(), cache, ck) + if err != nil { + t.Error(err) + } + }) + } +} + +func TestLookup(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // ignore the cache + cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), true) + defer done() + if err != nil { + t.Fatal(err) + } + if cacheHit { + t.Error("expected cache miss, got hit") + } + if cachedItems != nil { + t.Errorf("expected nil items, got %v", cachedItems) + } + + // Lookup the item + cacheHit, _, cachedItems, err, done = cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) + defer done() + + if err != nil { + t.Fatal(err) + } + if !cacheHit { + t.Fatal("expected cache hit, got miss") + } + if len(cachedItems) != 1 { + t.Fatalf("expected 1 item, got %v", len(cachedItems)) + } + + if cachedItems[0].GetType() != item.GetType() { + t.Errorf("expected type %v, got %v", item.GetType(), cachedItems[0].GetType()) + } + + if cachedItems[0].Health == nil { + t.Error("expected health to be set") + } + + if len(cachedItems[0].GetTags()) != len(item.GetTags()) { + t.Error("expected tags to be set") + } + + stats := cache.Purge(ctx, time.Now().Add(1*time.Hour)) + if stats.NumPurged != 1 { + t.Errorf("expected 1 item purged, got %v", stats.NumPurged) + } + + // Lookup the item + cacheHit, _, cachedItems, err, done = cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) + defer done() + + if err != nil { + t.Fatal(err) + } + if cacheHit { + t.Fatal("expected cache miss, got hit") + } + if len(cachedItems) != 0 { + t.Fatalf("expected 0 item, got %v", len(cachedItems)) + } + }) + } +} + +func TestStoreSearch(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + item := GenerateRandomItem() + item.Metadata.SourceQuery.Method = sdp.QueryMethod_SEARCH + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Lookup the item as GET request + cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, item.GetMetadata().GetSourceName(), sdp.QueryMethod_GET, item.GetScope(), item.GetType(), item.UniqueAttributeValue(), false) + defer done() + if err != nil { + t.Fatal(err) + } + + if !cacheHit { + t.Fatal("expected cache hit, got miss") + } + + if len(cachedItems) != 1 { + t.Fatalf("expected 1 item, got %v", len(cachedItems)) + } + + if cachedItems[0].GetType() != item.GetType() { + t.Errorf("expected type %v, got %v", item.GetType(), cachedItems[0].GetType()) + } + }) + } +} + +func TestLookupWithListMethod(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + // Store multiple items with same SST + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + listMethod := sdp.QueryMethod_LIST + + item1 := GenerateRandomItem() + item1.Scope = sst.Scope + item1.Type = sst.Type + item1.Metadata.SourceName = sst.SourceName + ck1 := CacheKey{SST: sst, Method: &listMethod} + cache.StoreItem(ctx, item1, 10*time.Second, ck1) + + item2 := GenerateRandomItem() + item2.Scope = sst.Scope + item2.Type = sst.Type + item2.Metadata.SourceName = sst.SourceName + ck2 := CacheKey{SST: sst, Method: &listMethod} + cache.StoreItem(ctx, item2, 10*time.Second, ck2) + + // Lookup with LIST should return both items + cacheHit, _, cachedItems, err, done := cache.Lookup(ctx, sst.SourceName, sdp.QueryMethod_LIST, sst.Scope, sst.Type, "", false) + defer done() + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !cacheHit { + t.Fatal("expected cache hit, got miss") + } + if len(cachedItems) != 2 { + t.Errorf("expected 2 items, got %v", len(cachedItems)) + } + }) + } +} + +func TestSearchWithListMethod(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + // Store items with LIST method + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + listMethod := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &listMethod} + + item1 := GenerateRandomItem() + item1.Scope = sst.Scope + item1.Type = sst.Type + cache.StoreItem(t.Context(), item1, 10*time.Second, ck) + + item2 := GenerateRandomItem() + item2.Scope = sst.Scope + item2.Type = sst.Type + cache.StoreItem(t.Context(), item2, 10*time.Second, ck) + + // Search should return both items + items, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 2 { + t.Errorf("expected 2 items, got %v", len(items)) + } + }) + } +} + +func TestSearchMethodWithDifferentQueries(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + searchMethod := sdp.QueryMethod_SEARCH + + // Store items with different search queries + query1 := "query1" + ck1 := CacheKey{SST: sst, Method: &searchMethod, Query: &query1} + item1 := GenerateRandomItem() + item1.Scope = sst.Scope + item1.Type = sst.Type + cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) + + query2 := "query2" + ck2 := CacheKey{SST: sst, Method: &searchMethod, Query: &query2} + item2 := GenerateRandomItem() + item2.Scope = sst.Scope + item2.Type = sst.Type + cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) + + // Search with query1 should only return item1 + items, err := testSearch(t.Context(), cache, ck1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item for query1, got %v", len(items)) + } + + // Search with query2 should only return item2 + items, err = testSearch(t.Context(), cache, ck2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item for query2, got %v", len(items)) + } + }) + } +} + +func TestSearchWithPartialCacheKey(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + + // Store items with different methods + getMethod := sdp.QueryMethod_GET + listMethod := sdp.QueryMethod_LIST + + item1 := GenerateRandomItem() + item1.Scope = sst.Scope + item1.Type = sst.Type + uav1 := "item1" + ck1 := CacheKey{SST: sst, Method: &getMethod, UniqueAttributeValue: &uav1} + cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) + + item2 := GenerateRandomItem() + item2.Scope = sst.Scope + item2.Type = sst.Type + ck2 := CacheKey{SST: sst, Method: &listMethod} + cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) + + // Search with SST only should return both items + ckPartial := CacheKey{SST: sst} + items, err := testSearch(t.Context(), cache, ckPartial) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 2 { + t.Errorf("expected 2 items with SST-only search, got %v", len(items)) + } + }) + } +} + +func TestDeleteWithPartialCacheKey(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + + // Store multiple items with same SST + item1 := GenerateRandomItem() + item1.Scope = sst.Scope + item1.Type = sst.Type + ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), sst.SourceName) + cache.StoreItem(t.Context(), item1, 10*time.Second, ck1) + + item2 := GenerateRandomItem() + item2.Scope = sst.Scope + item2.Type = sst.Type + ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), sst.SourceName) + cache.StoreItem(t.Context(), item2, 10*time.Second, ck2) + + // Delete with SST only should remove all items + cache.Delete(CacheKey{SST: sst}) + + // Verify all items are gone + items, err := testSearch(t.Context(), cache, CacheKey{SST: sst}) + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("expected ErrCacheNotFound after delete, got: %v", err) + } + if len(items) != 0 { + t.Errorf("expected 0 items after delete, got %v", len(items)) + } + }) + } +} + +func TestLookupWithCachedError(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + // Test different error types + errorTypes := []struct { + name string + errorType sdp.QueryError_ErrorType + }{ + {"NOTFOUND", sdp.QueryError_NOTFOUND}, + {"NOSCOPE", sdp.QueryError_NOSCOPE}, + {"TIMEOUT", sdp.QueryError_TIMEOUT}, + {"OTHER", sdp.QueryError_OTHER}, + } + + for i, et := range errorTypes { + t.Run(et.name, func(t *testing.T) { + sst := SST{ + SourceName: fmt.Sprintf("test%d", i), + Scope: "scope", + Type: "type", + } + method := sdp.QueryMethod_GET + query := "test" + ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &query} + + // Store error + qErr := &sdp.QueryError{ + ErrorType: et.errorType, + ErrorString: fmt.Sprintf("test error %s", et.name), + Scope: sst.Scope, + SourceName: sst.SourceName, + ItemType: sst.Type, + } + cache.StoreError(ctx, qErr, 10*time.Second, ck) + + // Lookup should return cached error + cacheHit, _, items, returnedErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + if !cacheHit { + t.Error("expected cache hit for cached error") + } + if items != nil { + t.Errorf("expected nil items, got %v", items) + } + if returnedErr == nil { + t.Fatal("expected error to be returned") + } + if returnedErr.GetErrorType() != et.errorType { + t.Errorf("expected error type %v, got %v", et.errorType, returnedErr.GetErrorType()) + } + }) + } + }) + } +} + +func TestGetMinWaitTime(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + minWaitTime := cache.GetMinWaitTime() + + // Should return a positive duration + if minWaitTime <= 0 { + t.Errorf("expected positive duration, got %v", minWaitTime) + } + + // Default should be reasonable (e.g., 5 seconds) + if minWaitTime > time.Minute { + t.Errorf("expected reasonable default (< 1 minute), got %v", minWaitTime) + } + }) + } +} + +func TestEmptyCacheOperations(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + cache := impl.factory() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + ck := CacheKey{SST: sst} + + // Search on empty cache + items, err := testSearch(t.Context(), cache, ck) + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("expected ErrCacheNotFound on empty cache, got: %v", err) + } + if len(items) != 0 { + t.Errorf("expected 0 items on empty cache, got %v", len(items)) + } + + // Delete on empty cache (should be idempotent) + cache.Delete(ck) + + // Purge on empty cache + stats := cache.Purge(t.Context(), time.Now()) + if stats.NumPurged != 0 { + t.Errorf("expected 0 items purged on empty cache, got %v", stats.NumPurged) + } + if stats.NextExpiry != nil { + t.Errorf("expected nil NextExpiry on empty cache, got %v", stats.NextExpiry) + } + + // Clear on empty cache (should not error) + cache.Clear() + }) + } +} + +func TestMultipleItemsSameSST(t *testing.T) { + implementations := cacheImplementations(t) + + for _, impl := range implementations { + t.Run(impl.name, func(t *testing.T) { + ctx := t.Context() + cache := impl.factory() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_GET + + // Store multiple items with same SST but different unique attributes + items := make([]*sdp.Item, 3) + for i := range 3 { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + uav := fmt.Sprintf("item%d", i) + + // Set the item's unique attribute value to match the CacheKey + attrs := make(map[string]interface{}) + if item.GetAttributes() != nil && item.GetAttributes().GetAttrStruct() != nil { + for k, v := range item.GetAttributes().GetAttrStruct().GetFields() { + attrs[k] = v + } + } + attrs["name"] = uav + attributes, _ := sdp.ToAttributes(attrs) + item.Attributes = attributes + + ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} + cache.StoreItem(ctx, item, 10*time.Second, ck) + items[i] = item + } + + // Search with SST only should return all 3 items + allItems, err := testSearch(t.Context(), cache, CacheKey{SST: sst}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(allItems) != 3 { + t.Errorf("expected 3 items, got %v", len(allItems)) + } + + // Search with specific unique attribute should return only that item + for i := range 3 { + uav := fmt.Sprintf("item%d", i) + ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} + foundItems, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Errorf("unexpected error for item%d: %v", i, err) + } + if len(foundItems) != 1 { + t.Errorf("expected 1 item for item%d, got %v", i, len(foundItems)) + } + } + }) + } +} + +// Implementation-specific tests for MemoryCache + +// TestMemoryCacheStartPurge tests the memory cache implementation's purger +func TestMemoryCacheStartPurge(t *testing.T) { + ctx := t.Context() + cache := NewMemoryCache() + cache.MinWaitTime = 100 * time.Millisecond + + cachedItems := []struct { + Item *sdp.Item + Expiry time.Time + }{ + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(0), + }, + { + Item: GenerateRandomItem(), + Expiry: time.Now().Add(100 * time.Millisecond), + }, + } + + for _, i := range cachedItems { + ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, i.Item, time.Until(i.Expiry), ck) + } + + ctx, done := context.WithCancel(ctx) + defer done() + + cache.StartPurger(ctx) + + // Wait for everything to be purged + time.Sleep(200 * time.Millisecond) + + // At this point everything should be been cleaned, and the purger should be + // sleeping forever + items, err := testSearch(t.Context(), cache, CacheKeyFromQuery( + cachedItems[1].Item.GetMetadata().GetSourceQuery(), + cachedItems[1].Item.GetMetadata().GetSourceName(), + )) + + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("unexpected error: %v", err) + t.Errorf("unexpected items: %v", len(items)) + } + + cache.purgeMutex.Lock() + if cache.nextPurge.Before(time.Now().Add(time.Hour)) { + // If the next purge is within the next hour that's an error, it should + // be really, really for in the future + t.Errorf("Expected next purge to be in 1000 years, got %v", cache.nextPurge.String()) + } + cache.purgeMutex.Unlock() + + // Adding a new item should kick off the purging again + for _, i := range cachedItems { + ck := CacheKeyFromQuery(i.Item.GetMetadata().GetSourceQuery(), i.Item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, i.Item, 100*time.Millisecond, ck) + } + + time.Sleep(200 * time.Millisecond) + + // It should be empty again + items, err = testSearch(t.Context(), cache, CacheKeyFromQuery( + cachedItems[1].Item.GetMetadata().GetSourceQuery(), + cachedItems[1].Item.GetMetadata().GetSourceName(), + )) + + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("unexpected error: %v", err) + t.Errorf("unexpected items: %v: %v", len(items), items) + } +} + +// TestMemoryCacheStopPurge tests the memory cache implementation's purger stop functionality +func TestMemoryCacheStopPurge(t *testing.T) { + cache := NewMemoryCache() + cache.MinWaitTime = 1 * time.Millisecond + + ctx, done := context.WithCancel(t.Context()) + + cache.StartPurger(ctx) + + // Stop the purger + done() + + // Insert an item + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(ctx, item, 1*time.Second, ck) + sst := SST{ + SourceName: item.GetMetadata().GetSourceName(), + Scope: item.GetScope(), + Type: item.GetType(), + } + + // Make sure it's not purged + time.Sleep(100 * time.Millisecond) + items, err := testSearch(t.Context(), cache, CacheKey{ + SST: sst, + }) + if err != nil { + t.Error(err) + } + + if len(items) != 1 { + t.Errorf("Expected 1 item, got %v", len(items)) + } +} + +// TestMemoryCacheConcurrent tests the memory cache implementation for data races. +// This test is designed to be run with -race to ensure that there aren't any +// data races +func TestMemoryCacheConcurrent(t *testing.T) { + cache := NewMemoryCache() + // Run the purger super fast to generate a worst-case scenario + cache.MinWaitTime = 1 * time.Millisecond + + ctx, done := context.WithCancel(t.Context()) + defer done() + cache.StartPurger(ctx) + var wg sync.WaitGroup + + numParallel := 1_000 + + for range numParallel { + wg.Add(1) + go func() { + defer wg.Done() + // Store the item + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + cache.StoreItem(ctx, item, 100*time.Millisecond, ck) + + wg.Add(1) + // Create a goroutine to also delete in parallel + go func() { + defer wg.Done() + cache.Delete(ck) + }() + }() + } + + wg.Wait() +} + +// TestMemoryCacheLookupDeduplication tests that multiple concurrent Lookup calls +// for the same cache key in MemoryCache result in only one caller doing work. +func TestMemoryCacheLookupDeduplication(t *testing.T) { + cache := NewMemoryCache() + ctx := t.Context() + + // Create a cache key for the test - use LIST method to avoid UniqueAttributeValue matching issues + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + + // Track how many goroutines actually do work + var workCount int32 + var mu sync.Mutex + var wg sync.WaitGroup + + numGoroutines := 10 + results := make([]struct { + hit bool + items []*sdp.Item + }, numGoroutines) + + startBarrier := make(chan struct{}) + + for i := range numGoroutines { + wg.Add(1) + go func(idx int) { + defer wg.Done() + <-startBarrier + + hit, ck, items, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + + if !hit { + mu.Lock() + workCount++ + mu.Unlock() + + time.Sleep(50 * time.Millisecond) + + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + cache.StoreItem(ctx, item, 10*time.Second, ck) + hit, _, items, _, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + } + + results[idx] = struct { + hit bool + items []*sdp.Item + }{hit, items} + }(i) + } + + close(startBarrier) + wg.Wait() + + if workCount != 1 { + t.Errorf("expected exactly 1 goroutine to do work, got %d", workCount) + } + + for i, r := range results { + if !r.hit { + t.Errorf("goroutine %d: expected cache hit after dedup, got miss", i) + } + if len(r.items) != 1 { + t.Errorf("goroutine %d: expected 1 item, got %d", i, len(r.items)) + } + } +} + +// TestMemoryCacheLookupDeduplicationCompleteWithoutStore tests the scenario where +// Complete is called but nothing was stored in the cache. This tests the explicit +// ErrCacheNotFound check in the re-check logic. +func TestMemoryCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { + cache := NewMemoryCache() + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "complete-without-store-test" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // Track results + var waiterHits []bool + var waiterMu sync.Mutex + + numWaiters := 3 + + // First goroutine: starts work and completes without storing anything + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that completes successfully but returns nothing + time.Sleep(50 * time.Millisecond) + + // Complete without storing anything - triggers ErrCacheNotFound on re-check + cache.pending.Complete(ck.String()) + }() + + // Waiter goroutines + for range numWaiters { + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + time.Sleep(10 * time.Millisecond) + + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + waiterMu.Lock() + waiterHits = append(waiterHits, hit) + waiterMu.Unlock() + }() + } + + close(startBarrier) + wg.Wait() + + if len(waiterHits) != numWaiters { + t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) + } + + // All waiters should get a cache miss since nothing was stored + for i, hit := range waiterHits { + if hit { + t.Errorf("waiter %d: expected cache miss (hit=false), got hit=true", i) + } + } +} + +func TestToIndexValues(t *testing.T) { + ck := CacheKey{ + SST: SST{ + SourceName: "foo", + Scope: "foo", + Type: "foo", + }, + } + + t.Run("with just SST", func(t *testing.T) { + iv := ck.ToIndexValues() + + if iv.SSTHash != ck.SST.Hash() { + t.Error("hash mismatch") + } + }) + + t.Run("with SST & Method", func(t *testing.T) { + ck.Method = sdp.QueryMethod_GET.Enum() + iv := ck.ToIndexValues() + + if iv.Method != sdp.QueryMethod_GET { + t.Errorf("expected %v, got %v", sdp.QueryMethod_GET, iv.Method) + } + }) + + t.Run("with SST & Query", func(t *testing.T) { + q := "query" + ck.Query = &q + iv := ck.ToIndexValues() + + if iv.Query != "query" { + t.Errorf("expected %v, got %v", "query", iv.Query) + } + }) + + t.Run("with SST & UniqueAttributeValue", func(t *testing.T) { + q := "foo" + ck.UniqueAttributeValue = &q + iv := ck.ToIndexValues() + + if iv.UniqueAttributeValue != "foo" { + t.Errorf("expected %v, got %v", "foo", iv.UniqueAttributeValue) + } + }) +} + +func TestUnexpiredOverwriteLogging(t *testing.T) { + cache := NewCache(t.Context()) + + t.Run("overwriting unexpired entry increments counter", func(t *testing.T) { + ctx := t.Context() + // Create an item and cache key + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + // Store the item with a long TTL (10 seconds) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Store the same item again before it expires (overwrite will be tracked via span attributes) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Store it again + cache.StoreItem(ctx, item, 10*time.Second, ck) + }) + + t.Run("overwriting expired entry does not increment counter", func(t *testing.T) { + ctx := t.Context() + // Create a new cache for this test + cache := NewCache(ctx) + + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + + // Store the item with a very short TTL + cache.StoreItem(ctx, item, 1*time.Millisecond, ck) + + // Wait for it to expire + time.Sleep(10 * time.Millisecond) + + // Store the same item again after it expired (overwrite tracking via span attributes) + cache.StoreItem(ctx, item, 10*time.Second, ck) + }) + + t.Run("overwriting different items does not increment counter", func(t *testing.T) { + ctx := t.Context() + // Create a new cache for this test + cache := NewCache(ctx) + + item1 := GenerateRandomItem() + item2 := GenerateRandomItem() + + ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), item1.GetMetadata().GetSourceName()) + ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), item2.GetMetadata().GetSourceName()) + + // Store two different items (no overwrites, just new items) + cache.StoreItem(ctx, item1, 10*time.Second, ck1) + cache.StoreItem(ctx, item2, 10*time.Second, ck2) + }) + + t.Run("overwriting error entries increments counter", func(t *testing.T) { + ctx := t.Context() + // Create a new cache for this test + cache := NewCache(ctx) + + sst := SST{ + SourceName: "test-source", + Scope: "test-scope", + Type: "test-type", + } + + method := sdp.QueryMethod_LIST + query := "test-query" + + ck := CacheKey{ + SST: sst, + Method: &method, + Query: &query, + } + + // Store an error + cache.StoreError(ctx, errors.New("test error"), 10*time.Second, ck) + + // Store the same error again before it expires (overwrite will be tracked via span attributes) + cache.StoreError(ctx, errors.New("another error"), 10*time.Second, ck) + }) +} + +// TestBoltCacheCloseAndDestroy verifies that CloseAndDestroy() correctly +// closes the database and deletes the cache file. +func TestBoltCacheCloseAndDestroy(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + // Create a cache and store some data + ctx := t.Context() + cache1, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + + // Store an item + item1 := GenerateRandomItem() + ck1 := CacheKeyFromQuery(item1.GetMetadata().GetSourceQuery(), item1.GetMetadata().GetSourceName()) + cache1.StoreItem(ctx, item1, 10*time.Second, ck1) + + // Store another item with a short TTL (will expire) + item2 := GenerateRandomItem() + ck2 := CacheKeyFromQuery(item2.GetMetadata().GetSourceQuery(), item2.GetMetadata().GetSourceName()) + cache1.StoreItem(ctx, item2, 100*time.Millisecond, ck2) + + // Verify both items are in the cache + items, err := testSearch(t.Context(), cache1, ck1) + if err != nil { + t.Errorf("failed to search for item1: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item for ck1, got %d", len(items)) + } + + // Verify the cache file exists + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + t.Fatal("cache file should exist before CloseAndDestroy") + } + + // Close and destroy the cache + if err := cache1.CloseAndDestroy(); err != nil { + t.Fatalf("failed to close and destroy cache1: %v", err) + } + + // Verify the cache file is deleted + if _, err := os.Stat(cachePath); !os.IsNotExist(err) { + t.Error("cache file should be deleted after CloseAndDestroy") + } + + // Create a new cache at the same path - should create a fresh, empty cache + cache2, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create new BoltCache: %v", err) + } + defer func() { + _ = cache2.CloseAndDestroy() + }() + + // Verify the old item is NOT accessible (cache was destroyed) + items, err = testSearch(ctx, cache2, ck1) + if !errors.Is(err, ErrCacheNotFound) { + t.Errorf("expected cache miss for item1 in new cache, got: err=%v, items=%d", err, len(items)) + } + + // Verify we can store new items in the fresh cache + item3 := GenerateRandomItem() + ck3 := CacheKeyFromQuery(item3.GetMetadata().GetSourceQuery(), item3.GetMetadata().GetSourceName()) + cache2.StoreItem(ctx, item3, 10*time.Second, ck3) + + items, err = testSearch(ctx, cache2, ck3) + if err != nil { + t.Errorf("failed to search for newly stored item3: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item for ck3, got %d", len(items)) + } +} + +// TestBoltCacheOperationsAfterCloseAndDestroy verifies that operations after +// CloseAndDestroy() return proper errors instead of panicking. +func TestBoltCacheOperationsAfterCloseAndDestroy(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + ctx := t.Context() + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + + // Store an item before closing + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Close and destroy the cache + if err := cache.CloseAndDestroy(); err != nil { + t.Fatalf("failed to close and destroy cache: %v", err) + } + + // Now try various operations after the cache is closed and destroyed + // These should return errors, not panic + + t.Run("Search after CloseAndDestroy", func(t *testing.T) { + // This should error because the database is closed + _, err := testSearch(ctx, cache, ck) + if err == nil { + t.Error("expected error when searching after CloseAndDestroy, got nil") + } + t.Logf("Search returned expected error: %v", err) + }) + + t.Run("StoreItem after CloseAndDestroy", func(t *testing.T) { + // This should not panic - it might silently fail or error + // The key is that it doesn't panic + newItem := GenerateRandomItem() + newCk := CacheKeyFromQuery(newItem.GetMetadata().GetSourceQuery(), newItem.GetMetadata().GetSourceName()) + + // This should either complete without panic or handle the closed DB gracefully + cache.StoreItem(ctx, newItem, 10*time.Second, newCk) + t.Log("StoreItem completed without panic (may have failed internally)") + }) + + t.Run("Delete after CloseAndDestroy", func(t *testing.T) { + // This should not panic + cache.Delete(ck) + t.Log("Delete completed without panic (may have failed internally)") + }) + + t.Run("Purge after CloseAndDestroy", func(t *testing.T) { + // This should not panic + stats := cache.Purge(ctx, time.Now()) + t.Logf("Purge completed without panic, purged %d items", stats.NumPurged) + }) +} + +// TestBoltCacheConcurrentCloseAndDestroy verifies that CloseAndDestroy() +// properly synchronizes with concurrent operations using the compaction lock. +func TestBoltCacheConcurrentCloseAndDestroy(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + ctx := t.Context() + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + + // Store some items + for range 10 { + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + } + + // Start some concurrent operations + var wg sync.WaitGroup + numOperations := 50 + + // Launch concurrent read/write operations + for range numOperations { + wg.Add(1) + go func() { + defer wg.Done() + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + }() + } + + // Wait a bit to let operations start + time.Sleep(10 * time.Millisecond) + + // Close and destroy while operations are in flight + // The compaction lock should serialize this properly + wg.Add(1) + go func() { + defer wg.Done() + err := cache.CloseAndDestroy() + if err != nil { + t.Logf("CloseAndDestroy returned error: %v", err) + } + }() + + // Wait for all operations to complete + wg.Wait() + + // Verify the file is deleted + if _, err := os.Stat(cachePath); !os.IsNotExist(err) { + t.Error("cache file should be deleted after CloseAndDestroy") + } + + t.Log("Concurrent operations with CloseAndDestroy completed without data races") +} + +// TestBoltCacheDiskFullErrorDetection tests the isDiskFullError helper function +func TestBoltCacheDiskFullErrorDetection(t *testing.T) { + // This test verifies that isDiskFullError correctly identifies disk full errors + // We can't easily simulate actual disk full in tests, but we can test the detection logic + + // Note: We can't directly test syscall.ENOSPC without actually filling the disk, + // but we can verify the function exists and works with the error types it's designed for. + // In a real scenario, BoltDB would return syscall.ENOSPC when the disk is full. + + // Test that non-disk-full errors are not detected + regularErr := errors.New("some other error") + if isDiskFullError(regularErr) { + t.Error("isDiskFullError should return false for regular errors") + } + + // Test nil error + if isDiskFullError(nil) { + t.Error("isDiskFullError should return false for nil") + } +} + +// TestBoltCacheDeleteOnDiskFull tests that the cache is deleted when disk is full +// and cleanup doesn't help. Since we can't easily simulate disk full in unit tests, +// this test verifies the deleteCacheFile method works correctly. +func TestBoltCacheDeleteOnDiskFull(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + // Create a cache and store some data + ctx := t.Context() + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + + // Store an item + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Verify the cache file exists + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + t.Fatal("cache file should exist") + } + + // Verify item is in cache + items, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Errorf("failed to search: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item, got %d", len(items)) + } + + // Delete the cache file (cache is already *BoltCache) + if err := cache.deleteCacheFile(ctx); err != nil { + t.Fatalf("failed to delete cache file: %v", err) + } + + // Verify the cache file is gone + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + t.Error("cache file should be recreated") + } + + // Verify the database is closed (can't search anymore) + _, _ = testSearch(t.Context(), cache, ck) + // The search might fail or return empty, but the important thing is the file is gone + // and we can't use the cache anymore +} + +// TestBoltCacheDiskFullDuringCompact tests error handling during compaction. +// Since we can't easily simulate disk full, this test verifies the compaction +// process works normally and that the error handling paths exist. +func TestBoltCacheDiskFullDuringCompact(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath, WithCompactThreshold(1024)) // Small threshold to trigger compaction + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { + _ = cache.CloseAndDestroy() + }() + + ctx := t.Context() + + // Store enough items to trigger compaction + // We'll store items and then delete them to accumulate deleted bytes + for range 10 { + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + } + + // Manually set deleted bytes to trigger compaction + cache.addDeletedBytes(cache.CompactThreshold) + + // Trigger purge which should trigger compaction + stats := cache.Purge(ctx, time.Now().Add(-1*time.Hour)) // Purge items from an hour ago (none should exist) + _ = stats // Use stats to avoid unused variable + + // Verify cache still works after compaction attempt + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + items, err := testSearch(t.Context(), cache, ck) + if err != nil { + t.Errorf("failed to search after compaction: %v", err) + } + if len(items) != 1 { + t.Errorf("expected 1 item after compaction, got %d", len(items)) + } +} + +// TestBoltCacheLookupDeduplication tests that multiple concurrent Lookup calls +// for the same cache key result in only one caller doing the actual work. +func TestBoltCacheLookupDeduplication(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + // Create a cache key for the test - use LIST method to avoid UniqueAttributeValue matching issues + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + + // Track how many goroutines actually do work (get cache miss as first caller) + var workCount int32 + var mu sync.Mutex + var wg sync.WaitGroup + + numGoroutines := 10 + results := make([]struct { + hit bool + items []*sdp.Item + err *sdp.QueryError + }, numGoroutines) + + // Start barrier to ensure all goroutines start at roughly the same time + startBarrier := make(chan struct{}) + + for i := range numGoroutines { + wg.Add(1) + go func(idx int) { + defer wg.Done() + + // Wait for the start signal + <-startBarrier + + // Lookup the cache - all should get miss initially + hit, ck, items, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + + if !hit { + // This goroutine is doing the work + mu.Lock() + workCount++ + mu.Unlock() + + // Simulate some work + time.Sleep(50 * time.Millisecond) + + // Create and store the item + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Re-lookup to get the stored item for our result + hit, _, items, qErr, done = cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + } + + results[idx] = struct { + hit bool + items []*sdp.Item + err *sdp.QueryError + }{hit, items, qErr} + }(i) + } + + // Release all goroutines at once + close(startBarrier) + + // Wait for all goroutines to complete + wg.Wait() + + // Verify that only one goroutine did the work + if workCount != 1 { + t.Errorf("expected exactly 1 goroutine to do work, got %d", workCount) + } + + // Verify all goroutines got results + for i, r := range results { + if !r.hit { + t.Errorf("goroutine %d: expected cache hit after dedup, got miss", i) + } + if len(r.items) != 1 { + t.Errorf("goroutine %d: expected 1 item, got %d", i, len(r.items)) + } + } +} + +// TestBoltCacheLookupDeduplicationTimeout tests that waiters properly timeout +// when the context is cancelled. +func TestBoltCacheLookupDeduplicationTimeout(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_GET + query := "timeout-test" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // First goroutine: does the work but takes a long time + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate slow work + time.Sleep(500 * time.Millisecond) + + // Store the item + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + cache.StoreItem(ctx, item, 10*time.Second, ck) + }() + + // Second goroutine: should timeout waiting + var secondHit bool + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + // Use a short timeout context + shortCtx, done := context.WithTimeout(ctx, 50*time.Millisecond) + defer done() + + hit, _, _, _, done := cache.Lookup(shortCtx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + secondHit = hit + }() + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // Second goroutine should have timed out and returned miss + if secondHit { + t.Error("second goroutine should have timed out and returned miss") + } +} + +// TestBoltCacheLookupDeduplicationError tests that waiters receive the error +// when the first caller stores an error. +func TestBoltCacheLookupDeduplicationError(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_GET + query := "error-test" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + expectedError := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "item not found", + Scope: sst.Scope, + SourceName: sst.SourceName, + ItemType: sst.Type, + } + + // Track results from waiters + var waiterErrors []*sdp.QueryError + var waiterMu sync.Mutex + + numWaiters := 5 + + // First goroutine: does the work and stores an error + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that results in an error + time.Sleep(50 * time.Millisecond) + + // Store the error + cache.StoreError(ctx, expectedError, 10*time.Second, ck) + }() + + // Waiter goroutines: should receive the error + for range numWaiters { + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + hit, _, _, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + waiterMu.Lock() + if hit && qErr != nil { + waiterErrors = append(waiterErrors, qErr) + } + waiterMu.Unlock() + }() + } + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // All waiters should have received the error + if len(waiterErrors) != numWaiters { + t.Errorf("expected %d waiters to receive error, got %d", numWaiters, len(waiterErrors)) + } + + // Verify the error content + for i, qErr := range waiterErrors { + if qErr.GetErrorType() != expectedError.GetErrorType() { + t.Errorf("waiter %d: expected error type %v, got %v", i, expectedError.GetErrorType(), qErr.GetErrorType()) + } + } +} + +// TestBoltCacheLookupDeduplicationCancel tests the Cancel() path for error recovery. +func TestBoltCacheLookupDeduplicationCancel(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_GET + query := "done-test" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // Track results + var waiterHits []bool + var waiterMu sync.Mutex + + numWaiters := 3 + + // First goroutine: starts work but then calls done() without storing anything + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + if hit { + t.Error("first goroutine: expected cache miss") + done() + return + } + + // Simulate work that fails - done the pending work + time.Sleep(50 * time.Millisecond) + done() + }() + + // Waiter goroutines + for range numWaiters { + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + waiterMu.Lock() + waiterHits = append(waiterHits, hit) + waiterMu.Unlock() + }() + } + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // When work is cancelled, waiters receive ok=false from Wait + // (because entry.cancelled is true) and return a cache miss without re-checking. + // This is the correct behavior - waiters don't hang forever and can retry. + if len(waiterHits) != numWaiters { + t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) + } +} + +// TestBoltCacheLookupDeduplicationCompleteWithoutStore tests the scenario where +// Complete is called but nothing was stored in the cache. This tests the explicit +// ErrCacheNotFound check in the re-check logic. +func TestBoltCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { + tempDir := t.TempDir() + cachePath := filepath.Join(tempDir, "cache.db") + + cache, err := NewBoltCache(cachePath) + if err != nil { + t.Fatalf("failed to create BoltCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + sst := SST{SourceName: "test-source", Scope: "test-scope", Type: "test-type"} + method := sdp.QueryMethod_LIST + query := "complete-without-store-test" + + var wg sync.WaitGroup + startBarrier := make(chan struct{}) + + // Track results + var waiterHits []bool + var waiterMu sync.Mutex + + numWaiters := 3 + + // First goroutine: starts work and completes without storing anything + // This simulates a LIST query that returns 0 items + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + if hit { + t.Error("first goroutine: expected cache miss") + return + } + + // Simulate work that completes successfully but returns nothing + time.Sleep(50 * time.Millisecond) + + // Complete without storing anything - no items, no error + // This triggers the ErrCacheNotFound path in waiters' re-check + cache.pending.Complete(ck.String()) + }() + + // Waiter goroutines + for range numWaiters { + wg.Add(1) + go func() { + defer wg.Done() + <-startBarrier + + // Small delay to ensure first goroutine starts first + time.Sleep(10 * time.Millisecond) + + hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) + defer done() + + waiterMu.Lock() + waiterHits = append(waiterHits, hit) + waiterMu.Unlock() + }() + } + + // Release all goroutines + close(startBarrier) + wg.Wait() + + // When Complete is called without storing anything: + // 1. Waiters' Wait returns ok=true (not cancelled) + // 2. Waiters re-check the cache and get ErrCacheNotFound + // 3. Waiters return hit=false (cache miss) + if len(waiterHits) != numWaiters { + t.Errorf("expected %d waiter results, got %d", numWaiters, len(waiterHits)) + } + + // All waiters should get a cache miss since nothing was stored + for i, hit := range waiterHits { + if hit { + t.Errorf("waiter %d: expected cache miss (hit=false), got hit=true", i) + } + } +} + +// TestPendingWorkUnit tests the pendingWork component in isolation. +func TestPendingWorkUnit(t *testing.T) { + t.Run("StartWork first caller", func(t *testing.T) { + pw := newPendingWork() + shouldWork, entry := pw.StartWork("key1") + + if !shouldWork { + t.Error("first caller should do work") + } + if entry == nil { + t.Error("entry should not be nil") + } + }) + + t.Run("StartWork second caller", func(t *testing.T) { + pw := newPendingWork() + + // First caller + shouldWork1, entry1 := pw.StartWork("key1") + if !shouldWork1 { + t.Error("first caller should do work") + } + + // Second caller for same key + shouldWork2, entry2 := pw.StartWork("key1") + if shouldWork2 { + t.Error("second caller should not do work") + } + if entry2 != entry1 { + t.Error("second caller should get same entry") + } + }) + + t.Run("Complete wakes waiters", func(t *testing.T) { + pw := newPendingWork() + ctx := context.Background() + + // First caller + _, entry := pw.StartWork("key1") + + // Second caller waits + var wg sync.WaitGroup + var waitOk bool + + wg.Add(1) + go func() { + defer wg.Done() + waitOk = pw.Wait(ctx, entry) + }() + + // Give waiter time to start waiting + time.Sleep(10 * time.Millisecond) + + // Complete the work + pw.Complete("key1") + + wg.Wait() + + if !waitOk { + t.Error("wait should succeed") + } + }) + + t.Run("Wait respects context donelation", func(t *testing.T) { + pw := newPendingWork() + ctx, done := context.WithCancel(context.Background()) + + // First caller + _, entry := pw.StartWork("key1") + + // Second caller waits with donelable context + var wg sync.WaitGroup + var waitOk bool + + wg.Add(1) + go func() { + defer wg.Done() + waitOk = pw.Wait(ctx, entry) + }() + + // Give waiter time to start waiting + time.Sleep(10 * time.Millisecond) + + // Cancel the context + done() + + wg.Wait() + + if waitOk { + t.Error("wait should fail due to context donelation") + } + }) +} diff --git a/go/sdpcache/item_generator_test.go b/go/sdpcache/item_generator_test.go new file mode 100644 index 00000000..ebe9bc91 --- /dev/null +++ b/go/sdpcache/item_generator_test.go @@ -0,0 +1,135 @@ +package sdpcache + +import ( + "math/rand" + "time" + + "github.com/google/uuid" + "github.com/overmindtech/cli/go/sdp-go" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var Types = []string{ + "person", + "dog", + "kite", + "flag", + "cat", + "leopard", + "fish", + "bird", + "kangaroo", + "ostrich", + "emu", + "hawk", + "mole", + "badger", + "lemur", +} + +const MaxAttributes = 30 +const MaxTags = 10 +const MaxTagKeyLength = 10 +const MaxTagValueLength = 10 +const MaxAttributeKeyLength = 20 +const MaxAttributeValueLength = 50 + +// TODO(LIQs): rewrite this to `MaxEdges` +const MaxLinkedItems = 10 + +// TODO(LIQs): delete +const MaxLinkedItemQueries = 10 + +// GenerateRandomItem Generates a random item and the tags for this item. The +// tags include the name, type and a tag called "all" with a value of "all" +func GenerateRandomItem() *sdp.Item { + attrs := make(map[string]interface{}) + + name := randSeq(rand.Intn(MaxAttributeValueLength)) + typ := Types[rand.Intn(len(Types))] + scope := randSeq(rand.Intn(MaxAttributeKeyLength)) + attrs["name"] = name + + for range rand.Intn(MaxAttributes) { + attrs[randSeq(rand.Intn(MaxAttributeKeyLength))] = randSeq(rand.Intn(MaxAttributeValueLength)) + } + + attributes, _ := sdp.ToAttributes(attrs) + + tags := make(map[string]string) + + for range rand.Intn(MaxTags) { + tags[randSeq(rand.Intn(MaxTagKeyLength))] = randSeq(rand.Intn(MaxTagValueLength)) + } + + // TODO(LIQs): rewrite this to `MaxEdges` and return and additional []*sdp.Edge + linkedItems := make([]*sdp.LinkedItem, rand.Intn(MaxLinkedItems)) + + for i := range linkedItems { + linkedItems[i] = &sdp.LinkedItem{Item: &sdp.Reference{ + Type: randSeq(rand.Intn(MaxAttributeKeyLength)), + UniqueAttributeValue: randSeq(rand.Intn(MaxAttributeValueLength)), + Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), + }} + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, rand.Intn(MaxLinkedItemQueries)) + + for i := range linkedItemQueries { + linkedItemQueries[i] = &sdp.LinkedItemQuery{Query: &sdp.Query{ + Type: randSeq(rand.Intn(MaxAttributeKeyLength)), + Method: sdp.QueryMethod(rand.Intn(3)), + Query: randSeq(rand.Intn(MaxAttributeValueLength)), + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: rand.Uint32(), + FollowOnlyBlastPropagation: rand.Intn(2) == 0, + }, + Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), + }} + } + + // Generate health (which is an int32 between 0 and 4) + health := sdp.Health(rand.Intn(int(sdp.Health_HEALTH_PENDING) + 1)) + + queryUuid := uuid.New() + + item := sdp.Item{ + Type: typ, + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + LinkedItemQueries: linkedItemQueries, + LinkedItems: linkedItems, + Metadata: &sdp.Metadata{ + SourceName: randSeq(rand.Intn(MaxAttributeKeyLength)), + SourceQuery: &sdp.Query{ + Type: typ, + Method: sdp.QueryMethod_GET, + Query: name, + RecursionBehaviour: &sdp.Query_RecursionBehaviour{ + LinkDepth: 1, + }, + Scope: scope, + UUID: queryUuid[:], + }, + Timestamp: timestamppb.New(time.Now()), + SourceDuration: durationpb.New(time.Millisecond * time.Duration(rand.Int63())), + SourceDurationPerItem: durationpb.New(time.Millisecond * time.Duration(rand.Int63())), + }, + Tags: tags, + Health: &health, + } + + return &item +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/go/sdpcache/pending.go b/go/sdpcache/pending.go new file mode 100644 index 00000000..dc72864b --- /dev/null +++ b/go/sdpcache/pending.go @@ -0,0 +1,114 @@ +package sdpcache + +import ( + "context" + "sync" + "time" +) + +// pendingWork tracks in-flight cache lookups to prevent duplicate work. +// When multiple goroutines request the same cache key simultaneously, +// only the first one does the actual work while others wait for the result. +type pendingWork struct { + mu sync.Mutex + pending map[string]*workEntry +} + +// maxPendingWorkAge is a safety timeout for pending work +const maxPendingWorkAge = 5 * time.Minute + +// workEntry represents a pending piece of work that one or more goroutines +// are waiting on. +type workEntry struct { + done chan struct{} + cancelled bool + startTime time.Time +} + +// newPendingWork creates a new pendingWork tracker. +func newPendingWork() *pendingWork { + return &pendingWork{ + pending: make(map[string]*workEntry), + } +} + +// StartWork checks if work is already pending for the given key. +// If no work is pending, it creates a new entry and returns (true, entry) - +// the caller should do the work and call Complete when done. +// If work is already pending, it returns (false, entry) - the caller should +// call Wait on the entry to get the result. +func (p *pendingWork) StartWork(key string) (shouldWork bool, entry *workEntry) { + p.mu.Lock() + defer p.mu.Unlock() + + if existing, ok := p.pending[key]; ok { + return false, existing + } + + entry = &workEntry{ + done: make(chan struct{}), + startTime: time.Now(), + } + p.pending[key] = entry + return true, entry +} + +// Wait blocks until the work entry is ready or the context is cancelled. +// Returns ok=true if the work completed successfully (caller should re-check cache). +// Returns ok=false if the context was cancelled or work was cancelled. +func (p *pendingWork) Wait(ctx context.Context, entry *workEntry) (ok bool) { + // Calculate safety timeout based on when work started + deadline := entry.startTime.Add(maxPendingWorkAge) + timeUntilDeadline := time.Until(deadline) + + // If we're already past the deadline, return immediately + if timeUntilDeadline <= 0 { + return false + } + + // Create a timer for the safety timeout + timer := time.NewTimer(timeUntilDeadline) + defer timer.Stop() + + select { + case <-entry.done: + // Work completed normally + return !entry.cancelled + case <-ctx.Done(): + // Context was cancelled by caller + return false + case <-timer.C: + // SAFETY: Work has been pending too long (worker likely forgot to call Complete/Cancel) + // Return false so caller gets a cache miss and can retry + return false + } +} + +// Complete marks the work as done and wakes all waiters. +// Waiters will receive ok=true and should re-lookup the cache. +func (p *pendingWork) Complete(key string) { + p.mu.Lock() + defer p.mu.Unlock() + + entry, ok := p.pending[key] + if !ok { + return + } + delete(p.pending, key) + close(entry.done) +} + +// Cancel removes a pending work entry without storing a result. +// Waiters will receive ok=false and should retry or return error. +func (p *pendingWork) Cancel(key string) { + p.mu.Lock() + defer p.mu.Unlock() + + entry, ok := p.pending[key] + if !ok { + return + } + delete(p.pending, key) + entry.cancelled = true + close(entry.done) +} diff --git a/go/tracing/deferlog.go b/go/tracing/deferlog.go new file mode 100644 index 00000000..b2fb04c7 --- /dev/null +++ b/go/tracing/deferlog.go @@ -0,0 +1,77 @@ +package tracing + +import ( + "context" + "fmt" + "os" + "runtime/debug" + + "github.com/getsentry/sentry-go" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// LogRecoverToReturn Recovers from a panic, logs and forwards it sentry and +// otel, then returns. Does nothing when there is no panic. +func LogRecoverToReturn(ctx context.Context, loc string) { + err := recover() + if err == nil { + return + } + + stack := string(debug.Stack()) + HandleError(ctx, loc, err, stack) +} + +// LogRecoverToError Recovers from a panic, logs and forwards it sentry and +// otel, then returns a new error describing the panic. Does nothing when there +// is no panic. +func LogRecoverToError(ctx context.Context, loc string) error { + err := recover() + if err == nil { + return nil + } + + stack := string(debug.Stack()) + HandleError(ctx, loc, err, stack) + + return fmt.Errorf("panic recovered: %v", err) +} + +// LogRecoverToExit Recovers from a panic, logs and forwards it sentry and otel, +// then exits the entire process. Does nothing when there is no panic. +func LogRecoverToExit(ctx context.Context, loc string) { + err := recover() + if err == nil { + return + } + + stack := string(debug.Stack()) + HandleError(ctx, loc, err, stack) + + // ensure that errors still get sent out + ShutdownTracer(ctx) + + os.Exit(1) +} + +func HandleError(ctx context.Context, loc string, err interface{}, stack string) { + msg := fmt.Sprintf("unhandled panic in %v, exiting: %v", loc, err) + + hub := sentry.CurrentHub() + if hub != nil { + hub.Recover(err) + } + + // always log to stderr (no WithContext!) + log.WithFields(log.Fields{"loc": loc, "stack": stack}).Error(msg) + + // if we have a context, try attaching additional info to the span + if ctx != nil { + log.WithContext(ctx).WithFields(log.Fields{"loc": loc, "stack": stack}).Error(msg) + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.String("ovm.panic.loc", loc)) + span.SetAttributes(attribute.String("ovm.panic.stack", stack)) + } +} diff --git a/go/tracing/header_carrier.go b/go/tracing/header_carrier.go new file mode 100644 index 00000000..de2b9fd6 --- /dev/null +++ b/go/tracing/header_carrier.go @@ -0,0 +1,31 @@ +package tracing + +import "github.com/nats-io/nats.go" + +// HeaderCarrier is a custom wrapper on top of nats.Headers for otel's TextMapCarrier. +type HeaderCarrier struct { + headers nats.Header +} + +// NewNatsHeaderCarrier creates a new HeaderCarrier. +func NewNatsHeaderCarrier(h nats.Header) *HeaderCarrier { + return &HeaderCarrier{ + headers: h, + } +} + +func (c *HeaderCarrier) Get(key string) string { + return c.headers.Get(key) +} + +func (c *HeaderCarrier) Set(key, value string) { + c.headers.Set(key, value) +} + +func (c *HeaderCarrier) Keys() []string { + keys := make([]string, 0, len(c.headers)) + for key := range c.headers { + keys = append(keys, key) + } + return keys +} diff --git a/go/tracing/main.go b/go/tracing/main.go new file mode 100644 index 00000000..013b65e3 --- /dev/null +++ b/go/tracing/main.go @@ -0,0 +1,383 @@ +package tracing + +import ( + "context" + "fmt" + "net/http" + "os" + "path/filepath" + "slices" + "time" + + _ "embed" + + "github.com/MrAlias/otel-schema-utils/schema" + "github.com/getsentry/sentry-go" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "go.opentelemetry.io/contrib/detectors/aws/ec2/v2" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" +) + +const instrumentationName = "github.com/overmindtech/workspace" + +// the following vars will be set during the build using `ldflags`, eg: +// +// go build -ldflags "-X github.com/overmindtech/cli/go/tracing.version=$VERSION" -o your-app +// +// This allows caching to work for dev and removes the last `go generate` +// requirement from the build. If we were embedding the version here each time +// we would always produce a slightly different compiled binary, and therefore +// it would look like there was a change each time +var ( + version = "dev" + commit = "none" +) + +var ( + tracer = otel.GetTracerProvider().Tracer( + instrumentationName, + trace.WithInstrumentationVersion(version), + trace.WithInstrumentationAttributes( + attribute.String("build.commit", commit), + ), + trace.WithSchemaURL(semconv.SchemaURL), + ) +) + +func Tracer() trace.Tracer { + return tracer +} + +// hasGitDir returns true if the current directory or any parent directory contains a .git directory +func hasGitDir() bool { + // Start with the current working directory + dir, err := os.Getwd() + if err != nil { + return false + } + + // Check the current directory and all parent directories + for { + // Check if .git exists in this directory + _, err := os.Stat(filepath.Join(dir, ".git")) + if err == nil { + return true // Found a .git directory + } + + // Get the parent directory + parentDir := filepath.Dir(dir) + + // If we've reached the root directory, stop searching + if parentDir == dir { + break + } + + // Move up to the parent directory + dir = parentDir + } + + return false // No .git directory found +} + +func tracingResource(component string) *resource.Resource { + // Identify your application using resource detection + resources := []*resource.Resource{} + + // the EC2 detector takes ~10s to time out outside EC2 + // disable it if we're running from a git checkout + if !hasGitDir() { + ec2Res, err := resource.New(context.Background(), resource.WithDetectors(ec2.NewResourceDetector())) + if err != nil { + log.WithError(err).Error("error initialising EC2 resource detector") + return nil + } + resources = append(resources, ec2Res) + } + + // Needs https://github.com/open-telemetry/opentelemetry-go-contrib/issues/1856 fixed first + // // the EKS detector is temperamental and doesn't like running outside of kube + // // hence we need to keep it from running when we know there's no kube + // if !viper.GetBool("disable-kube") { + // // Use the AWS resource detector to detect information about the runtime environment + // detectors = append(detectors, eks.NewResourceDetector()) + // } + + hostRes, err := resource.New(context.Background(), + resource.WithHost(), + resource.WithOS(), + resource.WithProcess(), + resource.WithContainer(), + resource.WithTelemetrySDK(), + ) + if err != nil { + log.WithError(err).Error("error initialising host resource") + return nil + } + resources = append(resources, hostRes) + + localRes, err := resource.New(context.Background(), + resource.WithSchemaURL(semconv.SchemaURL), + // Add your own custom attributes to identify your application + resource.WithAttributes( + semconv.ServiceNameKey.String(component), + semconv.ServiceVersionKey.String(version), + attribute.String("build.commit", commit), + ), + ) + if err != nil { + log.WithError(err).Error("error initialising local resource") + return nil + } + resources = append(resources, localRes) + + conv := schema.NewConverter(schema.DefaultClient) + res, err := conv.MergeResources(context.Background(), semconv.SchemaURL, resources...) + + if err != nil { + log.WithError(err).Error("error merging resource") + return nil + } + return res +} + +var tp *sdktrace.TracerProvider +var healthTp *sdktrace.TracerProvider + +// HealthCheckTracerProvider returns the tracer provider used for health checks. This has a built-in 1:100 sampler for health checks that are not captured by the default UserAgentSampler for ELB and kube-probe requests. +func HealthCheckTracerProvider() *sdktrace.TracerProvider { + if healthTp == nil { + panic("tracer providers not initialised") + } + return healthTp +} + +// healthCheckTracer is the tracer used for health checks. This is heavily sampled to avoid getting spammed by k8s or ELBs +func HealthCheckTracer() trace.Tracer { + return HealthCheckTracerProvider().Tracer( + instrumentationName, + trace.WithInstrumentationVersion(version), + trace.WithSchemaURL(semconv.SchemaURL), + trace.WithInstrumentationAttributes( + attribute.Bool("ovm.healthCheck", true), + ), + ) +} + +// InitTracerWithUpstreams initialises the tracer with uploading directly to Honeycomb and sentry if `honeycombApiKey` and `sentryDSN` is set respectively. `component` is used as the service name. +func InitTracerWithUpstreams(component, honeycombApiKey, sentryDSN string, opts ...otlptracehttp.Option) error { + if sentryDSN != "" { + var environment string + switch viper.GetString("run-mode") { + case "release": + environment = "prod" + case "test": + environment = "dogfood" + case "debug": + environment = "local" + default: + // Fallback to dev for backward compatibility + environment = "dev" + } + err := sentry.Init(sentry.ClientOptions{ + Dsn: sentryDSN, + AttachStacktrace: true, + EnableTracing: false, + Environment: environment, + // Set TracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + // We recommend adjusting this value in production, + TracesSampleRate: 1.0, + }) + if err != nil { + log.Errorf("sentry.Init: %s", err) + } + // setup recovery for an unexpected panic in this function + defer sentry.Flush(2 * time.Second) + defer sentry.Recover() + log.Trace("sentry configured") + } + + if honeycombApiKey != "" { + opts = append(opts, + otlptracehttp.WithEndpoint("api.honeycomb.io"), + otlptracehttp.WithHeaders(map[string]string{"x-honeycomb-team": honeycombApiKey}), + ) + } else { + // If no Honeycomb API key is provided, use the hardcoded OTLP collector + // endpoint, which is provided by the otel-collector service in the otel + // namespace. Since this a node-local service, it does not use TLS. + opts = append(opts, + otlptracehttp.WithEndpoint("otelcol-node-opentelemetry-collector.otel.svc.cluster.local:4318"), + otlptracehttp.WithInsecure(), + ) + } + + return InitTracer(component, opts...) +} + +func InitTracer(component string, opts ...otlptracehttp.Option) error { + client := otlptracehttp.NewClient(opts...) + otlpExp, err := otlptrace.New(context.Background(), client) + if err != nil { + return fmt.Errorf("creating OTLP trace exporter: %w", err) + } + + // Create unified sampler for health checks and otelpgx spans + overmindSampler := NewOvermindSampler() + + tracerOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(otlpExp), + sdktrace.WithResource(tracingResource(component)), + sdktrace.WithSampler(sdktrace.ParentBased(overmindSampler)), + } + if viper.GetBool("stdout-trace-dump") { + stdoutExp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + return err + } + tracerOpts = append(tracerOpts, sdktrace.WithBatcher(stdoutExp)) + } + tp = sdktrace.NewTracerProvider(tracerOpts...) + + // Set the default tracer provider for all the libraries + otel.SetTracerProvider(tp) + + tracerOpts = append(tracerOpts, sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased((0.1))))) + healthTp = sdktrace.NewTracerProvider(tracerOpts...) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + return nil +} + +func ShutdownTracer(ctx context.Context) { + // Flush buffered events before the program terminates. + defer sentry.Flush(5 * time.Second) + + // detach from the parent's cancellation, and ensure that we do not wait + // indefinitely on the trace provider shutdown + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + if tp != nil { + if err := tp.ForceFlush(ctx); err != nil { + log.WithContext(ctx).WithError(err).Error("Error flushing tracer provider") + } + if err := tp.Shutdown(ctx); err != nil { + log.WithContext(ctx).WithError(err).Error("Error shutting down tracer provider") + } + } + log.WithContext(ctx).Trace("tracing has shut down") +} + +// SamplingRule defines a single sampling rule with a rate and matching function +type SamplingRule struct { + SampleRate int + ShouldSample func(sdktrace.SamplingParameters) bool +} + +// OvermindSampler is a unified sampler that evaluates multiple sampling rules in order +type OvermindSampler struct { + rules []SamplingRule + ruleSamplers []sdktrace.Sampler +} + +// NewOvermindSampler creates a new unified sampler with the default rules +func NewOvermindSampler() *OvermindSampler { + rules := []SamplingRule{ + { + SampleRate: 200, + ShouldSample: UserAgentMatcher("ELB-HealthChecker/2.0", "kube-probe/1.27+"), + }, + { + SampleRate: 10, + ShouldSample: SpanNameMatcher("pool.acquire"), + }, + } + + // Pre-allocate samplers for each rule + ruleSamplers := make([]sdktrace.Sampler, 0, len(rules)) + for _, rule := range rules { + var sampler sdktrace.Sampler + switch { + case rule.SampleRate <= 0: + sampler = sdktrace.NeverSample() + case rule.SampleRate == 1: + sampler = sdktrace.AlwaysSample() + default: + sampler = sdktrace.TraceIDRatioBased(1.0 / float64(rule.SampleRate)) + } + ruleSamplers = append(ruleSamplers, sampler) + } + + return &OvermindSampler{ + rules: rules, + ruleSamplers: ruleSamplers, + } +} + +// UserAgentMatcher returns a function that matches specific user agents +func UserAgentMatcher(userAgents ...string) func(sdktrace.SamplingParameters) bool { + return func(parameters sdktrace.SamplingParameters) bool { + for _, attr := range parameters.Attributes { + if (attr.Key == "http.user_agent" || attr.Key == "user_agent.original") && + slices.Contains(userAgents, attr.Value.AsString()) { + return true + } + } + return false + } +} + +// SpanNameMatcher returns a function that matches specific span names +func SpanNameMatcher(spanNames ...string) func(sdktrace.SamplingParameters) bool { + return func(parameters sdktrace.SamplingParameters) bool { + return slices.Contains(spanNames, parameters.Name) + } +} + +// ShouldSample evaluates rules in order and returns the first matching decision +func (o *OvermindSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult { + for i, rule := range o.rules { + if rule.ShouldSample(parameters) { + // Use the pre-allocated sampler for this rule + result := o.ruleSamplers[i].ShouldSample(parameters) + if result.Decision == sdktrace.RecordAndSample { + result.Attributes = append(result.Attributes, + attribute.Int("SampleRate", rule.SampleRate)) + } + return result + } + } + + // Default to AlwaysSample if no rules match + return sdktrace.AlwaysSample().ShouldSample(parameters) +} + +// Description returns information describing the Sampler +func (o *OvermindSampler) Description() string { + return "Unified Overmind sampler combining multiple sampling strategies" +} + +// Version returns the version baked into the binary at build time. +func Version() string { + return version +} + +// HTTPClient returns an HTTP client with OpenTelemetry instrumentation. +// This replaces the deprecated otelhttp.DefaultClient and should be used +// throughout the codebase for HTTP requests that need tracing. +func HTTPClient() *http.Client { + return &http.Client{ + Transport: otelhttp.NewTransport(http.DefaultTransport), + } +} diff --git a/go/tracing/main_test.go b/go/tracing/main_test.go new file mode 100644 index 00000000..26bf36cf --- /dev/null +++ b/go/tracing/main_test.go @@ -0,0 +1,12 @@ +package tracing + +import ( + "testing" +) + +func TestTracingResource(t *testing.T) { + resource := tracingResource("test-component") + if resource == nil { + t.Error("Could not initialize tracing resource. Check the log!") + } +} diff --git a/go/tracing/memory.go b/go/tracing/memory.go new file mode 100644 index 00000000..22123307 --- /dev/null +++ b/go/tracing/memory.go @@ -0,0 +1,69 @@ +package tracing + +import ( + "runtime" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// safeUint64ToInt64 safely converts uint64 to int64 for OpenTelemetry attributes +// Returns int64 max value if the uint64 exceeds int64 maximum to prevent overflow +func safeUint64ToInt64(val uint64) int64 { + const maxInt64 = 9223372036854775807 // 2^63 - 1 + if val > maxInt64 { // Check if val exceeds int64 max + return maxInt64 // Return int64 max value + } + return int64(val) +} + +// MemoryStats represents memory statistics at a point in time, converted to int64 for safe use +type MemoryStats struct { + Alloc int64 // bytes allocated and not yet freed + HeapAlloc int64 // bytes allocated and not yet freed (same as Alloc above but specifically for heap objects) + Sys int64 // total bytes of memory obtained from the OS + NumGC int64 // number of completed GC cycles + PauseTotal int64 // cumulative nanoseconds in GC stop-the-world pauses +} + +// ReadMemoryStats captures current memory statistics and converts them to int64 +func ReadMemoryStats() MemoryStats { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + return MemoryStats{ + Alloc: safeUint64ToInt64(memStats.Alloc), + HeapAlloc: safeUint64ToInt64(memStats.HeapAlloc), + Sys: safeUint64ToInt64(memStats.Sys), + NumGC: int64(memStats.NumGC), + PauseTotal: safeUint64ToInt64(memStats.PauseTotalNs), + } +} + +// SetMemoryAttributes sets memory-related attributes on a span with the given prefix +func SetMemoryAttributes(span trace.Span, prefix string, memStats MemoryStats) { + span.SetAttributes( + attribute.Int64(prefix+".memoryBytes", memStats.Alloc), + attribute.Int64(prefix+".memoryHeapBytes", memStats.HeapAlloc), + attribute.Int64(prefix+".memorySysBytes", memStats.Sys), + attribute.Int64(prefix+".memoryNumGC", memStats.NumGC), + attribute.Int64(prefix+".memoryPauseTotalNs", memStats.PauseTotal), + ) +} + +// SetMemoryDeltaAttributes sets memory delta attributes on a span with the given prefix +// It calculates the difference between before and after memory stats +func SetMemoryDeltaAttributes(span trace.Span, prefix string, before, after MemoryStats) { + deltaAlloc := after.Alloc - before.Alloc + deltaHeapAlloc := after.HeapAlloc - before.HeapAlloc + deltaSys := after.Sys - before.Sys + deltaNumGC := after.NumGC - before.NumGC + deltaPauseTotal := after.PauseTotal - before.PauseTotal + + span.SetAttributes( + attribute.Int64(prefix+".memoryDeltaBytes", deltaAlloc), + attribute.Int64(prefix+".memoryDeltaHeapBytes", deltaHeapAlloc), + attribute.Int64(prefix+".memoryDeltaSysBytes", deltaSys), + attribute.Int64(prefix+".memoryDeltaNumGC", deltaNumGC), + attribute.Int64(prefix+".memoryDeltaPauseTotalNs", deltaPauseTotal), + ) +} diff --git a/go/tracing/memory_test.go b/go/tracing/memory_test.go new file mode 100644 index 00000000..4624548d --- /dev/null +++ b/go/tracing/memory_test.go @@ -0,0 +1,75 @@ +package tracing + +import ( + "testing" +) + +func TestSafeUint64ToInt64(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input uint64 + expected int64 + }{ + { + name: "small value", + input: 1000, + expected: 1000, + }, + { + name: "int64 max value", + input: 9223372036854775807, // 2^63 - 1 + expected: 9223372036854775807, + }, + { + name: "int64 max + 1", + input: 9223372036854775808, // 2^63 + expected: 9223372036854775807, // Should be clamped to int64 max + }, + { + name: "very large value", + input: 18446744073709551615, // uint64 max + expected: 9223372036854775807, // Should be clamped to int64 max + }, + } + + for _, tt := range tests { + test := tt // capture loop variable + t.Run(test.name, func(t *testing.T) { + t.Parallel() + result := safeUint64ToInt64(test.input) + if result != test.expected { + t.Errorf("safeUint64ToInt64(%d) = %d, expected %d", test.input, result, test.expected) + } + }) + } +} + +func TestReadMemoryStats(t *testing.T) { + t.Parallel() + + stats := ReadMemoryStats() + + // Basic sanity checks - these values should be reasonable + if stats.Alloc <= 0 { + t.Errorf("Alloc should be greater than 0, got %d", stats.Alloc) + } + if stats.HeapAlloc <= 0 { + t.Errorf("HeapAlloc should be greater than 0, got %d", stats.HeapAlloc) + } + if stats.Sys <= 0 { + t.Errorf("Sys should be greater than 0, got %d", stats.Sys) + } + + // Verify that values are within int64 range (they should be since we convert them) + if stats.Alloc < 0 { + t.Errorf("Alloc should not be negative, got %d", stats.Alloc) + } + if stats.HeapAlloc < 0 { + t.Errorf("HeapAlloc should not be negative, got %d", stats.HeapAlloc) + } + if stats.Sys < 0 { + t.Errorf("Sys should not be negative, got %d", stats.Sys) + } +} diff --git a/k8s-source/adapters/clusterrole.go b/k8s-source/adapters/clusterrole.go index 365e319a..c957f2e6 100644 --- a/k8s-source/adapters/clusterrole.go +++ b/k8s-source/adapters/clusterrole.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/clusterrole_test.go b/k8s-source/adapters/clusterrole_test.go index 9bd5982e..9651f356 100644 --- a/k8s-source/adapters/clusterrole_test.go +++ b/k8s-source/adapters/clusterrole_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var clusterRoleYAML = ` diff --git a/k8s-source/adapters/clusterrolebinding.go b/k8s-source/adapters/clusterrolebinding.go index 07b38cd4..af61c45a 100644 --- a/k8s-source/adapters/clusterrolebinding.go +++ b/k8s-source/adapters/clusterrolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/clusterrolebinding_test.go b/k8s-source/adapters/clusterrolebinding_test.go index 32e28854..e2b2c4aa 100644 --- a/k8s-source/adapters/clusterrolebinding_test.go +++ b/k8s-source/adapters/clusterrolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var clusterRoleBindingYAML = ` diff --git a/k8s-source/adapters/configmap.go b/k8s-source/adapters/configmap.go index d43efd41..1cb7928e 100644 --- a/k8s-source/adapters/configmap.go +++ b/k8s-source/adapters/configmap.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/configmap_test.go b/k8s-source/adapters/configmap_test.go index b056f668..a5c256cc 100644 --- a/k8s-source/adapters/configmap_test.go +++ b/k8s-source/adapters/configmap_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var configMapYAML = ` diff --git a/k8s-source/adapters/cronjob.go b/k8s-source/adapters/cronjob.go index 9fef5f7d..21c9bea3 100644 --- a/k8s-source/adapters/cronjob.go +++ b/k8s-source/adapters/cronjob.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/batch/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/cronjob_test.go b/k8s-source/adapters/cronjob_test.go index 8d35b85e..b78fd725 100644 --- a/k8s-source/adapters/cronjob_test.go +++ b/k8s-source/adapters/cronjob_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var cronJobYAML = ` diff --git a/k8s-source/adapters/daemonset.go b/k8s-source/adapters/daemonset.go index be512de0..3ed80a30 100644 --- a/k8s-source/adapters/daemonset.go +++ b/k8s-source/adapters/daemonset.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/daemonset_test.go b/k8s-source/adapters/daemonset_test.go index b923b4e2..d4e4ef61 100644 --- a/k8s-source/adapters/daemonset_test.go +++ b/k8s-source/adapters/daemonset_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var daemonSetYAML = ` diff --git a/k8s-source/adapters/deployment.go b/k8s-source/adapters/deployment.go index eb57734c..ddfd02e7 100644 --- a/k8s-source/adapters/deployment.go +++ b/k8s-source/adapters/deployment.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/deployment_test.go b/k8s-source/adapters/deployment_test.go index 60f3b615..f435a9bd 100644 --- a/k8s-source/adapters/deployment_test.go +++ b/k8s-source/adapters/deployment_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var deploymentYAML = ` diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index b98fe2c9..a1facc2f 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/endpoints_test.go b/k8s-source/adapters/endpoints_test.go index f6f83b07..732e1b9f 100644 --- a/k8s-source/adapters/endpoints_test.go +++ b/k8s-source/adapters/endpoints_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var endpointsYAML = ` diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index d9d096d4..38d32be8 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -5,9 +5,9 @@ import ( v1 "k8s.io/api/discovery/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/endpointslice_test.go b/k8s-source/adapters/endpointslice_test.go index 238fcc42..e1d99776 100644 --- a/k8s-source/adapters/endpointslice_test.go +++ b/k8s-source/adapters/endpointslice_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var endpointSliceYAML = ` diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index c6e2e78d..5d284787 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" corev1 "k8s.io/api/core/v1" k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/k8s-source/adapters/generic_source_test.go b/k8s-source/adapters/generic_source_test.go index 0cc9e342..a688efc9 100644 --- a/k8s-source/adapters/generic_source_test.go +++ b/k8s-source/adapters/generic_source_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/google/uuid" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" diff --git a/k8s-source/adapters/horizontalpodautoscaler.go b/k8s-source/adapters/horizontalpodautoscaler.go index 1ef1d84d..654b9541 100644 --- a/k8s-source/adapters/horizontalpodautoscaler.go +++ b/k8s-source/adapters/horizontalpodautoscaler.go @@ -3,9 +3,9 @@ package adapters import ( v2 "k8s.io/api/autoscaling/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/horizontalpodautoscaler_test.go b/k8s-source/adapters/horizontalpodautoscaler_test.go index ea1227b7..e5f87785 100644 --- a/k8s-source/adapters/horizontalpodautoscaler_test.go +++ b/k8s-source/adapters/horizontalpodautoscaler_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var horizontalPodAutoscalerYAML = ` diff --git a/k8s-source/adapters/ingress.go b/k8s-source/adapters/ingress.go index 23eda968..c0cc4a7d 100644 --- a/k8s-source/adapters/ingress.go +++ b/k8s-source/adapters/ingress.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/ingress_test.go b/k8s-source/adapters/ingress_test.go index f6933961..f3c8990c 100644 --- a/k8s-source/adapters/ingress_test.go +++ b/k8s-source/adapters/ingress_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var ingressYAML = ` diff --git a/k8s-source/adapters/job.go b/k8s-source/adapters/job.go index 714fc136..1dab45e4 100644 --- a/k8s-source/adapters/job.go +++ b/k8s-source/adapters/job.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/batch/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/job_test.go b/k8s-source/adapters/job_test.go index 91f9888a..8cc9cb0a 100644 --- a/k8s-source/adapters/job_test.go +++ b/k8s-source/adapters/job_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var jobYAML = ` diff --git a/k8s-source/adapters/limitrange.go b/k8s-source/adapters/limitrange.go index 5888708e..bf402bde 100644 --- a/k8s-source/adapters/limitrange.go +++ b/k8s-source/adapters/limitrange.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/limitrange_test.go b/k8s-source/adapters/limitrange_test.go index 54386246..b31120b4 100644 --- a/k8s-source/adapters/limitrange_test.go +++ b/k8s-source/adapters/limitrange_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var limitRangeYAML = ` diff --git a/k8s-source/adapters/main.go b/k8s-source/adapters/main.go index ce59869f..a9987f5b 100644 --- a/k8s-source/adapters/main.go +++ b/k8s-source/adapters/main.go @@ -1,8 +1,8 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/networkpolicy.go b/k8s-source/adapters/networkpolicy.go index eb7df6ae..85de6a22 100644 --- a/k8s-source/adapters/networkpolicy.go +++ b/k8s-source/adapters/networkpolicy.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/networking/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/networkpolicy_test.go b/k8s-source/adapters/networkpolicy_test.go index e2838848..a24ef700 100644 --- a/k8s-source/adapters/networkpolicy_test.go +++ b/k8s-source/adapters/networkpolicy_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var NetworkPolicyYAML = ` diff --git a/k8s-source/adapters/node.go b/k8s-source/adapters/node.go index a8f6a23d..7186496f 100644 --- a/k8s-source/adapters/node.go +++ b/k8s-source/adapters/node.go @@ -3,9 +3,9 @@ package adapters import ( "strings" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/node_test.go b/k8s-source/adapters/node_test.go index f0453ef1..ffd49f7a 100644 --- a/k8s-source/adapters/node_test.go +++ b/k8s-source/adapters/node_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNodeAdapter(t *testing.T) { diff --git a/k8s-source/adapters/persistentvolume.go b/k8s-source/adapters/persistentvolume.go index bcc377bd..561f4025 100644 --- a/k8s-source/adapters/persistentvolume.go +++ b/k8s-source/adapters/persistentvolume.go @@ -3,9 +3,9 @@ package adapters import ( "regexp" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/persistentvolume_test.go b/k8s-source/adapters/persistentvolume_test.go index f9747099..d154860b 100644 --- a/k8s-source/adapters/persistentvolume_test.go +++ b/k8s-source/adapters/persistentvolume_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var persistentVolumeYAML = ` diff --git a/k8s-source/adapters/persistentvolumeclaim.go b/k8s-source/adapters/persistentvolumeclaim.go index 684c41ee..65d655ae 100644 --- a/k8s-source/adapters/persistentvolumeclaim.go +++ b/k8s-source/adapters/persistentvolumeclaim.go @@ -3,9 +3,9 @@ package adapters import ( "errors" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/persistentvolumeclaim_test.go b/k8s-source/adapters/persistentvolumeclaim_test.go index 6f5f62ad..698b1ebb 100644 --- a/k8s-source/adapters/persistentvolumeclaim_test.go +++ b/k8s-source/adapters/persistentvolumeclaim_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var persistentVolumeClaimYAML = ` diff --git a/k8s-source/adapters/poddisruptionbudget.go b/k8s-source/adapters/poddisruptionbudget.go index dc2e48fb..2ec34256 100644 --- a/k8s-source/adapters/poddisruptionbudget.go +++ b/k8s-source/adapters/poddisruptionbudget.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/policy/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/poddisruptionbudget_test.go b/k8s-source/adapters/poddisruptionbudget_test.go index e18e3d22..93e2bc38 100644 --- a/k8s-source/adapters/poddisruptionbudget_test.go +++ b/k8s-source/adapters/poddisruptionbudget_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var PodDisruptionBudgetYAML = ` diff --git a/k8s-source/adapters/pods.go b/k8s-source/adapters/pods.go index 70d451df..45896aa4 100644 --- a/k8s-source/adapters/pods.go +++ b/k8s-source/adapters/pods.go @@ -5,9 +5,9 @@ import ( "slices" "time" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/pods_test.go b/k8s-source/adapters/pods_test.go index e1644443..d229e928 100644 --- a/k8s-source/adapters/pods_test.go +++ b/k8s-source/adapters/pods_test.go @@ -6,8 +6,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" ) diff --git a/k8s-source/adapters/priorityclass.go b/k8s-source/adapters/priorityclass.go index 8258b1a1..f3274ac1 100644 --- a/k8s-source/adapters/priorityclass.go +++ b/k8s-source/adapters/priorityclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/scheduling/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/priorityclass_test.go b/k8s-source/adapters/priorityclass_test.go index f9d9c33d..adf96207 100644 --- a/k8s-source/adapters/priorityclass_test.go +++ b/k8s-source/adapters/priorityclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var priorityClassYAML = ` diff --git a/k8s-source/adapters/replicaset.go b/k8s-source/adapters/replicaset.go index e32c90e3..a4f96ad4 100644 --- a/k8s-source/adapters/replicaset.go +++ b/k8s-source/adapters/replicaset.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/apps/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/replicaset_test.go b/k8s-source/adapters/replicaset_test.go index 0880eff7..69adb2fa 100644 --- a/k8s-source/adapters/replicaset_test.go +++ b/k8s-source/adapters/replicaset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var replicaSetYAML = ` diff --git a/k8s-source/adapters/replicationcontroller.go b/k8s-source/adapters/replicationcontroller.go index 908d0ddc..d30aa2c4 100644 --- a/k8s-source/adapters/replicationcontroller.go +++ b/k8s-source/adapters/replicationcontroller.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/replicationcontroller_test.go b/k8s-source/adapters/replicationcontroller_test.go index d55a7642..b8357fe8 100644 --- a/k8s-source/adapters/replicationcontroller_test.go +++ b/k8s-source/adapters/replicationcontroller_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var replicationControllerYAML = ` diff --git a/k8s-source/adapters/resourcequota.go b/k8s-source/adapters/resourcequota.go index 34f5e74d..ca71086e 100644 --- a/k8s-source/adapters/resourcequota.go +++ b/k8s-source/adapters/resourcequota.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/resourcequota_test.go b/k8s-source/adapters/resourcequota_test.go index eec11486..2d52cb2a 100644 --- a/k8s-source/adapters/resourcequota_test.go +++ b/k8s-source/adapters/resourcequota_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var resourceQuotaYAML = ` diff --git a/k8s-source/adapters/role.go b/k8s-source/adapters/role.go index fcf6c49f..cb699a0c 100644 --- a/k8s-source/adapters/role.go +++ b/k8s-source/adapters/role.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/rbac/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/role_test.go b/k8s-source/adapters/role_test.go index a84d5d97..9222f2f7 100644 --- a/k8s-source/adapters/role_test.go +++ b/k8s-source/adapters/role_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var RoleYAML = ` diff --git a/k8s-source/adapters/rolebinding.go b/k8s-source/adapters/rolebinding.go index b3c85c39..a030abdf 100644 --- a/k8s-source/adapters/rolebinding.go +++ b/k8s-source/adapters/rolebinding.go @@ -3,9 +3,9 @@ package adapters import ( v1 "k8s.io/api/rbac/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/rolebinding_test.go b/k8s-source/adapters/rolebinding_test.go index 1bb33c21..f6e63e66 100644 --- a/k8s-source/adapters/rolebinding_test.go +++ b/k8s-source/adapters/rolebinding_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var roleBindingYAML = ` diff --git a/k8s-source/adapters/secret.go b/k8s-source/adapters/secret.go index 57907195..24e0538d 100644 --- a/k8s-source/adapters/secret.go +++ b/k8s-source/adapters/secret.go @@ -3,9 +3,9 @@ package adapters import ( "crypto/sha512" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/secret_test.go b/k8s-source/adapters/secret_test.go index 5aa1a5ef..ba813cce 100644 --- a/k8s-source/adapters/secret_test.go +++ b/k8s-source/adapters/secret_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var secretYAML = ` diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index c080dff9..8d24f928 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/service_test.go b/k8s-source/adapters/service_test.go index 961523e2..d2fa89e1 100644 --- a/k8s-source/adapters/service_test.go +++ b/k8s-source/adapters/service_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var serviceYAML = ` diff --git a/k8s-source/adapters/serviceaccount.go b/k8s-source/adapters/serviceaccount.go index cad46207..5d202075 100644 --- a/k8s-source/adapters/serviceaccount.go +++ b/k8s-source/adapters/serviceaccount.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/serviceaccount_test.go b/k8s-source/adapters/serviceaccount_test.go index 0e0f2a81..4d2353cc 100644 --- a/k8s-source/adapters/serviceaccount_test.go +++ b/k8s-source/adapters/serviceaccount_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var serviceAccountYAML = ` diff --git a/k8s-source/adapters/shared_util.go b/k8s-source/adapters/shared_util.go index 6f59ea7c..8c0a46aa 100644 --- a/k8s-source/adapters/shared_util.go +++ b/k8s-source/adapters/shared_util.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/k8s-source/adapters/statefulset.go b/k8s-source/adapters/statefulset.go index 674ee3b8..a11fd48e 100644 --- a/k8s-source/adapters/statefulset.go +++ b/k8s-source/adapters/statefulset.go @@ -4,9 +4,9 @@ import ( v1 "k8s.io/api/apps/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/statefulset_test.go b/k8s-source/adapters/statefulset_test.go index fd68485f..fa6c9acc 100644 --- a/k8s-source/adapters/statefulset_test.go +++ b/k8s-source/adapters/statefulset_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var statefulSetYAML = ` diff --git a/k8s-source/adapters/storageclass.go b/k8s-source/adapters/storageclass.go index b440ac85..3c0f2d0c 100644 --- a/k8s-source/adapters/storageclass.go +++ b/k8s-source/adapters/storageclass.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" diff --git a/k8s-source/adapters/storageclass_test.go b/k8s-source/adapters/storageclass_test.go index 011408f8..50a5aaa8 100644 --- a/k8s-source/adapters/storageclass_test.go +++ b/k8s-source/adapters/storageclass_test.go @@ -3,7 +3,7 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) var storageClassYAML = ` diff --git a/k8s-source/adapters/volumeattachment.go b/k8s-source/adapters/volumeattachment.go index f59a11d3..7b8e9625 100644 --- a/k8s-source/adapters/volumeattachment.go +++ b/k8s-source/adapters/volumeattachment.go @@ -1,9 +1,9 @@ package adapters import ( - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" v1 "k8s.io/api/storage/v1" "k8s.io/client-go/kubernetes" ) diff --git a/k8s-source/adapters/volumeattachment_test.go b/k8s-source/adapters/volumeattachment_test.go index bec193c0..b835753c 100644 --- a/k8s-source/adapters/volumeattachment_test.go +++ b/k8s-source/adapters/volumeattachment_test.go @@ -3,8 +3,8 @@ package adapters import ( "testing" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) var volumeAttachmentYAML = ` diff --git a/k8s-source/build/package/Dockerfile b/k8s-source/build/package/Dockerfile index 5f013774..c9f3c6a8 100644 --- a/k8s-source/build/package/Dockerfile +++ b/k8s-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source k8s-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/k8s-source/cmd/root.go b/k8s-source/cmd/root.go index 1f4dcbf9..d6385bc9 100644 --- a/k8s-source/cmd/root.go +++ b/k8s-source/cmd/root.go @@ -16,12 +16,12 @@ import ( "time" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/k8s-source/adapters" "github.com/overmindtech/cli/k8s-source/proc" - "github.com/overmindtech/workspace/logging" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/tracing" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/sources/aws/apigateway-api-key.go b/sources/aws/apigateway-api-key.go index 185ebc1e..eb6d6bfe 100644 --- a/sources/aws/apigateway-api-key.go +++ b/sources/aws/apigateway-api-key.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/aws/apigateway-stage.go b/sources/aws/apigateway-stage.go index 2a0b34e8..ef582d8b 100644 --- a/sources/aws/apigateway-stage.go +++ b/sources/aws/apigateway-stage.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/overmindtech/cli/aws-source/adapters" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" awsshared "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/aws/base.go b/sources/aws/base.go index 1c369e72..1db67052 100644 --- a/sources/aws/base.go +++ b/sources/aws/base.go @@ -3,7 +3,7 @@ package aws import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/aws/errors.go b/sources/aws/errors.go index 7c283261..2a46d4a2 100644 --- a/sources/aws/errors.go +++ b/sources/aws/errors.go @@ -6,7 +6,7 @@ import ( awsHttp "github.com/aws/smithy-go/transport/http" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // queryError takes an error and returns a sdp.QueryError. diff --git a/sources/aws/validation_test.go b/sources/aws/validation_test.go index c5a40f11..f79390d2 100644 --- a/sources/aws/validation_test.go +++ b/sources/aws/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/azure/build/package/Dockerfile b/sources/azure/build/package/Dockerfile index d53e7e4a..3b607dc5 100644 --- a/sources/azure/build/package/Dockerfile +++ b/sources/azure/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source sources/azure/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/azure/cmd/root.go b/sources/azure/cmd/root.go index df4324cf..ae641c37 100644 --- a/sources/azure/cmd/root.go +++ b/sources/azure/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/logging" + "github.com/overmindtech/cli/go/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/sources/azure/proc" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/tracing" ) var cfgFile string diff --git a/sources/azure/integration-tests/authorization-role-assignment_test.go b/sources/azure/integration-tests/authorization-role-assignment_test.go index 5982988f..60ea32d6 100644 --- a/sources/azure/integration-tests/authorization-role-assignment_test.go +++ b/sources/azure/integration-tests/authorization-role-assignment_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index c267b75f..7e62209d 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -18,9 +18,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index 3e7cb904..5049a01c 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go index c71097fe..7b4e76c7 100644 --- a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go +++ b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go @@ -14,9 +14,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-dedicated-host-group_test.go b/sources/azure/integration-tests/compute-dedicated-host-group_test.go index eb012459..adb0edfa 100644 --- a/sources/azure/integration-tests/compute-dedicated-host-group_test.go +++ b/sources/azure/integration-tests/compute-dedicated-host-group_test.go @@ -14,9 +14,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-disk-access_test.go b/sources/azure/integration-tests/compute-disk-access_test.go index 5c14aecc..3f245afd 100644 --- a/sources/azure/integration-tests/compute-disk-access_test.go +++ b/sources/azure/integration-tests/compute-disk-access_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" 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 ddb247fc..5b104edb 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-disk_test.go b/sources/azure/integration-tests/compute-disk_test.go index c068672d..6c7cbb5b 100644 --- a/sources/azure/integration-tests/compute-disk_test.go +++ b/sources/azure/integration-tests/compute-disk_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-gallery-application-version_test.go b/sources/azure/integration-tests/compute-gallery-application-version_test.go index 88d2b400..eeb4b84b 100644 --- a/sources/azure/integration-tests/compute-gallery-application-version_test.go +++ b/sources/azure/integration-tests/compute-gallery-application-version_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-image_test.go b/sources/azure/integration-tests/compute-image_test.go index 01de67f3..71af70f1 100644 --- a/sources/azure/integration-tests/compute-image_test.go +++ b/sources/azure/integration-tests/compute-image_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-proximity-placement-group_test.go b/sources/azure/integration-tests/compute-proximity-placement-group_test.go index 5c12a005..81606299 100644 --- a/sources/azure/integration-tests/compute-proximity-placement-group_test.go +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" 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 9c4b3a5f..bc4809cd 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" 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 1d4267df..edbed38d 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" 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 6f111bef..b1c309e8 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index b0525875..d1707ff9 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -16,9 +16,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/dbforpostgresql-database_test.go b/sources/azure/integration-tests/dbforpostgresql-database_test.go index 85e65454..5cb4065e 100644 --- a/sources/azure/integration-tests/dbforpostgresql-database_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go index b6bc370a..fd7ff62c 100644 --- a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/documentdb-database-accounts_test.go b/sources/azure/integration-tests/documentdb-database-accounts_test.go index 5f9ef70b..6d59dbb4 100644 --- a/sources/azure/integration-tests/documentdb-database-accounts_test.go +++ b/sources/azure/integration-tests/documentdb-database-accounts_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-managed-hsm_test.go b/sources/azure/integration-tests/keyvault-managed-hsm_test.go index d3a9c7b1..f9a0e8b0 100644 --- a/sources/azure/integration-tests/keyvault-managed-hsm_test.go +++ b/sources/azure/integration-tests/keyvault-managed-hsm_test.go @@ -14,9 +14,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-secret_test.go b/sources/azure/integration-tests/keyvault-secret_test.go index 67433469..c74ad5a7 100644 --- a/sources/azure/integration-tests/keyvault-secret_test.go +++ b/sources/azure/integration-tests/keyvault-secret_test.go @@ -14,8 +14,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/keyvault-vault_test.go b/sources/azure/integration-tests/keyvault-vault_test.go index 17d866f1..187756c2 100644 --- a/sources/azure/integration-tests/keyvault-vault_test.go +++ b/sources/azure/integration-tests/keyvault-vault_test.go @@ -15,8 +15,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go index 2c5f556a..70374b50 100644 --- a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index 08e3ab92..fe729639 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index fb80fb14..aa11334f 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index 517dcaee..ca4c5d2f 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-network-security-group_test.go b/sources/azure/integration-tests/network-network-security-group_test.go index 0954a53d..17039a7c 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" 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 8b343f4d..2cda00ee 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index 9e0dae8f..19634e65 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index b08e1a70..fe12269c 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/network-zone_test.go b/sources/azure/integration-tests/network-zone_test.go index 525774d7..70b87a76 100644 --- a/sources/azure/integration-tests/network-zone_test.go +++ b/sources/azure/integration-tests/network-zone_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index 7b46799a..289ff425 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -17,9 +17,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/sql-server_test.go b/sources/azure/integration-tests/sql-server_test.go index e7fc3c70..48a965de 100644 --- a/sources/azure/integration-tests/sql-server_test.go +++ b/sources/azure/integration-tests/sql-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-account_test.go b/sources/azure/integration-tests/storage-account_test.go index 37641751..d9501f1c 100644 --- a/sources/azure/integration-tests/storage-account_test.go +++ b/sources/azure/integration-tests/storage-account_test.go @@ -8,9 +8,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-blob-container_test.go b/sources/azure/integration-tests/storage-blob-container_test.go index 755c4f69..0ff9b17d 100644 --- a/sources/azure/integration-tests/storage-blob-container_test.go +++ b/sources/azure/integration-tests/storage-blob-container_test.go @@ -17,8 +17,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-fileshare_test.go b/sources/azure/integration-tests/storage-fileshare_test.go index 9b503958..d9a5a992 100644 --- a/sources/azure/integration-tests/storage-fileshare_test.go +++ b/sources/azure/integration-tests/storage-fileshare_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-queues_test.go b/sources/azure/integration-tests/storage-queues_test.go index ee1d4697..6a97d2a6 100644 --- a/sources/azure/integration-tests/storage-queues_test.go +++ b/sources/azure/integration-tests/storage-queues_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/integration-tests/storage-table_test.go b/sources/azure/integration-tests/storage-table_test.go index 292be150..52a6b3a1 100644 --- a/sources/azure/integration-tests/storage-table_test.go +++ b/sources/azure/integration-tests/storage-table_test.go @@ -13,8 +13,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 386c10b2..7cbe33ae 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -19,8 +19,8 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index a20feaf1..2c5a956f 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -6,13 +6,13 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var AuthorizationRoleAssignmentLookupByName = shared.NewItemTypeLookup("name", azureshared.AuthorizationRoleAssignment) diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index d94a0565..6f719de0 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index 27af9a19..ddc8aceb 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var BatchAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchAccount) diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index adb32891..4c3622e4 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-availability-set.go b/sources/azure/manual/compute-availability-set.go index 6c0dca2c..f9d36dee 100644 --- a/sources/azure/manual/compute-availability-set.go +++ b/sources/azure/manual/compute-availability-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-availability-set_test.go b/sources/azure/manual/compute-availability-set_test.go index b773940f..f04d8f5c 100644 --- a/sources/azure/manual/compute-availability-set_test.go +++ b/sources/azure/manual/compute-availability-set_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-capacity-reservation-group.go b/sources/azure/manual/compute-capacity-reservation-group.go index e813c7cf..21e221a8 100644 --- a/sources/azure/manual/compute-capacity-reservation-group.go +++ b/sources/azure/manual/compute-capacity-reservation-group.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go index 68a92376..46afaff2 100644 --- a/sources/azure/manual/compute-capacity-reservation-group_test.go +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-dedicated-host-group.go b/sources/azure/manual/compute-dedicated-host-group.go index d4d331db..4e8f1b6d 100644 --- a/sources/azure/manual/compute-dedicated-host-group.go +++ b/sources/azure/manual/compute-dedicated-host-group.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-dedicated-host-group_test.go b/sources/azure/manual/compute-dedicated-host-group_test.go index 6f760ac0..a51042b0 100644 --- a/sources/azure/manual/compute-dedicated-host-group_test.go +++ b/sources/azure/manual/compute-dedicated-host-group_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-disk-access.go b/sources/azure/manual/compute-disk-access.go index 4e7ba8dc..f11f811e 100644 --- a/sources/azure/manual/compute-disk-access.go +++ b/sources/azure/manual/compute-disk-access.go @@ -5,9 +5,9 @@ import ( "errors" armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - discovery "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - sdpcache "github.com/overmindtech/workspace/sdpcache" + discovery "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + sdpcache "github.com/overmindtech/cli/go/sdpcache" sources "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-disk-access_test.go b/sources/azure/manual/compute-disk-access_test.go index bdec9e20..213f68ed 100644 --- a/sources/azure/manual/compute-disk-access_test.go +++ b/sources/azure/manual/compute-disk-access_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-disk-encryption-set.go b/sources/azure/manual/compute-disk-encryption-set.go index 8c5d91df..5beb9a7b 100644 --- a/sources/azure/manual/compute-disk-encryption-set.go +++ b/sources/azure/manual/compute-disk-encryption-set.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-disk-encryption-set_test.go b/sources/azure/manual/compute-disk-encryption-set_test.go index e5f75451..e12ee497 100644 --- a/sources/azure/manual/compute-disk-encryption-set_test.go +++ b/sources/azure/manual/compute-disk-encryption-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-disk.go b/sources/azure/manual/compute-disk.go index d6fd975e..fc9413e2 100644 --- a/sources/azure/manual/compute-disk.go +++ b/sources/azure/manual/compute-disk.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index 0e064480..eff2b580 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index 07c72fbb..3aa5812a 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go index fffb7bd5..28f1ad04 100644 --- a/sources/azure/manual/compute-gallery-application-version_test.go +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-image.go b/sources/azure/manual/compute-image.go index 8181f8af..d9102966 100644 --- a/sources/azure/manual/compute-image.go +++ b/sources/azure/manual/compute-image.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-image_test.go b/sources/azure/manual/compute-image_test.go index efa8d2d9..4d9b7035 100644 --- a/sources/azure/manual/compute-image_test.go +++ b/sources/azure/manual/compute-image_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go index a26589da..5d07ffee 100644 --- a/sources/azure/manual/compute-proximity-placement-group.go +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -5,13 +5,13 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var ComputeProximityPlacementGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeProximityPlacementGroup) diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go index b3f14b63..a2663161 100644 --- a/sources/azure/manual/compute-proximity-placement-group_test.go +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-virtual-machine-extension.go b/sources/azure/manual/compute-virtual-machine-extension.go index 5856cc69..645a31e8 100644 --- a/sources/azure/manual/compute-virtual-machine-extension.go +++ b/sources/azure/manual/compute-virtual-machine-extension.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index da4ccbba..5727a131 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index a1cdde51..517b5664 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-run-command_test.go b/sources/azure/manual/compute-virtual-machine-run-command_test.go index dcd5929c..90d45c09 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/compute-virtual-machine-scale-set.go b/sources/azure/manual/compute-virtual-machine-scale-set.go index 7a9a7f56..3f6f003a 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine-scale-set_test.go b/sources/azure/manual/compute-virtual-machine-scale-set_test.go index d2b663a9..0495d589 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine.go b/sources/azure/manual/compute-virtual-machine.go index 67c7b8d7..b3907780 100644 --- a/sources/azure/manual/compute-virtual-machine.go +++ b/sources/azure/manual/compute-virtual-machine.go @@ -6,9 +6,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/compute-virtual-machine_test.go b/sources/azure/manual/compute-virtual-machine_test.go index c5dcd69c..3d8eb94a 100644 --- a/sources/azure/manual/compute-virtual-machine_test.go +++ b/sources/azure/manual/compute-virtual-machine_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-database.go b/sources/azure/manual/dbforpostgresql-database.go index a263362c..66962894 100644 --- a/sources/azure/manual/dbforpostgresql-database.go +++ b/sources/azure/manual/dbforpostgresql-database.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-database_test.go b/sources/azure/manual/dbforpostgresql-database_test.go index fcd04b67..322cc3d8 100644 --- a/sources/azure/manual/dbforpostgresql-database_test.go +++ b/sources/azure/manual/dbforpostgresql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/dbforpostgresql-flexible-server.go b/sources/azure/manual/dbforpostgresql-flexible-server.go index c116dc18..a74541e3 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index 4090c383..d21cd6b8 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/dns_links.go b/sources/azure/manual/dns_links.go index c03a1867..44824c1a 100644 --- a/sources/azure/manual/dns_links.go +++ b/sources/azure/manual/dns_links.go @@ -3,7 +3,7 @@ package manual import ( "net" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 5757c854..874d1818 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index 228a25a8..dfd3a1a2 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-managed-hsm.go b/sources/azure/manual/keyvault-managed-hsm.go index daeb232e..05308a3d 100644 --- a/sources/azure/manual/keyvault-managed-hsm.go +++ b/sources/azure/manual/keyvault-managed-hsm.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index 03eeb7e0..287ecdac 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index 88e758da..b9ef5f2d 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index b31cf531..a3201a34 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index d51fabfe..2f7f1a4c 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index c0cca2d0..1641d9a2 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index b892839d..5e6eb2ae 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -1,7 +1,7 @@ package manual import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/azure/manual/managedidentity-user-assigned-identity.go b/sources/azure/manual/managedidentity-user-assigned-identity.go index 66f85f67..11713958 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/managedidentity-user-assigned-identity_test.go b/sources/azure/manual/managedidentity-user-assigned-identity_test.go index dd7534e4..59670209 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index 49a2e28c..04d3fa0c 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index 1840ec20..da03b36a 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index 0c681b7d..804d8073 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -7,14 +7,14 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var NetworkLoadBalancerLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkLoadBalancer) diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index 70d16a3d..70ebbe63 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 5811ab1c..91e3bf79 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var NetworkNetworkInterfaceLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkInterface) diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index c0229cac..62cabf0c 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index f61dda88..949e87d4 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index c7a2d0dc..ff2e7bd5 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index 7775222f..fc8be72a 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index 3adf2b2a..c69f1680 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 10c569a6..0f55f8a1 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var NetworkRouteTableLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkRouteTable) diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 2d57c078..0c771346 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index 63c2c026..b20da836 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/sdpcache" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/go/discovery" ) var NetworkVirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index a7641002..6e849e89 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-zone.go b/sources/azure/manual/network-zone.go index fe28f5e2..a5d904ff 100644 --- a/sources/azure/manual/network-zone.go +++ b/sources/azure/manual/network-zone.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/network-zone_test.go b/sources/azure/manual/network-zone_test.go index 946db118..e75323bc 100644 --- a/sources/azure/manual/network-zone_test.go +++ b/sources/azure/manual/network-zone_test.go @@ -11,9 +11,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index 8a28e0e1..054523de 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index 1f9da171..dba27b27 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/sql-server.go b/sources/azure/manual/sql-server.go index 8d378720..8639384b 100644 --- a/sources/azure/manual/sql-server.go +++ b/sources/azure/manual/sql-server.go @@ -5,9 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index 7457b34a..3f698a00 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index 3737227a..7c076319 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -5,14 +5,14 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" ) var StorageAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.StorageAccount) diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 87935781..00e66b16 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/manual" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index 298e4111..ec50794d 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index 10492bd7..c4b1358c 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index 1748cc35..2ab24c9d 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index 2247cd2b..a9ec705a 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index 406ca758..47d673ee 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -4,7 +4,7 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index 920a966a..ca61a425 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index 0fef764d..921e63db 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index b172f224..177a8ace 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -9,9 +9,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" "github.com/overmindtech/cli/sources/azure/manual" diff --git a/sources/azure/proc/proc.go b/sources/azure/proc/proc.go index d4f0936a..2fddfca4 100644 --- a/sources/azure/proc/proc.go +++ b/sources/azure/proc/proc.go @@ -10,9 +10,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" // TODO: Uncomment when Azure dynamic adapters are implemented // "github.com/overmindtech/cli/sources/azure/dynamic" diff --git a/sources/azure/proc/proc_test.go b/sources/azure/proc/proc_test.go index afb278f5..ddcaf803 100644 --- a/sources/azure/proc/proc_test.go +++ b/sources/azure/proc/proc_test.go @@ -6,7 +6,7 @@ import ( // TODO: Uncomment when Azure dynamic adapters are implemented // _ "github.com/overmindtech/cli/sources/azure/dynamic" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" ) diff --git a/sources/azure/shared/adapter-meta.go b/sources/azure/shared/adapter-meta.go index 75b0c606..32012274 100644 --- a/sources/azure/shared/adapter-meta.go +++ b/sources/azure/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/base.go b/sources/azure/shared/base.go index 20253ba0..40e9605e 100644 --- a/sources/azure/shared/base.go +++ b/sources/azure/shared/base.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/azure/shared/errors.go b/sources/azure/shared/errors.go index 8019b62a..abc77b06 100644 --- a/sources/azure/shared/errors.go +++ b/sources/azure/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/azure/shared/scope.go b/sources/azure/shared/scope.go index 4adf5e0d..bed1d144 100644 --- a/sources/azure/shared/scope.go +++ b/sources/azure/shared/scope.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/base.go b/sources/example/base.go index 80fb6af1..2bbf09bd 100644 --- a/sources/example/base.go +++ b/sources/example/base.go @@ -3,7 +3,7 @@ package example import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/custom_searchable_listable.go b/sources/example/custom_searchable_listable.go index 0e9bbc32..bb118a7e 100644 --- a/sources/example/custom_searchable_listable.go +++ b/sources/example/custom_searchable_listable.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/example/errors.go b/sources/example/errors.go index b5756565..a83b7bc8 100644 --- a/sources/example/errors.go +++ b/sources/example/errors.go @@ -3,7 +3,7 @@ package example import ( "errors" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // queryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/example/metadata_test.go b/sources/example/metadata_test.go index 7f3a5cdc..db60d638 100644 --- a/sources/example/metadata_test.go +++ b/sources/example/metadata_test.go @@ -8,8 +8,8 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/example/shared" ) diff --git a/sources/example/standard_searchable_listable.go b/sources/example/standard_searchable_listable.go index 57a13033..9eaa051f 100644 --- a/sources/example/standard_searchable_listable.go +++ b/sources/example/standard_searchable_listable.go @@ -3,7 +3,7 @@ package example import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" exampleshared "github.com/overmindtech/cli/sources/example/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/example/standard_searchable_listable_test.go b/sources/example/standard_searchable_listable_test.go index 655f5e77..44c0c1a2 100644 --- a/sources/example/standard_searchable_listable_test.go +++ b/sources/example/standard_searchable_listable_test.go @@ -6,7 +6,7 @@ import ( "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/example" "github.com/overmindtech/cli/sources/example/mocks" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/example/validation_test.go b/sources/example/validation_test.go index fa8d6ed3..88ddcb42 100644 --- a/sources/example/validation_test.go +++ b/sources/example/validation_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" ) diff --git a/sources/gcp/build/package/Dockerfile b/sources/gcp/build/package/Dockerfile index a933bfeb..853a571a 100644 --- a/sources/gcp/build/package/Dockerfile +++ b/sources/gcp/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source sources/gcp/main.go FROM alpine:3.23 WORKDIR / diff --git a/sources/gcp/cmd/root.go b/sources/gcp/cmd/root.go index 0c6e7640..08ef2d42 100644 --- a/sources/gcp/cmd/root.go +++ b/sources/gcp/cmd/root.go @@ -9,15 +9,15 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/logging" + "github.com/overmindtech/cli/go/logging" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/overmindtech/workspace/discovery" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/sources/gcp/proc" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/tracing" ) var cfgFile string diff --git a/sources/gcp/dynamic/adapter-listable.go b/sources/gcp/dynamic/adapter-listable.go index b1dd5748..02c745d0 100644 --- a/sources/gcp/dynamic/adapter-listable.go +++ b/sources/gcp/dynamic/adapter-listable.go @@ -6,9 +6,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index fd2eb4aa..d439690e 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -7,9 +7,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapter-searchable.go b/sources/gcp/dynamic/adapter-searchable.go index 4cc059ad..ebebfb53 100644 --- a/sources/gcp/dynamic/adapter-searchable.go +++ b/sources/gcp/dynamic/adapter-searchable.go @@ -7,9 +7,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index 42d62f7d..f301d12a 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -8,9 +8,9 @@ import ( "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters.go b/sources/gcp/dynamic/adapters.go index 5e19af55..1011183a 100644 --- a/sources/gcp/dynamic/adapters.go +++ b/sources/gcp/dynamic/adapters.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go index 7c0a4f98..35d2b8e2 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go index 7c073437..52438dcc 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-batch-prediction-job_test.go @@ -22,9 +22,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" "google.golang.org/genproto/googleapis/rpc/status" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go index df3af6e8..d395c0ad 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go index fb440b92..ae50dcb7 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go index a74a09f8..9b86fb9d 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index c27daa77..df9170ce 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go index b1743718..67c3bb66 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go index 312f907b..d761c5af 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model-deployment-monitoring-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-model.go b/sources/gcp/dynamic/adapters/ai-platform-model.go index df7c8926..778fcb23 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index 61632c78..5034f43f 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/aiplatform/apiv1/aiplatformpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go index 747ecb25..37ac598b 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go index 85e04021..3e2cd358 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/aiplatform/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go index 18281532..855e4f1b 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go index 833ad530..3833732d 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-docker-image_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/artifactregistry/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/artifact-registry-repository.go b/sources/gcp/dynamic/adapters/artifact-registry-repository.go index 1d6d5c67..7a984451 100644 --- a/sources/gcp/dynamic/adapters/artifact-registry-repository.go +++ b/sources/gcp/dynamic/adapters/artifact-registry-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go index 5489a605..069d451b 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go index ab5c67fc..b35e51fe 100644 --- a/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go +++ b/sources/gcp/dynamic/adapters/big-query-data-transfer-transfer-config_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/bigquery/datatransfer/apiv1/datatransferpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go index d57cd5fe..6af259ed 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go index d966acc7..2cd25def 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-app-profile_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup.go b/sources/gcp/dynamic/adapters/big-table-admin-backup.go index f3eb2297..accf93b5 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go index 2eba1222..c1c48ec8 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go index b38a9f31..29fe249a 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go index a253104a..51eadc8f 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index 9de582ed..10657461 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go index 8ac43791..2e0fa848 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigtable/admin/apiv2/adminpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 9d8365b6..8194fe7e 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go index 4e5e2984..a8615272 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/bigtableadmin/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go index 167430c9..2c1808e0 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go index 7b325452..a94ee7d9 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudbilling/v1" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-build-build.go b/sources/gcp/dynamic/adapters/cloud-build-build.go index 94ec9101..80f67df8 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-build-build_test.go b/sources/gcp/dynamic/adapters/cloud-build-build_test.go index 3003bb38..ca64382a 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build_test.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/cloudbuild/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go index c4f619c3..20a2ec76 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go index cc4f6e2f..c542ead7 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/api/cloudresourcemanager/v3" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go index 00261328..32032c42 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go index 842e87f7..51fd89cb 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-key_test.go @@ -9,8 +9,8 @@ import ( resourcemanagerpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go index 54fadede..c08b60ad 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go index b5f88440..7ad034db 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-tag-value_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function.go b/sources/gcp/dynamic/adapters/cloudfunctions-function.go index 9ffa2e85..a0d4dfc9 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go index b4efc987..bd203dfd 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/functions/apiv2/functionspb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-accelerator-type.go b/sources/gcp/dynamic/adapters/compute-accelerator-type.go index b51dabf0..f9fe612f 100644 --- a/sources/gcp/dynamic/adapters/compute-accelerator-type.go +++ b/sources/gcp/dynamic/adapters/compute-accelerator-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-disk-type.go b/sources/gcp/dynamic/adapters/compute-disk-type.go index f9c0bd8c..3d255096 100644 --- a/sources/gcp/dynamic/adapters/compute-disk-type.go +++ b/sources/gcp/dynamic/adapters/compute-disk-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go index 043d7c4e..adf87ace 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go index bade13c7..be2726ec 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index 0b5256a1..22aeaffc 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-firewall_test.go b/sources/gcp/dynamic/adapters/compute-firewall_test.go index 689518e5..aaddcf5d 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall_test.go +++ b/sources/gcp/dynamic/adapters/compute-firewall_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-global-address.go b/sources/gcp/dynamic/adapters/compute-global-address.go index 62e22dc9..dade7ed2 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address.go +++ b/sources/gcp/dynamic/adapters/compute-global-address.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-global-address_test.go b/sources/gcp/dynamic/adapters/compute-global-address_test.go index ba95765b..7fccd453 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-address_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go index c00784ec..88f91838 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go index 17e80308..95cb69dd 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check.go b/sources/gcp/dynamic/adapters/compute-http-health-check.go index a45bcc0a..77146bb2 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go index 6325222f..a3134320 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -6,9 +6,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index 92040e41..592cfecf 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-instance-template_test.go b/sources/gcp/dynamic/adapters/compute-instance-template_test.go index 98b2fa68..4159b551 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template_test.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-license.go b/sources/gcp/dynamic/adapters/compute-license.go index 2025eaf3..da33d5dd 100644 --- a/sources/gcp/dynamic/adapters/compute-license.go +++ b/sources/gcp/dynamic/adapters/compute-license.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go index 69513568..a95f640b 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go index 1cd8c5b5..a4e723ff 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-network.go b/sources/gcp/dynamic/adapters/compute-network.go index 261553f8..753d9fa6 100644 --- a/sources/gcp/dynamic/adapters/compute-network.go +++ b/sources/gcp/dynamic/adapters/compute-network.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-network_test.go b/sources/gcp/dynamic/adapters/compute-network_test.go index 70ab7cd8..4968ad42 100644 --- a/sources/gcp/dynamic/adapters/compute-network_test.go +++ b/sources/gcp/dynamic/adapters/compute-network_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-project.go b/sources/gcp/dynamic/adapters/compute-project.go index 52c8b272..f4b9a0df 100644 --- a/sources/gcp/dynamic/adapters/compute-project.go +++ b/sources/gcp/dynamic/adapters/compute-project.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-project_test.go b/sources/gcp/dynamic/adapters/compute-project_test.go index b7f54ade..d475df6d 100644 --- a/sources/gcp/dynamic/adapters/compute-project_test.go +++ b/sources/gcp/dynamic/adapters/compute-project_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go index 26080368..a3788e69 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go index 195b7e7e..623ec276 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment.go b/sources/gcp/dynamic/adapters/compute-region-commitment.go index 75d6701e..c3775785 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go index 5a189545..a56ef8e8 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-resource-policy.go b/sources/gcp/dynamic/adapters/compute-resource-policy.go index 97370e4e..25799663 100644 --- a/sources/gcp/dynamic/adapters/compute-resource-policy.go +++ b/sources/gcp/dynamic/adapters/compute-resource-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index 9c7c79ae..3a90e854 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-route_test.go b/sources/gcp/dynamic/adapters/compute-route_test.go index 881d1cc1..79a90518 100644 --- a/sources/gcp/dynamic/adapters/compute-route_test.go +++ b/sources/gcp/dynamic/adapters/compute-route_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-router.go b/sources/gcp/dynamic/adapters/compute-router.go index ffb57ea7..da87dbbc 100644 --- a/sources/gcp/dynamic/adapters/compute-router.go +++ b/sources/gcp/dynamic/adapters/compute-router.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index 13fef494..1171e13d 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go index 5b97c6a5..cedebec6 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go index 217940bc..b73f49ca 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy.go b/sources/gcp/dynamic/adapters/compute-ssl-policy.go index eb130d4b..fcb0e8d4 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go index 99c3da7d..e0ee4f86 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-storage-pool.go b/sources/gcp/dynamic/adapters/compute-storage-pool.go index 5e50e787..474f44f9 100644 --- a/sources/gcp/dynamic/adapters/compute-storage-pool.go +++ b/sources/gcp/dynamic/adapters/compute-storage-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork.go b/sources/gcp/dynamic/adapters/compute-subnetwork.go index ab32e06e..64a1a803 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go index a3eff130..84643afc 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/compute/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go index 6c416fd6..20223743 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go index 0fd8c06d..2f2be523 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go index 558dadaa..fb4403c5 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go index ae3caafb..6354aa99 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-target-pool.go b/sources/gcp/dynamic/adapters/compute-target-pool.go index af83e0a7..0479c23c 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-target-pool_test.go b/sources/gcp/dynamic/adapters/compute-target-pool_test.go index a27c4b42..aa5fbb2e 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-url-map.go b/sources/gcp/dynamic/adapters/compute-url-map.go index ae5b6300..26325c9e 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map.go +++ b/sources/gcp/dynamic/adapters/compute-url-map.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-url-map_test.go b/sources/gcp/dynamic/adapters/compute-url-map_test.go index 7abb4516..c997f8bb 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map_test.go +++ b/sources/gcp/dynamic/adapters/compute-url-map_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go index 13450425..550fffdb 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go index f7342c49..984becbc 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go index ac4e9ed6..18a88c30 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go index 4902c9b5..4ea038f1 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/container-cluster.go b/sources/gcp/dynamic/adapters/container-cluster.go index f62112db..c637be9a 100644 --- a/sources/gcp/dynamic/adapters/container-cluster.go +++ b/sources/gcp/dynamic/adapters/container-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 79330046..649f5214 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/container-node-pool.go b/sources/gcp/dynamic/adapters/container-node-pool.go index b91f98a4..6028cc23 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool.go +++ b/sources/gcp/dynamic/adapters/container-node-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/container-node-pool_test.go b/sources/gcp/dynamic/adapters/container-node-pool_test.go index 2630d2f6..0b01b9c3 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool_test.go +++ b/sources/gcp/dynamic/adapters/container-node-pool_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/container/apiv1/containerpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataform-repository.go b/sources/gcp/dynamic/adapters/dataform-repository.go index 465f89b7..3f36e4ab 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository.go +++ b/sources/gcp/dynamic/adapters/dataform-repository.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/dataform-repository_test.go b/sources/gcp/dynamic/adapters/dataform-repository_test.go index 47011624..70518f10 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository_test.go +++ b/sources/gcp/dynamic/adapters/dataform-repository_test.go @@ -8,9 +8,9 @@ import ( dataform "google.golang.org/api/dataform/v1beta1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go index e342c527..7a02e7bd 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go index 907d17f8..fcafe96b 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go @@ -10,8 +10,8 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan.go b/sources/gcp/dynamic/adapters/dataplex-data-scan.go index cd16119b..71f0ba04 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go index af4552bf..8f67d5c2 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataplex/apiv1/dataplexpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group.go b/sources/gcp/dynamic/adapters/dataplex-entry-group.go index 1db80552..7f91ba66 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go index bab58b4c..a2b71164 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/dataplex/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go index facb5f79..0c6d99b2 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go index aea41fac..1b8c6348 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go @@ -9,8 +9,8 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" "google.golang.org/protobuf/types/known/durationpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster.go b/sources/gcp/dynamic/adapters/dataproc-cluster.go index b8e1bca8..2a073a63 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go index e4ba123a..a9daf0c6 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/dataproc/v2/apiv1/dataprocpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone.go b/sources/gcp/dynamic/adapters/dns-managed-zone.go index d05ec820..2ada968b 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go index 620da6c6..59bf5b46 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/dns/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact.go b/sources/gcp/dynamic/adapters/essential-contacts-contact.go index ac5a3929..e2aa3ccd 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go index 716b8ab1..1dd41c5c 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/essentialcontacts/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/eventarc-trigger.go b/sources/gcp/dynamic/adapters/eventarc-trigger.go index e4d78838..fe6ba4c5 100644 --- a/sources/gcp/dynamic/adapters/eventarc-trigger.go +++ b/sources/gcp/dynamic/adapters/eventarc-trigger.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/file-instance.go b/sources/gcp/dynamic/adapters/file-instance.go index 1b5fde1e..d4768491 100644 --- a/sources/gcp/dynamic/adapters/file-instance.go +++ b/sources/gcp/dynamic/adapters/file-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/file-instance_test.go b/sources/gcp/dynamic/adapters/file-instance_test.go index ec7de556..d09cb5cc 100644 --- a/sources/gcp/dynamic/adapters/file-instance_test.go +++ b/sources/gcp/dynamic/adapters/file-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/filestore/apiv1/filestorepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/iam-role.go b/sources/gcp/dynamic/adapters/iam-role.go index aeeb04fd..bff139b3 100644 --- a/sources/gcp/dynamic/adapters/iam-role.go +++ b/sources/gcp/dynamic/adapters/iam-role.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/iam-role_test.go b/sources/gcp/dynamic/adapters/iam-role_test.go index 71e0db5b..058cba6c 100644 --- a/sources/gcp/dynamic/adapters/iam-role_test.go +++ b/sources/gcp/dynamic/adapters/iam-role_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/iam/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-bucket.go b/sources/gcp/dynamic/adapters/logging-bucket.go index 53f101d3..b3bb259c 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket.go +++ b/sources/gcp/dynamic/adapters/logging-bucket.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-bucket_test.go b/sources/gcp/dynamic/adapters/logging-bucket_test.go index 20ab9db0..563cfa66 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket_test.go +++ b/sources/gcp/dynamic/adapters/logging-bucket_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-link.go b/sources/gcp/dynamic/adapters/logging-link.go index e338c8de..415b4bbc 100644 --- a/sources/gcp/dynamic/adapters/logging-link.go +++ b/sources/gcp/dynamic/adapters/logging-link.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-link_test.go b/sources/gcp/dynamic/adapters/logging-link_test.go index 9f586df4..3b333b2f 100644 --- a/sources/gcp/dynamic/adapters/logging-link_test.go +++ b/sources/gcp/dynamic/adapters/logging-link_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/logging-saved-query.go b/sources/gcp/dynamic/adapters/logging-saved-query.go index c23220a5..0e8567bb 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/logging-saved-query_test.go b/sources/gcp/dynamic/adapters/logging-saved-query_test.go index a60082f2..9fd02880 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query_test.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/logging/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go index b3b44e37..6b79d6ac 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index 4ff559be..7c10d7a0 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go index 4970facf..a7057142 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go index 0b5e954b..2ed436e8 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go @@ -8,8 +8,8 @@ import ( "google.golang.org/api/monitoring/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go index ced1a890..7fbce7d6 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go index 4a44fe7d..3057412b 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy.go b/sources/gcp/dynamic/adapters/orgpolicy-policy.go index 7c571eaa..fd12b69e 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go index e87ba80b..a476e090 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/orgpolicy/apiv2/orgpolicypb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription.go b/sources/gcp/dynamic/adapters/pubsub-subscription.go index c486b016..93e0c798 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index e20edf51..d1378ca2 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/pubsub-topic.go b/sources/gcp/dynamic/adapters/pubsub-topic.go index 34231585..8501f2de 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" diff --git a/sources/gcp/dynamic/adapters/pubsub-topic_test.go b/sources/gcp/dynamic/adapters/pubsub-topic_test.go index 5a9b6a2a..a88b0302 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/pubsub/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/redis-instance.go b/sources/gcp/dynamic/adapters/redis-instance.go index 681230ad..de20dac9 100644 --- a/sources/gcp/dynamic/adapters/redis-instance.go +++ b/sources/gcp/dynamic/adapters/redis-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index 42f5c233..19066bea 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/redis/apiv1/redispb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-revision.go b/sources/gcp/dynamic/adapters/run-revision.go index c7dea3c5..ca9aa0ec 100644 --- a/sources/gcp/dynamic/adapters/run-revision.go +++ b/sources/gcp/dynamic/adapters/run-revision.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/run-revision_test.go b/sources/gcp/dynamic/adapters/run-revision_test.go index 3a5a3d65..b5a813b5 100644 --- a/sources/gcp/dynamic/adapters/run-revision_test.go +++ b/sources/gcp/dynamic/adapters/run-revision_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/run/v2" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-service.go b/sources/gcp/dynamic/adapters/run-service.go index a0aa5d30..edbbefbc 100644 --- a/sources/gcp/dynamic/adapters/run-service.go +++ b/sources/gcp/dynamic/adapters/run-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/run-service_test.go b/sources/gcp/dynamic/adapters/run-service_test.go index f8f09e55..5d57ff4b 100644 --- a/sources/gcp/dynamic/adapters/run-service_test.go +++ b/sources/gcp/dynamic/adapters/run-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/run/apiv2/runpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/run-worker-pool.go b/sources/gcp/dynamic/adapters/run-worker-pool.go index 32a72c6a..937cc94a 100644 --- a/sources/gcp/dynamic/adapters/run-worker-pool.go +++ b/sources/gcp/dynamic/adapters/run-worker-pool.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret.go b/sources/gcp/dynamic/adapters/secret-manager-secret.go index c8bb9047..223f0cd0 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go index 5e34b28e..4a6f9924 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go index 1d7bfb41..722cecec 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go index 7f3df818..442c70d9 100644 --- a/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go +++ b/sources/gcp/dynamic/adapters/security-center-management-security-center-service_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/securitycentermanagement/apiv1/securitycentermanagementpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint.go b/sources/gcp/dynamic/adapters/service-directory-endpoint.go index ef527beb..906cb0a4 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go index 5277bc3b..96ba6461 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/servicedirectory/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/service-directory-service.go b/sources/gcp/dynamic/adapters/service-directory-service.go index 5f1f6741..7470338d 100644 --- a/sources/gcp/dynamic/adapters/service-directory-service.go +++ b/sources/gcp/dynamic/adapters/service-directory-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/service-usage-service.go b/sources/gcp/dynamic/adapters/service-usage-service.go index 713515e6..350a9074 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service.go +++ b/sources/gcp/dynamic/adapters/service-usage-service.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/service-usage-service_test.go b/sources/gcp/dynamic/adapters/service-usage-service_test.go index ee09fc9d..51ced862 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service_test.go +++ b/sources/gcp/dynamic/adapters/service-usage-service_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/serviceusage/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/spanner-backup.go b/sources/gcp/dynamic/adapters/spanner-backup.go index ae851489..3dce3115 100644 --- a/sources/gcp/dynamic/adapters/spanner-backup.go +++ b/sources/gcp/dynamic/adapters/spanner-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-database.go b/sources/gcp/dynamic/adapters/spanner-database.go index 9b17b9aa..257e107f 100644 --- a/sources/gcp/dynamic/adapters/spanner-database.go +++ b/sources/gcp/dynamic/adapters/spanner-database.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-database_test.go b/sources/gcp/dynamic/adapters/spanner-database_test.go index 53b76d3b..adc2e351 100644 --- a/sources/gcp/dynamic/adapters/spanner-database_test.go +++ b/sources/gcp/dynamic/adapters/spanner-database_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/spanner/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/spanner-instance-config.go b/sources/gcp/dynamic/adapters/spanner-instance-config.go index 7c852024..da9a01d4 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance-config.go +++ b/sources/gcp/dynamic/adapters/spanner-instance-config.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-instance.go b/sources/gcp/dynamic/adapters/spanner-instance.go index 3ffceb66..60780633 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance.go +++ b/sources/gcp/dynamic/adapters/spanner-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/spanner-instance_test.go b/sources/gcp/dynamic/adapters/spanner-instance_test.go index ce1cc38b..2f543c10 100644 --- a/sources/gcp/dynamic/adapters/spanner-instance_test.go +++ b/sources/gcp/dynamic/adapters/spanner-instance_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go index 2d4f185a..16d5aa06 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup-run.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup-run.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup.go b/sources/gcp/dynamic/adapters/sql-admin-backup.go index 061c5eec..036a50ca 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index d54365d4..1ff3843b 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance.go b/sources/gcp/dynamic/adapters/sql-admin-instance.go index a410c8b9..f0d003b7 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance.go @@ -1,7 +1,7 @@ package adapters import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index 1bbcfd3c..3491a795 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/sqladmin/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/storage-bucket.go b/sources/gcp/dynamic/adapters/storage-bucket.go index 7a917ddc..798827cb 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket.go +++ b/sources/gcp/dynamic/adapters/storage-bucket.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index aa30b4ee..937922be 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -8,9 +8,9 @@ import ( "google.golang.org/api/storage/v1" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index 9c63605f..d13eeae9 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -3,7 +3,7 @@ package adapters import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go index decea52a..b8f85d5c 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/storagetransfer/apiv1/storagetransferpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/adapters_test.go b/sources/gcp/dynamic/adapters_test.go index 06e6f25e..59ba8c50 100644 --- a/sources/gcp/dynamic/adapters_test.go +++ b/sources/gcp/dynamic/adapters_test.go @@ -4,9 +4,9 @@ import ( "net/http" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index c86e08e1..b6ea6204 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -14,9 +14,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/dynamic/shared_test.go b/sources/gcp/dynamic/shared_test.go index 0ab37ed3..1c7e099d 100644 --- a/sources/gcp/dynamic/shared_test.go +++ b/sources/gcp/dynamic/shared_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/protobuf/types/known/structpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/integration-tests/big-query-model_test.go b/sources/gcp/integration-tests/big-query-model_test.go index 37688097..73362aa9 100644 --- a/sources/gcp/integration-tests/big-query-model_test.go +++ b/sources/gcp/integration-tests/big-query-model_test.go @@ -9,7 +9,7 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-address_test.go b/sources/gcp/integration-tests/compute-address_test.go index c3691def..008b1c83 100644 --- a/sources/gcp/integration-tests/compute-address_test.go +++ b/sources/gcp/integration-tests/compute-address_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-autoscaler_test.go b/sources/gcp/integration-tests/compute-autoscaler_test.go index 9b2454b5..8853286e 100644 --- a/sources/gcp/integration-tests/compute-autoscaler_test.go +++ b/sources/gcp/integration-tests/compute-autoscaler_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-disk_test.go b/sources/gcp/integration-tests/compute-disk_test.go index 8f93b675..58997c1c 100644 --- a/sources/gcp/integration-tests/compute-disk_test.go +++ b/sources/gcp/integration-tests/compute-disk_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-forwarding-rule_test.go b/sources/gcp/integration-tests/compute-forwarding-rule_test.go index 9f1bfdf0..0b747dbe 100644 --- a/sources/gcp/integration-tests/compute-forwarding-rule_test.go +++ b/sources/gcp/integration-tests/compute-forwarding-rule_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-healthcheck_test.go b/sources/gcp/integration-tests/compute-healthcheck_test.go index 1f749417..4e8638ab 100644 --- a/sources/gcp/integration-tests/compute-healthcheck_test.go +++ b/sources/gcp/integration-tests/compute-healthcheck_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-image_test.go b/sources/gcp/integration-tests/compute-image_test.go index 5a3a58ba..a58ef552 100644 --- a/sources/gcp/integration-tests/compute-image_test.go +++ b/sources/gcp/integration-tests/compute-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group-manager_test.go b/sources/gcp/integration-tests/compute-instance-group-manager_test.go index 7ec88bec..e286d301 100644 --- a/sources/gcp/integration-tests/compute-instance-group-manager_test.go +++ b/sources/gcp/integration-tests/compute-instance-group-manager_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance-group_test.go b/sources/gcp/integration-tests/compute-instance-group_test.go index 2972942b..8017775e 100644 --- a/sources/gcp/integration-tests/compute-instance-group_test.go +++ b/sources/gcp/integration-tests/compute-instance-group_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instance_test.go b/sources/gcp/integration-tests/compute-instance_test.go index d5845431..23d4fb87 100644 --- a/sources/gcp/integration-tests/compute-instance_test.go +++ b/sources/gcp/integration-tests/compute-instance_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-instant-snapshot_test.go b/sources/gcp/integration-tests/compute-instant-snapshot_test.go index 16c30f5c..09de7f70 100644 --- a/sources/gcp/integration-tests/compute-instant-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-instant-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-machine-image_test.go b/sources/gcp/integration-tests/compute-machine-image_test.go index 706151ab..59f1ca2b 100644 --- a/sources/gcp/integration-tests/compute-machine-image_test.go +++ b/sources/gcp/integration-tests/compute-machine-image_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-network_test.go b/sources/gcp/integration-tests/compute-network_test.go index 3df39bb5..8fd975cb 100644 --- a/sources/gcp/integration-tests/compute-network_test.go +++ b/sources/gcp/integration-tests/compute-network_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/compute-node-group_test.go b/sources/gcp/integration-tests/compute-node-group_test.go index 120cdcb0..63edb298 100644 --- a/sources/gcp/integration-tests/compute-node-group_test.go +++ b/sources/gcp/integration-tests/compute-node-group_test.go @@ -15,9 +15,9 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-reservation_test.go b/sources/gcp/integration-tests/compute-reservation_test.go index 916f99f2..2046f4b2 100644 --- a/sources/gcp/integration-tests/compute-reservation_test.go +++ b/sources/gcp/integration-tests/compute-reservation_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-snapshot_test.go b/sources/gcp/integration-tests/compute-snapshot_test.go index 5ecad320..b6a73c28 100644 --- a/sources/gcp/integration-tests/compute-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-snapshot_test.go @@ -14,8 +14,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/integration-tests/compute-subnetwork_test.go b/sources/gcp/integration-tests/compute-subnetwork_test.go index 01f722dd..f154ae4e 100644 --- a/sources/gcp/integration-tests/compute-subnetwork_test.go +++ b/sources/gcp/integration-tests/compute-subnetwork_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/computer-instance-template_test.go b/sources/gcp/integration-tests/computer-instance-template_test.go index ce171501..3e257c35 100644 --- a/sources/gcp/integration-tests/computer-instance-template_test.go +++ b/sources/gcp/integration-tests/computer-instance-template_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/integration-tests/spanner-database_test.go b/sources/gcp/integration-tests/spanner-database_test.go index 6ea73a4b..3500f1d2 100644 --- a/sources/gcp/integration-tests/spanner-database_test.go +++ b/sources/gcp/integration-tests/spanner-database_test.go @@ -14,7 +14,7 @@ import ( "github.com/googleapis/gax-go/v2/apierror" "google.golang.org/grpc/codes" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/integration-tests/spanner-instance_test.go b/sources/gcp/integration-tests/spanner-instance_test.go index c302c865..2ea00833 100644 --- a/sources/gcp/integration-tests/spanner-instance_test.go +++ b/sources/gcp/integration-tests/spanner-instance_test.go @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index eba915ec..a28cfb49 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -13,8 +13,8 @@ import ( "golang.org/x/oauth2" "google.golang.org/api/option" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/shared" ) diff --git a/sources/gcp/manual/big-query-dataset.go b/sources/gcp/manual/big-query-dataset.go index 376011df..79343481 100644 --- a/sources/gcp/manual/big-query-dataset.go +++ b/sources/gcp/manual/big-query-dataset.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-dataset_test.go b/sources/gcp/manual/big-query-dataset_test.go index cce7047f..8b137880 100644 --- a/sources/gcp/manual/big-query-dataset_test.go +++ b/sources/gcp/manual/big-query-dataset_test.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-model.go b/sources/gcp/manual/big-query-model.go index 351e6631..7c722476 100644 --- a/sources/gcp/manual/big-query-model.go +++ b/sources/gcp/manual/big-query-model.go @@ -5,9 +5,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-model_test.go b/sources/gcp/manual/big-query-model_test.go index 4487fb34..2a83d6ef 100644 --- a/sources/gcp/manual/big-query-model_test.go +++ b/sources/gcp/manual/big-query-model_test.go @@ -7,9 +7,9 @@ import ( bigquery "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-routine.go b/sources/gcp/manual/big-query-routine.go index d13b3692..566645d9 100644 --- a/sources/gcp/manual/big-query-routine.go +++ b/sources/gcp/manual/big-query-routine.go @@ -5,9 +5,9 @@ import ( "strings" "cloud.google.com/go/bigquery" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-routine_test.go b/sources/gcp/manual/big-query-routine_test.go index 31f11533..911e8609 100644 --- a/sources/gcp/manual/big-query-routine_test.go +++ b/sources/gcp/manual/big-query-routine_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/big-query-table.go b/sources/gcp/manual/big-query-table.go index d37fddb7..f0ac0121 100644 --- a/sources/gcp/manual/big-query-table.go +++ b/sources/gcp/manual/big-query-table.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/bigquery" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/big-query-table_test.go b/sources/gcp/manual/big-query-table_test.go index 28ec3780..22a261f2 100644 --- a/sources/gcp/manual/big-query-table_test.go +++ b/sources/gcp/manual/big-query-table_test.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/bigquery" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/certificate-manager-certificate.go b/sources/gcp/manual/certificate-manager-certificate.go index adeb568a..973ddc42 100644 --- a/sources/gcp/manual/certificate-manager-certificate.go +++ b/sources/gcp/manual/certificate-manager-certificate.go @@ -7,9 +7,9 @@ import ( certificatemanagerpb "cloud.google.com/go/certificatemanager/apiv1/certificatemanagerpb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/certificate-manager-certificate_test.go b/sources/gcp/manual/certificate-manager-certificate_test.go index ca753f4e..dd2ff0fd 100644 --- a/sources/gcp/manual/certificate-manager-certificate_test.go +++ b/sources/gcp/manual/certificate-manager-certificate_test.go @@ -9,9 +9,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version.go b/sources/gcp/manual/cloud-kms-crypto-key-version.go index 780bfcb1..b6e7d755 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go index 5896e7ff..e8c8c976 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key.go b/sources/gcp/manual/cloud-kms-crypto-key.go index a680fe9e..c69cbb78 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key.go +++ b/sources/gcp/manual/cloud-kms-crypto-key.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index 28dc56b5..4bd7195f 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/cloud-kms-key-ring.go b/sources/gcp/manual/cloud-kms-key-ring.go index e0d6bb88..ef91a848 100644 --- a/sources/gcp/manual/cloud-kms-key-ring.go +++ b/sources/gcp/manual/cloud-kms-key-ring.go @@ -3,9 +3,9 @@ package manual import ( "context" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index 42b11012..713d169b 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index d8290a00..3e5f8735 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index 9add24ac..8171cd94 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index 645ccf44..18923b2a 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index ca87a170..67ab1aa3 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index 08847b5e..d61a47dd 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 0ebacf3f..0a781f0f 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -13,9 +13,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index 7703f89c..697e074c 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index b051c4ba..4fcd801d 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index 360e9ac1..2eecc0ac 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index aab70018..976cbea9 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index 789a577e..b24e0ae6 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 61fc1cde..5df60f4f 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -13,9 +13,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-image.go b/sources/gcp/manual/compute-image.go index 4d0670bb..c2fc5784 100644 --- a/sources/gcp/manual/compute-image.go +++ b/sources/gcp/manual/compute-image.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index 7003dfd5..16567d49 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -14,9 +14,9 @@ import ( "google.golang.org/grpc/status" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance-group-manager-shared.go b/sources/gcp/manual/compute-instance-group-manager-shared.go index e4a5b133..adcf27c8 100644 --- a/sources/gcp/manual/compute-instance-group-manager-shared.go +++ b/sources/gcp/manual/compute-instance-group-manager-shared.go @@ -6,7 +6,7 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index f82b2f77..bb6cf3e5 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index 3aa3d4ab..ea85e239 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index b0d1a35c..82184f13 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index f6525c54..9fcfc6ef 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 6605c430..240d3728 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 4b0d8581..f441a8e2 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index e5dc575f..fb9d71e4 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index dffa8a9d..62813897 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-machine-image.go b/sources/gcp/manual/compute-machine-image.go index 9ee354b7..4c9343d9 100644 --- a/sources/gcp/manual/compute-machine-image.go +++ b/sources/gcp/manual/compute-machine-image.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index 9ceb43ee..7aac8391 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index 45c4bd50..cc6b6960 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -11,9 +11,9 @@ import ( "google.golang.org/protobuf/proto" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index 191fd63d..e099d5f3 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index 46535cb6..34b40b17 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index c82a6464..e4299916 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-region-instance-group-manager.go b/sources/gcp/manual/compute-region-instance-group-manager.go index cd87d6cf..02221d8a 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager.go +++ b/sources/gcp/manual/compute-region-instance-group-manager.go @@ -9,9 +9,9 @@ import ( "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-region-instance-group-manager_test.go b/sources/gcp/manual/compute-region-instance-group-manager_test.go index ed5f4ef5..2e90abaa 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index e35bbe4d..b29260a8 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/protobuf/proto" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 0ede68be..9e317dbc 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -11,9 +11,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-security-policy.go b/sources/gcp/manual/compute-security-policy.go index 1041704a..1ccf1355 100644 --- a/sources/gcp/manual/compute-security-policy.go +++ b/sources/gcp/manual/compute-security-policy.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index 7f3c3127..b93d4db3 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/compute-snapshot.go b/sources/gcp/manual/compute-snapshot.go index f4112374..124cba90 100644 --- a/sources/gcp/manual/compute-snapshot.go +++ b/sources/gcp/manual/compute-snapshot.go @@ -7,9 +7,9 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 87520cdf..34b013f8 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/api/iterator" "k8s.io/utils/ptr" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/iam-service-account-key.go b/sources/gcp/manual/iam-service-account-key.go index 5d7b72ec..4ce0dbbc 100644 --- a/sources/gcp/manual/iam-service-account-key.go +++ b/sources/gcp/manual/iam-service-account-key.go @@ -6,9 +6,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/iam-service-account-key_test.go b/sources/gcp/manual/iam-service-account-key_test.go index 7ee7be17..ae2c745c 100644 --- a/sources/gcp/manual/iam-service-account-key_test.go +++ b/sources/gcp/manual/iam-service-account-key_test.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "go.uber.org/mock/gomock" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/iam-service-account.go b/sources/gcp/manual/iam-service-account.go index 8a1fd392..1d5c4f2e 100644 --- a/sources/gcp/manual/iam-service-account.go +++ b/sources/gcp/manual/iam-service-account.go @@ -8,9 +8,9 @@ import ( "cloud.google.com/go/iam/admin/apiv1/adminpb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/iam-service-account_test.go b/sources/gcp/manual/iam-service-account_test.go index 48cdac1c..08b2caba 100644 --- a/sources/gcp/manual/iam-service-account_test.go +++ b/sources/gcp/manual/iam-service-account_test.go @@ -9,9 +9,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/logging-sink.go b/sources/gcp/manual/logging-sink.go index 000cb827..b98b8726 100644 --- a/sources/gcp/manual/logging-sink.go +++ b/sources/gcp/manual/logging-sink.go @@ -9,9 +9,9 @@ import ( "cloud.google.com/go/logging/apiv2/loggingpb" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/logging-sink_test.go b/sources/gcp/manual/logging-sink_test.go index 1a43f578..d106a5e1 100644 --- a/sources/gcp/manual/logging-sink_test.go +++ b/sources/gcp/manual/logging-sink_test.go @@ -10,9 +10,9 @@ import ( "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/manual/storage-bucket-iam-policy.go b/sources/gcp/manual/storage-bucket-iam-policy.go index 1f573f50..7c470a3f 100644 --- a/sources/gcp/manual/storage-bucket-iam-policy.go +++ b/sources/gcp/manual/storage-bucket-iam-policy.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/gcp/manual/storage-bucket-iam-policy_test.go b/sources/gcp/manual/storage-bucket-iam-policy_test.go index a363d953..51da30ad 100644 --- a/sources/gcp/manual/storage-bucket-iam-policy_test.go +++ b/sources/gcp/manual/storage-bucket-iam-policy_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/gcp/manual" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" diff --git a/sources/gcp/proc/proc.go b/sources/gcp/proc/proc.go index 229732f2..588f8e3d 100644 --- a/sources/gcp/proc/proc.go +++ b/sources/gcp/proc/proc.go @@ -19,9 +19,9 @@ import ( "google.golang.org/api/iterator" "google.golang.org/api/option" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/gcp/dynamic" _ "github.com/overmindtech/cli/sources/gcp/dynamic/adapters" // Import all adapters to register them "github.com/overmindtech/cli/sources/gcp/manual" diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index 10b0abc2..d244fcf7 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" _ "github.com/overmindtech/cli/sources/gcp/dynamic" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "google.golang.org/protobuf/types/known/structpb" diff --git a/sources/gcp/shared/adapter-meta.go b/sources/gcp/shared/adapter-meta.go index dfbf0748..455ad73c 100644 --- a/sources/gcp/shared/adapter-meta.go +++ b/sources/gcp/shared/adapter-meta.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/base.go b/sources/gcp/shared/base.go index e41c7d3b..57e46afa 100644 --- a/sources/gcp/shared/base.go +++ b/sources/gcp/shared/base.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/big-query-clients.go b/sources/gcp/shared/big-query-clients.go index 69dedb15..79308a9e 100644 --- a/sources/gcp/shared/big-query-clients.go +++ b/sources/gcp/shared/big-query-clients.go @@ -8,8 +8,8 @@ import ( "cloud.google.com/go/bigquery" "google.golang.org/api/iterator" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) type BigQueryRoutineClient interface { @@ -75,7 +75,7 @@ func NewBigQueryRoutineClient(client *bigquery.Client) BigQueryRoutineClient { } } -//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/workspace/sdp-go +//go:generate mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/go/sdp-go type BigQueryDatasetClient interface { Get(ctx context.Context, projectID, datasetID string) (*bigquery.DatasetMetadata, error) List(ctx context.Context, projectID string, toSDPItem func(ctx context.Context, dataset *bigquery.DatasetMetadata) (*sdp.Item, *sdp.QueryError)) ([]*sdp.Item, *sdp.QueryError) diff --git a/sources/gcp/shared/cross_project_linking_test.go b/sources/gcp/shared/cross_project_linking_test.go index 0b05964e..f115e1b8 100644 --- a/sources/gcp/shared/cross_project_linking_test.go +++ b/sources/gcp/shared/cross_project_linking_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestProjectBaseLinkedItemQueryByName_CrossProject verifies that project-level diff --git a/sources/gcp/shared/errors.go b/sources/gcp/shared/errors.go index dc9a5e6d..e13f6976 100644 --- a/sources/gcp/shared/errors.go +++ b/sources/gcp/shared/errors.go @@ -4,7 +4,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // QueryError is a helper function to convert errors into sdp.QueryError diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go index 5e646232..93c481cb 100644 --- a/sources/gcp/shared/kms-asset-loader.go +++ b/sources/gcp/shared/kms-asset-loader.go @@ -15,9 +15,9 @@ import ( "golang.org/x/sync/singleflight" "google.golang.org/protobuf/encoding/protojson" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 66516a74..719640b8 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/shared/linker_test.go b/sources/gcp/shared/linker_test.go index 9ea5c562..71806580 100644 --- a/sources/gcp/shared/linker_test.go +++ b/sources/gcp/shared/linker_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index b1cba0d5..d3004001 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" diff --git a/sources/gcp/shared/manual-adapter-links_test.go b/sources/gcp/shared/manual-adapter-links_test.go index 20559f7f..7370f7f9 100644 --- a/sources/gcp/shared/manual-adapter-links_test.go +++ b/sources/gcp/shared/manual-adapter-links_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" aws "github.com/overmindtech/cli/sources/aws/shared" "github.com/overmindtech/cli/sources/stdlib" ) diff --git a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go index 9781a187..8e32ea34 100644 --- a/sources/gcp/shared/mocks/mock_big_query_dataset_client.go +++ b/sources/gcp/shared/mocks/mock_big_query_dataset_client.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/workspace/sdp-go +// mockgen -destination=./mocks/mock_big_query_dataset_client.go -package=mocks -source=big-query-clients.go -imports=sdp=github.com/overmindtech/cli/go/sdp-go // // Package mocks is a generated GoMock package. @@ -14,8 +14,8 @@ import ( reflect "reflect" bigquery "cloud.google.com/go/bigquery" - discovery "github.com/overmindtech/workspace/discovery" - sdp "github.com/overmindtech/workspace/sdp-go" + discovery "github.com/overmindtech/cli/go/discovery" + sdp "github.com/overmindtech/cli/go/sdp-go" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/gcp/shared/terraform-mappings.go b/sources/gcp/shared/terraform-mappings.go index 94c4e367..b7335f9f 100644 --- a/sources/gcp/shared/terraform-mappings.go +++ b/sources/gcp/shared/terraform-mappings.go @@ -1,7 +1,7 @@ package shared import ( - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources/shared" ) diff --git a/sources/shared/base.go b/sources/shared/base.go index d31c37e3..6789486d 100644 --- a/sources/shared/base.go +++ b/sources/shared/base.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // Base is a struct that holds fundamental pieces for creating an adapter. diff --git a/sources/shared/testing.go b/sources/shared/testing.go index 8cc2f637..d1898125 100644 --- a/sources/shared/testing.go +++ b/sources/shared/testing.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) // RunStaticTests runs static tests on the given adapter and item. diff --git a/sources/shared/util.go b/sources/shared/util.go index 5ae3e010..4f04fa51 100644 --- a/sources/shared/util.go +++ b/sources/shared/util.go @@ -3,7 +3,7 @@ package shared import ( "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // ToAttributesWithExclude converts an interface to SDP attributes using the `sdp.ToAttributesSorted` diff --git a/sources/transformer.go b/sources/transformer.go index 9ad83b70..92c9f0d0 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -9,9 +9,9 @@ import ( "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" azureshared "github.com/overmindtech/cli/sources/azure/shared" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/sources/transformer_test.go b/sources/transformer_test.go index 87dbd4c6..ac18c762 100644 --- a/sources/transformer_test.go +++ b/sources/transformer_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" aws "github.com/overmindtech/cli/sources/aws/shared" gcp "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" diff --git a/stdlib-source/adapters/certificate.go b/stdlib-source/adapters/certificate.go index b072dab8..018c2c5e 100644 --- a/stdlib-source/adapters/certificate.go +++ b/stdlib-source/adapters/certificate.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // CertToName Returns the name of a cert as a string. This is in the format of: diff --git a/stdlib-source/adapters/certificate_test.go b/stdlib-source/adapters/certificate_test.go index a00ad011..871fda7c 100644 --- a/stdlib-source/adapters/certificate_test.go +++ b/stdlib-source/adapters/certificate_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) var chain = `-----BEGIN CERTIFICATE----- diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index 72533731..3d7879fe 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -11,8 +11,8 @@ import ( "github.com/cenkalti/backoff/v5" "github.com/miekg/dns" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) diff --git a/stdlib-source/adapters/dns_test.go b/stdlib-source/adapters/dns_test.go index 5501ff67..091c880f 100644 --- a/stdlib-source/adapters/dns_test.go +++ b/stdlib-source/adapters/dns_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestSearch(t *testing.T) { diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 53a25ecb..4db87391 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index db0cecf3..5370cd79 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) const TestHTTPTimeout = 3 * time.Second diff --git a/stdlib-source/adapters/ip.go b/stdlib-source/adapters/ip.go index 0ac8feb4..dfc1d419 100644 --- a/stdlib-source/adapters/ip.go +++ b/stdlib-source/adapters/ip.go @@ -5,7 +5,7 @@ import ( "fmt" "net" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // IPAdapter struct on which all methods are registered diff --git a/stdlib-source/adapters/ip_test.go b/stdlib-source/adapters/ip_test.go index 0db5e634..89e7ee52 100644 --- a/stdlib-source/adapters/ip_test.go +++ b/stdlib-source/adapters/ip_test.go @@ -5,8 +5,8 @@ import ( "regexp" "testing" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" ) func TestIPGet(t *testing.T) { diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index 3f2eb9da..1c8ddfa0 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -9,9 +9,9 @@ import ( "time" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/stdlib-source/adapters/test" _ "embed" diff --git a/stdlib-source/adapters/rdap-asn.go b/stdlib-source/adapters/rdap-asn.go index 15d712f5..dd41e7b9 100644 --- a/stdlib-source/adapters/rdap-asn.go +++ b/stdlib-source/adapters/rdap-asn.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapASNAdapter struct { diff --git a/stdlib-source/adapters/rdap-asn_test.go b/stdlib-source/adapters/rdap-asn_test.go index 6a03f0c3..6559d922 100644 --- a/stdlib-source/adapters/rdap-asn_test.go +++ b/stdlib-source/adapters/rdap-asn_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestASNAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-domain.go b/stdlib-source/adapters/rdap-domain.go index 00ace4b4..1502e069 100644 --- a/stdlib-source/adapters/rdap-domain.go +++ b/stdlib-source/adapters/rdap-domain.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapDomainAdapter struct { diff --git a/stdlib-source/adapters/rdap-domain_test.go b/stdlib-source/adapters/rdap-domain_test.go index 7a95b4ec..62260794 100644 --- a/stdlib-source/adapters/rdap-domain_test.go +++ b/stdlib-source/adapters/rdap-domain_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestDomainAdapterGet(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-entity.go b/stdlib-source/adapters/rdap-entity.go index 13adc8de..9cb30028 100644 --- a/stdlib-source/adapters/rdap-entity.go +++ b/stdlib-source/adapters/rdap-entity.go @@ -6,8 +6,8 @@ import ( "net/url" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapEntityAdapter struct { diff --git a/stdlib-source/adapters/rdap-entity_test.go b/stdlib-source/adapters/rdap-entity_test.go index a4f2825b..2c37be37 100644 --- a/stdlib-source/adapters/rdap-entity_test.go +++ b/stdlib-source/adapters/rdap-entity_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) func TestEntityAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-ip-network.go b/stdlib-source/adapters/rdap-ip-network.go index d06bea06..212b55a8 100644 --- a/stdlib-source/adapters/rdap-ip-network.go +++ b/stdlib-source/adapters/rdap-ip-network.go @@ -6,8 +6,8 @@ import ( "net" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapIPNetworkAdapter struct { diff --git a/stdlib-source/adapters/rdap-ip-network_test.go b/stdlib-source/adapters/rdap-ip-network_test.go index 7edcff51..2bb29fe0 100644 --- a/stdlib-source/adapters/rdap-ip-network_test.go +++ b/stdlib-source/adapters/rdap-ip-network_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestIpNetworkAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/rdap-nameserver.go b/stdlib-source/adapters/rdap-nameserver.go index 414c8bd2..0d0f052f 100644 --- a/stdlib-source/adapters/rdap-nameserver.go +++ b/stdlib-source/adapters/rdap-nameserver.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdp-go" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" ) type RdapNameserverAdapter struct { diff --git a/stdlib-source/adapters/rdap-nameserver_test.go b/stdlib-source/adapters/rdap-nameserver_test.go index 0a57e28e..ebfdd282 100644 --- a/stdlib-source/adapters/rdap-nameserver_test.go +++ b/stdlib-source/adapters/rdap-nameserver_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/openrdap/rdap" - "github.com/overmindtech/workspace/sdpcache" + "github.com/overmindtech/cli/go/sdpcache" ) func TestNameserverAdapterSearch(t *testing.T) { diff --git a/stdlib-source/adapters/test/data.go b/stdlib-source/adapters/test/data.go index 140ed090..4f736554 100644 --- a/stdlib-source/adapters/test/data.go +++ b/stdlib-source/adapters/test/data.go @@ -5,7 +5,7 @@ import ( "sync/atomic" "time" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/stdlib-source/adapters/test/testdog.go b/stdlib-source/adapters/test/testdog.go index 96776474..225def11 100644 --- a/stdlib-source/adapters/test/testdog.go +++ b/stdlib-source/adapters/test/testdog.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestDogAdapter An adapter of `dog` items for automated tests. diff --git a/stdlib-source/adapters/test/testfood.go b/stdlib-source/adapters/test/testfood.go index 6b4ccd5c..4d7e2366 100644 --- a/stdlib-source/adapters/test/testfood.go +++ b/stdlib-source/adapters/test/testfood.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestFoodAdapter A adapter of `food` items for automated tests. diff --git a/stdlib-source/adapters/test/testgroup.go b/stdlib-source/adapters/test/testgroup.go index 6bd2d96f..af73941d 100644 --- a/stdlib-source/adapters/test/testgroup.go +++ b/stdlib-source/adapters/test/testgroup.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestGroupAdapter A adapter of `group` items for automated tests. diff --git a/stdlib-source/adapters/test/testhobby.go b/stdlib-source/adapters/test/testhobby.go index 29037d90..43ea989b 100644 --- a/stdlib-source/adapters/test/testhobby.go +++ b/stdlib-source/adapters/test/testhobby.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestHobbyAdapter A adapter of `hobby` items for automated tests. diff --git a/stdlib-source/adapters/test/testlocation.go b/stdlib-source/adapters/test/testlocation.go index 91f644b0..cf487d72 100644 --- a/stdlib-source/adapters/test/testlocation.go +++ b/stdlib-source/adapters/test/testlocation.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestLocationAdapter A adapter of `location` items for automated tests. diff --git a/stdlib-source/adapters/test/testperson.go b/stdlib-source/adapters/test/testperson.go index 4d25b960..d043e2e5 100644 --- a/stdlib-source/adapters/test/testperson.go +++ b/stdlib-source/adapters/test/testperson.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestPersonAdapter A adapter of `person` items for automated tests. diff --git a/stdlib-source/adapters/test/testregion.go b/stdlib-source/adapters/test/testregion.go index 7ad32442..42e49ac3 100644 --- a/stdlib-source/adapters/test/testregion.go +++ b/stdlib-source/adapters/test/testregion.go @@ -3,7 +3,7 @@ package test import ( "context" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" ) // TestRegionAdapter A adapter of `region` items for automated tests. diff --git a/stdlib-source/build/package/Dockerfile b/stdlib-source/build/package/Dockerfile index c7f6d7f5..deb0f149 100644 --- a/stdlib-source/build/package/Dockerfile +++ b/stdlib-source/build/package/Dockerfile @@ -16,7 +16,7 @@ COPY . . # Build RUN --mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/workspace/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/workspace/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go + GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w -X github.com/overmindtech/cli/go/tracing.version=${BUILD_VERSION} -X github.com/overmindtech/cli/go/tracing.commit=${BUILD_COMMIT}" -o source stdlib-source/main.go FROM alpine:3.23 WORKDIR / diff --git a/stdlib-source/cmd/root.go b/stdlib-source/cmd/root.go index 6c2e5582..d2e37d6d 100644 --- a/stdlib-source/cmd/root.go +++ b/stdlib-source/cmd/root.go @@ -9,10 +9,10 @@ import ( "syscall" "github.com/getsentry/sentry-go" - "github.com/overmindtech/workspace/discovery" - "github.com/overmindtech/workspace/logging" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/workspace/tracing" + "github.com/overmindtech/cli/go/tracing" "github.com/spf13/cobra" "github.com/spf13/pflag" diff --git a/tfutils/plan_mapper.go b/tfutils/plan_mapper.go index 43f81b81..d79409eb 100644 --- a/tfutils/plan_mapper.go +++ b/tfutils/plan_mapper.go @@ -13,7 +13,7 @@ import ( "github.com/google/uuid" awsAdapters "github.com/overmindtech/cli/aws-source/adapters" k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" azureAdapters "github.com/overmindtech/cli/sources/azure/proc" log "github.com/sirupsen/logrus" diff --git a/tfutils/plan_mapper_test.go b/tfutils/plan_mapper_test.go index 2d242d7f..e6788c2f 100644 --- a/tfutils/plan_mapper_test.go +++ b/tfutils/plan_mapper_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/overmindtech/workspace/sdp-go" + "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/xiam/dig" From 715959ab95c0195cb94b0ce16ac2151fa4223f07 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:05:06 +0100 Subject: [PATCH 26/51] Eng 2430 create computesnapshot adapter (with cursor skill) (#3908) > [!NOTE] > **Medium Risk** > Adds a new Azure `ComputeSnapshot` discovery adapter and wires it into adapter initialization, which expands discovery surface area. Also changes `ExtractPathParamsFromResourceID` matching semantics (case-insensitive, structural-slot-only), which could affect link extraction across existing adapters. > > **Overview** > Introduces a new `ComputeSnapshot` adapter (with `List`, `ListStream`, and `Get`) that converts Azure snapshots into SDP items, including **health mapping** and extensive **linked-item queries** (disks/snapshots, disk access, encryption sets, storage account/container + HTTP/DNS/IP, gallery images, Elastic SAN snapshots, and Key Vault resources). > > Wires snapshot discovery into `manual/adapters.go` (real and placeholder modes) and adds a `SnapshotsClient` interface + generated gomock for testability. > > Hardens Azure resource ID parsing by making `ExtractPathParamsFromResourceID` case-insensitive and only matching keys in structural path positions, and updates community gallery parsing accordingly; adds unit tests and a full integration test that creates a disk+snapshot and validates `Get`/`List` behavior and links. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c763c543a66caba6d140138ba1bf945587377f69. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 49020216bbe668de90cd729190b23d0ec63bb067 --- sources/azure/clients/snapshots-client.go | 36 + .../compute-snapshot_test.go | 442 +++++++++++ sources/azure/manual/adapters.go | 18 +- sources/azure/manual/compute-disk.go | 2 +- sources/azure/manual/compute-snapshot.go | 751 ++++++++++++++++++ sources/azure/manual/compute-snapshot_test.go | 739 +++++++++++++++++ .../shared/mocks/mock_snapshots_client.go | 72 ++ sources/azure/shared/utils.go | 10 +- sources/azure/shared/utils_test.go | 30 + 9 files changed, 2094 insertions(+), 6 deletions(-) create mode 100644 sources/azure/clients/snapshots-client.go create mode 100644 sources/azure/integration-tests/compute-snapshot_test.go create mode 100644 sources/azure/manual/compute-snapshot.go create mode 100644 sources/azure/manual/compute-snapshot_test.go create mode 100644 sources/azure/shared/mocks/mock_snapshots_client.go diff --git a/sources/azure/clients/snapshots-client.go b/sources/azure/clients/snapshots-client.go new file mode 100644 index 00000000..9070c3ce --- /dev/null +++ b/sources/azure/clients/snapshots-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_snapshots_client.go -package=mocks -source=snapshots-client.go + +// SnapshotsPager is a type alias for the generic Pager interface with snapshot response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type SnapshotsPager = Pager[armcompute.SnapshotsClientListByResourceGroupResponse] + +// SnapshotsClient is an interface for interacting with Azure snapshots +type SnapshotsClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) SnapshotsPager + Get(ctx context.Context, resourceGroupName string, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) +} + +type snapshotsClient struct { + client *armcompute.SnapshotsClient +} + +func (a *snapshotsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) SnapshotsPager { + return a.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (a *snapshotsClient) Get(ctx context.Context, resourceGroupName string, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, snapshotName, options) +} + +// NewSnapshotsClient creates a new SnapshotsClient from the Azure SDK client +func NewSnapshotsClient(client *armcompute.SnapshotsClient) SnapshotsClient { + return &snapshotsClient{client: client} +} diff --git a/sources/azure/integration-tests/compute-snapshot_test.go b/sources/azure/integration-tests/compute-snapshot_test.go new file mode 100644 index 00000000..3ad4d733 --- /dev/null +++ b/sources/azure/integration-tests/compute-snapshot_test.go @@ -0,0 +1,442 @@ +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" + log "github.com/sirupsen/logrus" + "k8s.io/utils/ptr" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +const ( + integrationTestSnapshotName = "ovm-integ-test-snapshot" + integrationTestDiskForSnapName = "ovm-integ-test-disk-for-snap" +) + +func TestComputeSnapshotIntegration(t *testing.T) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Skip("AZURE_SUBSCRIPTION_ID environment variable not set") + } + + cred, err := azureshared.NewAzureCredential(t.Context()) + if err != nil { + t.Fatalf("Failed to create Azure credential: %v", err) + } + + snapshotClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Snapshots client: %v", err) + } + + diskClient, err := armcompute.NewDisksClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Disks client: %v", err) + } + + rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) + if err != nil { + t.Fatalf("Failed to create Resource Groups client: %v", err) + } + + t.Run("Setup", func(t *testing.T) { + ctx := t.Context() + + err := createResourceGroup(ctx, rgClient, integrationTestResourceGroup, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create resource group: %v", err) + } + + // Create a disk to snapshot from + err = createDisk(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName, integrationTestLocation) + if err != nil { + t.Fatalf("Failed to create disk: %v", err) + } + + err = waitForDiskAvailable(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName) + if err != nil { + t.Fatalf("Failed waiting for disk to be available: %v", err) + } + + // Get disk ID for snapshot creation + diskResp, err := diskClient.Get(ctx, integrationTestResourceGroup, integrationTestDiskForSnapName, nil) + if err != nil { + t.Fatalf("Failed to get disk: %v", err) + } + + // Create snapshot from the disk + err = createSnapshot(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName, integrationTestLocation, *diskResp.ID) + if err != nil { + t.Fatalf("Failed to create snapshot: %v", err) + } + + err = waitForSnapshotAvailable(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName) + if err != nil { + t.Fatalf("Failed waiting for snapshot to be available: %v", err) + } + }) + + t.Run("Run", func(t *testing.T) { + ctx := t.Context() + _, err := snapshotClient.Get(ctx, integrationTestResourceGroup, integrationTestSnapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + t.Skipf("Snapshot %s does not exist - Setup may have failed. Skipping Run tests.", integrationTestSnapshotName) + } + } + + t.Run("GetSnapshot", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Retrieving snapshot %s in subscription %s, resource group %s", + integrationTestSnapshotName, subscriptionID, integrationTestResourceGroup) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem == nil { + t.Fatalf("Expected sdpItem to be non-nil") + } + + uniqueAttrKey := sdpItem.GetUniqueAttribute() + uniqueAttrValue, err := sdpItem.GetAttributes().Get(uniqueAttrKey) + if err != nil { + t.Fatalf("Failed to get unique attribute: %v", err) + } + + if uniqueAttrValue != integrationTestSnapshotName { + t.Fatalf("Expected unique attribute value to be %s, got %s", integrationTestSnapshotName, uniqueAttrValue) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Fatalf("Expected type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + log.Printf("Successfully retrieved snapshot %s", integrationTestSnapshotName) + }) + + t.Run("ListSnapshots", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Listing snapshots in subscription %s, resource group %s", + subscriptionID, integrationTestResourceGroup) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + + listable, ok := snapshotAdapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Failed to list snapshots: %v", err) + } + + if len(sdpItems) < 1 { + t.Fatalf("Expected at least one snapshot, got %d", len(sdpItems)) + } + + var found bool + for _, item := range sdpItems { + uniqueAttrKey := item.GetUniqueAttribute() + if v, err := item.GetAttributes().Get(uniqueAttrKey); err == nil && v == integrationTestSnapshotName { + found = true + if item.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSnapshot, item.GetType()) + } + break + } + } + + if !found { + t.Fatalf("Expected to find snapshot %s in the list of snapshots", integrationTestSnapshotName) + } + + log.Printf("Found %d snapshots in resource group %s", len(sdpItems), integrationTestResourceGroup) + }) + + t.Run("VerifyItemAttributes", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying item attributes for snapshot %s", integrationTestSnapshotName) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected item type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + expectedScope := fmt.Sprintf("%s.%s", subscriptionID, integrationTestResourceGroup) + if sdpItem.GetScope() != expectedScope { + t.Errorf("Expected scope %s, got %s", expectedScope, sdpItem.GetScope()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("Expected health OK, got %s", sdpItem.GetHealth()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Item validation failed: %v", err) + } + + log.Printf("Verified item attributes for snapshot %s", integrationTestSnapshotName) + }) + + t.Run("VerifyLinkedItems", func(t *testing.T) { + ctx := t.Context() + + log.Printf("Verifying linked items for snapshot %s", integrationTestSnapshotName) + + snapshotWrapper := manual.NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotClient), + []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, + ) + scope := snapshotWrapper.Scopes()[0] + + snapshotAdapter := sources.WrapperToAdapter(snapshotWrapper, sdpcache.NewNoOpCache()) + sdpItem, qErr := snapshotAdapter.Get(ctx, scope, integrationTestSnapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + linkedQueries := sdpItem.GetLinkedItemQueries() + log.Printf("Found %d linked item queries for snapshot %s", len(linkedQueries), integrationTestSnapshotName) + + // The snapshot was created from a disk, so we expect a link to the source disk + var hasDiskLink bool + for _, liq := range linkedQueries { + query := liq.GetQuery() + if query == nil { + t.Error("Linked item query has nil Query") + continue + } + + if query.GetType() == "" { + t.Error("Linked item query has empty Type") + } + if query.GetMethod() != sdp.QueryMethod_GET && query.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Linked item query has unexpected Method: %v", query.GetMethod()) + } + if query.GetQuery() == "" { + t.Error("Linked item query has empty Query") + } + if query.GetScope() == "" { + t.Error("Linked item query has empty Scope") + } + + bp := liq.GetBlastPropagation() + if bp == nil { + t.Error("Linked item query has nil BlastPropagation") + } + + if query.GetType() == azureshared.ComputeDisk.String() { + hasDiskLink = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected disk link method to be GET, got %s", query.GetMethod()) + } + if query.GetQuery() != integrationTestDiskForSnapName { + t.Errorf("Expected disk link query to be %s, got %s", integrationTestDiskForSnapName, query.GetQuery()) + } + if bp != nil { + if bp.GetIn() != true { + t.Error("Expected disk blast propagation In=true, got false") + } + if bp.GetOut() != false { + t.Error("Expected disk blast propagation Out=false, got true") + } + } + } + + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) + } + + if !hasDiskLink { + t.Error("Expected to find a link to the source disk") + } + + log.Printf("Verified %d linked item queries for snapshot %s", len(linkedQueries), integrationTestSnapshotName) + }) + }) + + t.Run("Teardown", func(t *testing.T) { + ctx := t.Context() + + // Delete snapshot first + err := deleteSnapshot(ctx, snapshotClient, integrationTestResourceGroup, integrationTestSnapshotName) + if err != nil { + t.Fatalf("Failed to delete snapshot: %v", err) + } + + // Delete the source disk + err = deleteDisk(ctx, diskClient, integrationTestResourceGroup, integrationTestDiskForSnapName) + if err != nil { + t.Fatalf("Failed to delete disk: %v", err) + } + }) +} + +// createSnapshot creates an Azure snapshot from a source disk (idempotent) +func createSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName, location, sourceDiskID string) error { + existingSnapshot, err := client.Get(ctx, resourceGroupName, snapshotName, nil) + if err == nil { + if existingSnapshot.Properties != nil && existingSnapshot.Properties.ProvisioningState != nil { + state := *existingSnapshot.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Snapshot %s already exists with state %s, skipping creation", snapshotName, state) + return nil + } + log.Printf("Snapshot %s exists but in state %s, will wait for it", snapshotName, state) + } else { + log.Printf("Snapshot %s already exists, skipping creation", snapshotName) + return nil + } + } + + poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, snapshotName, armcompute.Snapshot{ + Location: ptr.To(location), + Properties: &armcompute.SnapshotProperties{ + CreationData: &armcompute.CreationData{ + CreateOption: ptr.To(armcompute.DiskCreateOptionCopy), + SourceResourceID: ptr.To(sourceDiskID), + }, + }, + Tags: map[string]*string{ + "purpose": ptr.To("overmind-integration-tests"), + "test": ptr.To("compute-snapshot"), + }, + }, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusConflict { + log.Printf("Snapshot %s already exists (conflict), skipping creation", snapshotName) + return nil + } + return fmt.Errorf("failed to begin creating snapshot: %w", err) + } + + resp, err := poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to create snapshot: %w", err) + } + + if resp.Properties == nil || resp.Properties.ProvisioningState == nil { + return fmt.Errorf("snapshot created but provisioning state is unknown") + } + + provisioningState := *resp.Properties.ProvisioningState + if provisioningState != "Succeeded" { + return fmt.Errorf("snapshot provisioning state is %s, expected Succeeded", provisioningState) + } + + log.Printf("Snapshot %s created successfully with provisioning state: %s", snapshotName, provisioningState) + return nil +} + +// waitForSnapshotAvailable polls until the snapshot is available via the Get API +func waitForSnapshotAvailable(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName string) error { + maxAttempts := 20 + pollInterval := 5 * time.Second + + log.Printf("Waiting for snapshot %s to be available via API...", snapshotName) + + for attempt := 1; attempt <= maxAttempts; attempt++ { + resp, err := client.Get(ctx, resourceGroupName, snapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Snapshot %s not yet available (attempt %d/%d), waiting %v...", snapshotName, attempt, maxAttempts, pollInterval) + time.Sleep(pollInterval) + continue + } + return fmt.Errorf("error checking snapshot availability: %w", err) + } + + if resp.Properties != nil && resp.Properties.ProvisioningState != nil { + state := *resp.Properties.ProvisioningState + if state == "Succeeded" { + log.Printf("Snapshot %s is available with provisioning state: %s", snapshotName, state) + return nil + } + if state == "Failed" { + return fmt.Errorf("snapshot provisioning failed with state: %s", state) + } + log.Printf("Snapshot %s provisioning state: %s (attempt %d/%d), waiting...", snapshotName, state, attempt, maxAttempts) + time.Sleep(pollInterval) + continue + } + + log.Printf("Snapshot %s is available", snapshotName) + return nil + } + + return fmt.Errorf("timeout waiting for snapshot %s to be available after %d attempts", snapshotName, maxAttempts) +} + +// deleteSnapshot deletes an Azure snapshot +func deleteSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, resourceGroupName, snapshotName string) error { + poller, err := client.BeginDelete(ctx, resourceGroupName, snapshotName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound { + log.Printf("Snapshot %s not found, skipping deletion", snapshotName) + return nil + } + return fmt.Errorf("failed to begin deleting snapshot: %w", err) + } + + _, err = poller.PollUntilDone(ctx, nil) + if err != nil { + return fmt.Errorf("failed to delete snapshot: %w", err) + } + + log.Printf("Snapshot %s deleted successfully", snapshotName) + return nil +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 7cbe33ae..242ec381 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -258,6 +258,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create gallery application versions client: %w", err) } + snapshotsClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create snapshots client: %w", err) + } + // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { adapters = append(adapters, @@ -405,10 +410,14 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), resourceGroupScopes, ), cache), - sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( - clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), - resourceGroupScopes, - ), cache), + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotsClient), + resourceGroupScopes, + ), cache), ) } @@ -461,6 +470,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/compute-disk.go b/sources/azure/manual/compute-disk.go index fc9413e2..3b793324 100644 --- a/sources/azure/manual/compute-disk.go +++ b/sources/azure/manual/compute-disk.go @@ -451,7 +451,7 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri allParts := strings.Split(strings.Trim(communityGalleryImageID, "/"), "/") communityGalleryName := "" for i, part := range allParts { - if part == "CommunityGalleries" && i+1 < len(allParts) { + if strings.EqualFold(part, "CommunityGalleries") && i+1 < len(allParts) { communityGalleryName = allParts[i+1] break } diff --git a/sources/azure/manual/compute-snapshot.go b/sources/azure/manual/compute-snapshot.go new file mode 100644 index 00000000..a5f3397f --- /dev/null +++ b/sources/azure/manual/compute-snapshot.go @@ -0,0 +1,751 @@ +package manual + +import ( + "context" + "errors" + "net" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ComputeSnapshotLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeSnapshot) + +type computeSnapshotWrapper struct { + client clients.SnapshotsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeSnapshot(client clients.SnapshotsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeSnapshotWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.ComputeSnapshot, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/list-by-resource-group +func (c computeSnapshotWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, snapshot := range page.Value { + if snapshot.Name == nil { + continue + } + item, sdpErr := c.azureSnapshotToSDPItem(snapshot, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeSnapshotWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, snapshot := range page.Value { + if snapshot.Name == nil { + continue + } + item, sdpErr := c.azureSnapshotToSDPItem(snapshot, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/get +func (c computeSnapshotWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be at least 1 and be the snapshot name"), scope, c.Type()) + } + snapshotName := queryParts[0] + if snapshotName == "" { + return nil, azureshared.QueryError(errors.New("snapshotName cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + result, err := c.client.Get(ctx, rgScope.ResourceGroup, snapshotName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureSnapshotToSDPItem(&result.Snapshot, scope) +} + +func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snapshot, scope string) (*sdp.Item, *sdp.QueryError) { + if snapshot.Name == nil { + return nil, azureshared.QueryError(errors.New("snapshot name is nil"), scope, c.Type()) + } + attributes, err := shared.ToAttributesWithExclude(snapshot, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeSnapshot.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(snapshot.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Health status from ProvisioningState + if snapshot.Properties != nil && snapshot.Properties.ProvisioningState != nil { + switch *snapshot.Properties.ProvisioningState { + case "Succeeded": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "Creating", "Updating", "Deleting": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "Failed", "Canceled": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to Disk Access from Properties.DiskAccessID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-accesses/get + if snapshot.Properties != nil && snapshot.Properties.DiskAccessID != nil && *snapshot.Properties.DiskAccessID != "" { + diskAccessName := azureshared.ExtractResourceName(*snapshot.Properties.DiskAccessID) + if diskAccessName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.DiskAccessID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskAccess.String(), + Method: sdp.QueryMethod_GET, + Query: diskAccessName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Disk Access is deleted/modified → snapshot private endpoint access is affected + Out: false, // If snapshot is deleted → Disk Access remains + }, + }) + } + } + + // Link to Disk Encryption Set from Properties.Encryption.DiskEncryptionSetID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-encryption-sets/get + if snapshot.Properties != nil && snapshot.Properties.Encryption != nil && snapshot.Properties.Encryption.DiskEncryptionSetID != nil && *snapshot.Properties.Encryption.DiskEncryptionSetID != "" { + encryptionSetName := azureshared.ExtractResourceName(*snapshot.Properties.Encryption.DiskEncryptionSetID) + if encryptionSetName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.Encryption.DiskEncryptionSetID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: encryptionSetName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Disk Encryption Set is deleted/modified → snapshot encryption is affected + Out: false, // If snapshot is deleted → Disk Encryption Set remains + }, + }) + } + } + + // Link to Disk Encryption Set from Properties.SecurityProfile.SecureVMDiskEncryptionSetID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disk-encryption-sets/get + if snapshot.Properties != nil && snapshot.Properties.SecurityProfile != nil && snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID != nil && *snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID != "" { + encryptionSetName := azureshared.ExtractResourceName(*snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID) + if encryptionSetName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*snapshot.Properties.SecurityProfile.SecureVMDiskEncryptionSetID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDiskEncryptionSet.String(), + Method: sdp.QueryMethod_GET, + Query: encryptionSetName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Disk Encryption Set is deleted/modified → snapshot encryption is affected + Out: false, // If snapshot is deleted → Disk Encryption Set remains + }, + }) + } + } + + // Link to source resources from Properties.CreationData + if snapshot.Properties != nil && snapshot.Properties.CreationData != nil { + creationData := snapshot.Properties.CreationData + + // Link to source Disk or Snapshot from SourceResourceID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/disks/get + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/get + if creationData.SourceResourceID != nil && *creationData.SourceResourceID != "" { + sourceResourceID := *creationData.SourceResourceID + sourceResourceIDLower := strings.ToLower(sourceResourceID) + if strings.Contains(sourceResourceIDLower, "/disks/") { + diskName := azureshared.ExtractResourceName(sourceResourceID) + if diskName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(sourceResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDisk.String(), + Method: sdp.QueryMethod_GET, + Query: diskName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If source disk is deleted/modified → snapshot may be affected + Out: false, // If snapshot is deleted → source disk remains + }, + }) + } + } else if strings.Contains(sourceResourceIDLower, "/snapshots/") { + snapshotName := azureshared.ExtractResourceName(sourceResourceID) + if snapshotName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(sourceResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSnapshot.String(), + Method: sdp.QueryMethod_GET, + Query: snapshotName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If source snapshot is deleted/modified → this snapshot may be affected + Out: false, // If this snapshot is deleted → source snapshot remains + }, + }) + } + } + } + + // Link to Storage Account from StorageAccountID + // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/get-properties + if creationData.StorageAccountID != nil && *creationData.StorageAccountID != "" { + storageAccountName := azureshared.ExtractResourceName(*creationData.StorageAccountID) + if storageAccountName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*creationData.StorageAccountID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Storage Account is deleted/modified → snapshot import may fail + Out: false, // If snapshot is deleted → Storage Account remains + }, + }) + } + } + + // Link to Storage Account and DNS from SourceURI (blob URI used for Import) + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/snapshots/create-or-update + if creationData.SourceURI != nil && *creationData.SourceURI != "" { + sourceURI := *creationData.SourceURI + storageAccountName := azureshared.ExtractStorageAccountNameFromBlobURI(sourceURI) + if storageAccountName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Storage Account is deleted/modified → snapshot import blob becomes inaccessible + Out: false, // If snapshot is deleted → Storage Account remains + }, + }) + + containerName := azureshared.ExtractContainerNameFromBlobURI(sourceURI) + if containerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageBlobContainer.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(storageAccountName, containerName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If blob container is deleted/modified → snapshot import source is lost + Out: false, // If snapshot is deleted → blob container remains + }, + }) + } + } + + if strings.HasPrefix(sourceURI, "http://") || strings.HasPrefix(sourceURI, "https://") { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: sourceURI, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If HTTP endpoint is unavailable → snapshot import source is lost + Out: true, // Bidirectional: changes to either side may affect the other + }, + }) + } + + host := azureshared.ExtractDNSFromURL(sourceURI) + if host != "" { + if net.ParseIP(host) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: host, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + } + + // Link to Image from ImageReference.ID + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/images/get + if creationData.ImageReference != nil && creationData.ImageReference.ID != nil && *creationData.ImageReference.ID != "" { + imageID := *creationData.ImageReference.ID + imageIDLower := strings.ToLower(imageID) + if strings.Contains(imageIDLower, "/images/") && !strings.Contains(imageIDLower, "/galleries/") { + imageName := azureshared.ExtractResourceName(imageID) + if imageName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(imageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeImage.String(), + Method: sdp.QueryMethod_GET, + Query: imageName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Image is deleted/modified → snapshot created from image may be affected + Out: false, // If snapshot is deleted → Image remains + }, + }) + } + } + } + + // Link to Gallery Image from GalleryImageReference + // Reference: https://learn.microsoft.com/en-us/rest/api/compute/gallery-images/get + if creationData.GalleryImageReference != nil { + if creationData.GalleryImageReference.ID != nil && *creationData.GalleryImageReference.ID != "" { + galleryImageID := *creationData.GalleryImageReference.ID + parts := azureshared.ExtractPathParamsFromResourceID(galleryImageID, []string{"galleries", "images", "versions"}) + if len(parts) >= 3 { + galleryName := parts[0] + imageName := parts[1] + version := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(galleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, imageName, version), + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Gallery Image is deleted/modified → snapshot created from image may be affected + Out: false, // If snapshot is deleted → Gallery Image remains + }, + }) + } + } + + if creationData.GalleryImageReference.SharedGalleryImageID != nil && *creationData.GalleryImageReference.SharedGalleryImageID != "" { + sharedGalleryImageID := *creationData.GalleryImageReference.SharedGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(sharedGalleryImageID, []string{"galleries", "images", "versions"}) + if len(parts) >= 3 { + galleryName := parts[0] + imageName := parts[1] + version := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(sharedGalleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, imageName, version), + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Gallery Image is deleted/modified → snapshot created from image may be affected + Out: false, // If snapshot is deleted → Gallery Image remains + }, + }) + } + } + + if creationData.GalleryImageReference.CommunityGalleryImageID != nil && *creationData.GalleryImageReference.CommunityGalleryImageID != "" { + communityGalleryImageID := *creationData.GalleryImageReference.CommunityGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(communityGalleryImageID, []string{"Images", "Versions"}) + if len(parts) >= 2 { + imageName := parts[0] + version := parts[1] + allParts := strings.Split(strings.Trim(communityGalleryImageID, "/"), "/") + communityGalleryName := "" + for i, part := range allParts { + if strings.EqualFold(part, "CommunityGalleries") && i+1 < len(allParts) { + communityGalleryName = allParts[i+1] + break + } + } + if communityGalleryName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(communityGalleryImageID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeCommunityGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Community Gallery Image is deleted/modified → snapshot may be affected + Out: false, // If snapshot is deleted → Community Gallery Image remains + }, + }) + } + } + } + } + + // Link to Elastic SAN Volume Snapshot from ElasticSanResourceID + // Reference: https://learn.microsoft.com/en-us/rest/api/elasticsan/volume-snapshots/get + if creationData.ElasticSanResourceID != nil && *creationData.ElasticSanResourceID != "" { + elasticSanResourceID := *creationData.ElasticSanResourceID + parts := azureshared.ExtractPathParamsFromResourceID(elasticSanResourceID, []string{"elasticSans", "volumegroups", "snapshots"}) + if len(parts) >= 3 { + elasticSanName := parts[0] + volumeGroupName := parts[1] + esSnapshotName := parts[2] + extractedScope := azureshared.ExtractScopeFromResourceID(elasticSanResourceID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ElasticSanVolumeSnapshot.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(elasticSanName, volumeGroupName, esSnapshotName), + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Elastic SAN snapshot is deleted/modified → this snapshot may be affected + Out: false, // If this snapshot is deleted → Elastic SAN snapshot remains + }, + }) + } + } + } + + // Link to Key Vault resources from EncryptionSettingsCollection + // Reference: https://learn.microsoft.com/en-us/rest/api/keyvault/vaults/get + if snapshot.Properties != nil && snapshot.Properties.EncryptionSettingsCollection != nil && snapshot.Properties.EncryptionSettingsCollection.EncryptionSettings != nil { + for _, encryptionSetting := range snapshot.Properties.EncryptionSettingsCollection.EncryptionSettings { + if encryptionSetting == nil { + continue + } + + // Link to Key Vault from DiskEncryptionKey.SourceVault.ID + if encryptionSetting.DiskEncryptionKey != nil && encryptionSetting.DiskEncryptionKey.SourceVault != nil && encryptionSetting.DiskEncryptionKey.SourceVault.ID != nil && *encryptionSetting.DiskEncryptionKey.SourceVault.ID != "" { + vaultName := azureshared.ExtractResourceName(*encryptionSetting.DiskEncryptionKey.SourceVault.ID) + if vaultName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*encryptionSetting.DiskEncryptionKey.SourceVault.ID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: vaultName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Key Vault is deleted/modified → snapshot encryption key access is affected + Out: false, // If snapshot is deleted → Key Vault remains + }, + }) + } + } + + // Link to Key Vault Secret from DiskEncryptionKey.SecretURL + if encryptionSetting.DiskEncryptionKey != nil && encryptionSetting.DiskEncryptionKey.SecretURL != nil && *encryptionSetting.DiskEncryptionKey.SecretURL != "" { + secretURL := *encryptionSetting.DiskEncryptionKey.SecretURL + vaultName := azureshared.ExtractVaultNameFromURI(secretURL) + secretName := azureshared.ExtractSecretNameFromURI(secretURL) + if vaultName != "" && secretName != "" { + // Derive scope from the DiskEncryptionKey's SourceVault when available + secretScope := scope + if encryptionSetting.DiskEncryptionKey.SourceVault != nil && encryptionSetting.DiskEncryptionKey.SourceVault.ID != nil { + if extracted := azureshared.ExtractScopeFromResourceID(*encryptionSetting.DiskEncryptionKey.SourceVault.ID); extracted != "" { + secretScope = extracted + } + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultSecret.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vaultName, secretName), + Scope: secretScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Key Vault Secret is deleted/modified → snapshot encryption key is affected + Out: false, // If snapshot is deleted → Key Vault Secret remains + }, + }) + } + + secretHost := azureshared.ExtractDNSFromURL(secretURL) + if secretHost != "" { + if net.ParseIP(secretHost) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: secretHost, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: secretHost, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + } + + // Link to Key Vault from KeyEncryptionKey.SourceVault.ID + if encryptionSetting.KeyEncryptionKey != nil && encryptionSetting.KeyEncryptionKey.SourceVault != nil && encryptionSetting.KeyEncryptionKey.SourceVault.ID != nil && *encryptionSetting.KeyEncryptionKey.SourceVault.ID != "" { + vaultName := azureshared.ExtractResourceName(*encryptionSetting.KeyEncryptionKey.SourceVault.ID) + if vaultName != "" { + extractedScope := azureshared.ExtractScopeFromResourceID(*encryptionSetting.KeyEncryptionKey.SourceVault.ID) + if extractedScope == "" { + extractedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: vaultName, + Scope: extractedScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Key Vault is deleted/modified → key encryption key access is affected + Out: false, // If snapshot is deleted → Key Vault remains + }, + }) + } + } + + // Link to Key Vault Key from KeyEncryptionKey.KeyURL + if encryptionSetting.KeyEncryptionKey != nil && encryptionSetting.KeyEncryptionKey.KeyURL != nil && *encryptionSetting.KeyEncryptionKey.KeyURL != "" { + keyURL := *encryptionSetting.KeyEncryptionKey.KeyURL + vaultName := azureshared.ExtractVaultNameFromURI(keyURL) + keyName := azureshared.ExtractKeyNameFromURI(keyURL) + if vaultName != "" && keyName != "" { + // Derive scope from the KeyEncryptionKey's SourceVault when available + keyScope := scope + if encryptionSetting.KeyEncryptionKey.SourceVault != nil && encryptionSetting.KeyEncryptionKey.SourceVault.ID != nil { + if extracted := azureshared.ExtractScopeFromResourceID(*encryptionSetting.KeyEncryptionKey.SourceVault.ID); extracted != "" { + keyScope = extracted + } + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultKey.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vaultName, keyName), + Scope: keyScope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If Key Vault Key is deleted/modified → key encryption key is affected + Out: false, // If snapshot is deleted → Key Vault Key remains + }, + }) + } + + keyHost := azureshared.ExtractDNSFromURL(keyURL) + if keyHost != "" { + if net.ParseIP(keyHost) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: keyHost, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: keyHost, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + } + } + } + } + + return sdpItem, nil +} + +func (c computeSnapshotWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeSnapshotLookupByName, + } +} + +func (c computeSnapshotWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeDisk, + azureshared.ComputeSnapshot, + azureshared.ComputeDiskAccess, + azureshared.ComputeDiskEncryptionSet, + azureshared.ComputeImage, + azureshared.ComputeSharedGalleryImage, + azureshared.ComputeCommunityGalleryImage, + azureshared.StorageAccount, + azureshared.StorageBlobContainer, + azureshared.ElasticSanVolumeSnapshot, + azureshared.KeyVaultVault, + azureshared.KeyVaultSecret, + azureshared.KeyVaultKey, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/snapshot +func (c computeSnapshotWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_snapshot.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeSnapshotWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/snapshots/read", + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/compute +func (c computeSnapshotWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-snapshot_test.go b/sources/azure/manual/compute-snapshot_test.go new file mode 100644 index 00000000..f0a52877 --- /dev/null +++ b/sources/azure/manual/compute-snapshot_test.go @@ -0,0 +1,739 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func TestComputeSnapshot(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + + t.Run("Get", func(t *testing.T) { + snapshotName := "test-snapshot" + snapshot := createAzureSnapshot(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSnapshot.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSnapshot, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != snapshotName { + t.Errorf("Expected unique attribute value %s, got %s", snapshotName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + if sdpItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("Expected health OK, got %s", sdpItem.GetHealth()) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.DiskAccessID + ExpectedType: azureshared.ComputeDiskAccess.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-disk-access", + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + { + // Properties.Encryption.DiskEncryptionSetID + ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-des", + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + { + // Properties.CreationData.SourceResourceID (disk) + ExpectedType: azureshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-disk", + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSnapshotSource", func(t *testing.T) { + snapshotName := "test-snapshot-from-snapshot" + snapshot := createAzureSnapshotFromSnapshot(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceResourceID (snapshot) + ExpectedType: azureshared.ComputeSnapshot.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-snapshot", + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSourceURI", func(t *testing.T) { + snapshotName := "test-snapshot-from-blob" + snapshot := createAzureSnapshotFromBlobURI(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceURI → Storage Account + ExpectedType: azureshared.StorageAccount.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "teststorageaccount", + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + { + // Properties.CreationData.SourceURI → Blob Container + ExpectedType: azureshared.StorageBlobContainer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("teststorageaccount", "vhds"), + ExpectedScope: subscriptionID + "." + resourceGroup, + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: false, + }, + }, + { + // Properties.CreationData.SourceURI → HTTP + ExpectedType: stdlib.NetworkHTTP.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd", + ExpectedScope: "global", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + // Properties.CreationData.SourceURI → DNS + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "teststorageaccount.blob.core.windows.net", + ExpectedScope: "global", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithSourceURIUsingIPHost", func(t *testing.T) { + snapshotName := "test-snapshot-from-ip-blob" + snapshot := createAzureSnapshotFromIPBlobURI(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + // Properties.CreationData.SourceURI → HTTP + ExpectedType: stdlib.NetworkHTTP.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "https://10.0.0.1/vhds/my-disk.vhd", + ExpectedScope: "global", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + { + // Properties.CreationData.SourceURI → IP (host is IP address) + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.1", + ExpectedScope: "global", + ExpectedBlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + + // Verify no DNS link was emitted for the IP host + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == stdlib.NetworkDNS.String() { + t.Error("Expected no DNS link when SourceURI host is an IP address") + } + } + }) + + t.Run("GetWithEncryptionIPHosts", func(t *testing.T) { + snapshotName := "test-snapshot-encryption-ip" + snapshot := createAzureSnapshotWithEncryptionIPHosts(snapshotName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundSecretIPLink := false + foundKeyIPLink := false + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == stdlib.NetworkIP.String() { + if link.GetQuery().GetQuery() == "10.0.0.2" { + foundSecretIPLink = true + } + if link.GetQuery().GetQuery() == "10.0.0.3" { + foundKeyIPLink = true + } + if link.GetQuery().GetScope() != "global" { + t.Errorf("Expected IP scope 'global', got %s", link.GetQuery().GetScope()) + } + if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected IP method GET, got %s", link.GetQuery().GetMethod()) + } + } + if link.GetQuery().GetType() == stdlib.NetworkDNS.String() { + t.Error("Expected no DNS link when SecretURL/KeyURL hosts are IP addresses") + } + } + + if !foundSecretIPLink { + t.Error("Expected to find IP link for SecretURL host 10.0.0.2") + } + if !foundKeyIPLink { + t.Error("Expected to find IP link for KeyURL host 10.0.0.3") + } + }) + + t.Run("GetWithCrossResourceGroupLinks", func(t *testing.T) { + snapshotName := "test-snapshot-cross-rg" + snapshot := createAzureSnapshotWithCrossResourceGroupLinks(snapshotName, subscriptionID) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundDiskAccessLink := false + foundDiskLink := false + for _, link := range sdpItem.GetLinkedItemQueries() { + if link.GetQuery().GetType() == azureshared.ComputeDiskAccess.String() { + foundDiskAccessLink = true + expectedScope := subscriptionID + ".other-rg" + if link.GetQuery().GetScope() != expectedScope { + t.Errorf("Expected DiskAccess scope %s, got %s", expectedScope, link.GetQuery().GetScope()) + } + } + if link.GetQuery().GetType() == azureshared.ComputeDisk.String() { + foundDiskLink = true + expectedScope := subscriptionID + ".disk-rg" + if link.GetQuery().GetScope() != expectedScope { + t.Errorf("Expected Disk scope %s, got %s", expectedScope, link.GetQuery().GetScope()) + } + } + } + + if !foundDiskAccessLink { + t.Error("Expected to find Disk Access link") + } + if !foundDiskLink { + t.Error("Expected to find Disk link") + } + }) + + t.Run("GetWithoutLinks", func(t *testing.T) { + snapshotName := "test-snapshot-no-links" + snapshot := createAzureSnapshotWithoutLinks(snapshotName) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, snapshotName, nil).Return( + armcompute.SnapshotsClientGetResponse{ + Snapshot: *snapshot, + }, nil) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], snapshotName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if len(sdpItem.GetLinkedItemQueries()) != 0 { + t.Errorf("Expected no linked queries, got %d", len(sdpItem.GetLinkedItemQueries())) + } + }) + + t.Run("List", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshot2 := createAzureSnapshot("test-snapshot-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshot2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, wrapper.Scopes()[0], true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + + if item.GetTags()["env"] != "test" { + t.Fatalf("Expected tag 'env=test', got: %s", item.GetTags()["env"]) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshot2 := createAzureSnapshot("test-snapshot-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshot2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, wrapper.Scopes()[0], true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + + // Verify adapter doesn't support SearchStream + _, ok = adapter.(discovery.SearchStreamableAdapter) + if ok { + t.Fatalf("Adapter should not support SearchStream operation") + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) + snapshotNilName := &armcompute.Snapshot{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockPager := newMockSnapshotsPager(ctrl, []*armcompute.Snapshot{snapshot1, snapshotNilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, wrapper.Scopes()[0], true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("snapshot not found") + + mockClient := mocks.NewMockSnapshotsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-snapshot", nil).Return( + armcompute.SnapshotsClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-snapshot", true) + if qErr == nil { + t.Error("Expected error when getting non-existent snapshot, but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockSnapshotsClient(ctrl) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "", true) + if qErr == nil { + t.Error("Expected error when getting snapshot with empty name, but got nil") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSnapshotsClient(ctrl) + + wrapper := manual.NewComputeSnapshot(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when getting snapshot with insufficient query parts, but got nil") + } + }) +} + +// createAzureSnapshot creates a mock Azure Snapshot with linked resources for testing +func createAzureSnapshot(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + Encryption: &armcompute.Encryption{ + DiskEncryptionSetID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-des"), + }, + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/source-disk"), + }, + }, + } +} + +// createAzureSnapshotFromSnapshot creates a mock Snapshot that was copied from another snapshot +func createAzureSnapshotFromSnapshot(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/source-snapshot"), + }, + }, + } +} + +// createAzureSnapshotFromBlobURI creates a mock Snapshot imported from a blob URI +func createAzureSnapshotFromBlobURI(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), + SourceURI: to.Ptr("https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd"), + }, + }, + } +} + +// createAzureSnapshotFromIPBlobURI creates a mock Snapshot imported from a blob URI with an IP address host +func createAzureSnapshotFromIPBlobURI(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), + SourceURI: to.Ptr("https://10.0.0.1/vhds/my-disk.vhd"), + }, + }, + } +} + +// createAzureSnapshotWithEncryptionIPHosts creates a mock Snapshot with encryption settings using IP-based SecretURL and KeyURL +func createAzureSnapshotWithEncryptionIPHosts(name, subscriptionID, resourceGroup string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + }, + EncryptionSettingsCollection: &armcompute.EncryptionSettingsCollection{ + Enabled: to.Ptr(true), + EncryptionSettings: []*armcompute.EncryptionSettingsElement{ + { + DiskEncryptionKey: &armcompute.KeyVaultAndSecretReference{ + SourceVault: &armcompute.SourceVault{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + }, + SecretURL: to.Ptr("https://10.0.0.2/secrets/my-secret/version1"), + }, + KeyEncryptionKey: &armcompute.KeyVaultAndKeyReference{ + SourceVault: &armcompute.SourceVault{ + ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + }, + KeyURL: to.Ptr("https://10.0.0.3/keys/my-key/version1"), + }, + }, + }, + }, + }, + } +} + +// createAzureSnapshotWithCrossResourceGroupLinks creates a mock Snapshot with links to resources in different resource groups +func createAzureSnapshotWithCrossResourceGroupLinks(name, subscriptionID string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), + SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/disk-rg/providers/Microsoft.Compute/disks/source-disk"), + }, + }, + } +} + +// createAzureSnapshotWithoutLinks creates a mock Snapshot without any linked resources +func createAzureSnapshotWithoutLinks(name string) *armcompute.Snapshot { + return &armcompute.Snapshot{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.SnapshotProperties{ + ProvisioningState: to.Ptr("Succeeded"), + CreationData: &armcompute.CreationData{ + CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + }, + }, + } +} + +// mockSnapshotsPager is a simple mock implementation of the Pager interface for testing +type mockSnapshotsPager struct { + ctrl *gomock.Controller + items []*armcompute.Snapshot + index int + more bool +} + +func newMockSnapshotsPager(ctrl *gomock.Controller, items []*armcompute.Snapshot) clients.SnapshotsPager { + return &mockSnapshotsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockSnapshotsPager) More() bool { + return m.more +} + +func (m *mockSnapshotsPager) NextPage(ctx context.Context) (armcompute.SnapshotsClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.SnapshotsClientListByResourceGroupResponse{ + SnapshotList: armcompute.SnapshotList{ + Value: []*armcompute.Snapshot{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.SnapshotsClientListByResourceGroupResponse{ + SnapshotList: armcompute.SnapshotList{ + Value: []*armcompute.Snapshot{item}, + }, + }, nil +} diff --git a/sources/azure/shared/mocks/mock_snapshots_client.go b/sources/azure/shared/mocks/mock_snapshots_client.go new file mode 100644 index 00000000..bd7a9159 --- /dev/null +++ b/sources/azure/shared/mocks/mock_snapshots_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: snapshots-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_snapshots_client.go -package=mocks -source=snapshots-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSnapshotsClient is a mock of SnapshotsClient interface. +type MockSnapshotsClient struct { + ctrl *gomock.Controller + recorder *MockSnapshotsClientMockRecorder + isgomock struct{} +} + +// MockSnapshotsClientMockRecorder is the mock recorder for MockSnapshotsClient. +type MockSnapshotsClientMockRecorder struct { + mock *MockSnapshotsClient +} + +// NewMockSnapshotsClient creates a new mock instance. +func NewMockSnapshotsClient(ctrl *gomock.Controller) *MockSnapshotsClient { + mock := &MockSnapshotsClient{ctrl: ctrl} + mock.recorder = &MockSnapshotsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSnapshotsClient) EXPECT() *MockSnapshotsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSnapshotsClient) Get(ctx context.Context, resourceGroupName, snapshotName string, options *armcompute.SnapshotsClientGetOptions) (armcompute.SnapshotsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, snapshotName, options) + ret0, _ := ret[0].(armcompute.SnapshotsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSnapshotsClientMockRecorder) Get(ctx, resourceGroupName, snapshotName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSnapshotsClient)(nil).Get), ctx, resourceGroupName, snapshotName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockSnapshotsClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.SnapshotsClientListByResourceGroupOptions) clients.SnapshotsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.SnapshotsPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockSnapshotsClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockSnapshotsClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index fdc634a5..06c9a0dd 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -62,6 +62,10 @@ func ExtractResourceName(resourceID string) string { // For example, for input="/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/queueServices/default/queues/{queue}" // and keys=["storageAccounts", "queues"], it will return ["{account}", "{queue}"]. // +// Key matching is case-insensitive (Azure resource IDs are case-insensitive) but +// only matches at even-indexed path positions (structural key slots) to avoid +// misidentifying a resource name that happens to equal a key. +// // If a key is not found, the function will return nil. func ExtractPathParamsFromResourceID(resourceID string, keys []string) []string { if resourceID == "" || len(keys) == 0 { @@ -74,7 +78,11 @@ func ExtractPathParamsFromResourceID(resourceID string, keys []string) []string for _, key := range keys { found := false for i, part := range parts { - if part == key && i+1 < len(parts) { + // Azure resource IDs alternate key/value segments after trimming: + // key0/value0/key1/value1/... Keys are at even indices (0, 2, 4, ...), + // values at odd indices. Only match at key positions to prevent a + // resource name like "images" from being treated as a path key. + if i%2 == 0 && strings.EqualFold(part, key) && i+1 < len(parts) { results = append(results, parts[i+1]) found = true break diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index 4cf9bbb6..21126095 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -205,6 +205,36 @@ func TestExtractPathParamsFromResourceID(t *testing.T) { keys: []string{"storageAccounts", "queues"}, expected: []string{"test-storage-account_123", "test_queue-name"}, }, + { + name: "case-insensitive key matching - lowercase keys match uppercase segments", + resourceID: "/CommunityGalleries/test-gallery/Images/test-image/Versions/1.0.0", + keys: []string{"communitygalleries", "images", "versions"}, + expected: []string{"test-gallery", "test-image", "1.0.0"}, + }, + { + name: "case-insensitive key matching - uppercase keys match lowercase segments", + resourceID: "/communitygalleries/test-gallery/images/test-image/versions/1.0.0", + keys: []string{"CommunityGalleries", "Images", "Versions"}, + expected: []string{"test-gallery", "test-image", "1.0.0"}, + }, + { + name: "case-insensitive key matching - mixed case", + resourceID: "/subscriptions/12345678/resourcegroups/test-rg/providers/Microsoft.Storage/storageaccounts/myaccount", + keys: []string{"storageAccounts"}, + expected: []string{"myaccount"}, + }, + { + name: "value matching key name is not misidentified - gallery named 'images'", + resourceID: "/galleries/images/images/real-image/versions/1.0.0", + keys: []string{"images", "versions"}, + expected: []string{"real-image", "1.0.0"}, + }, + { + name: "value matching key name is not misidentified - gallery named 'versions'", + resourceID: "/galleries/versions/images/real-image/versions/1.0.0", + keys: []string{"images", "versions"}, + expected: []string{"real-image", "1.0.0"}, + }, } for _, tc := range tests { From bcd2fd99f0d1f648f747d31d0bef7c69eb09d236 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 17 Feb 2026 16:21:20 +0100 Subject: [PATCH 27/51] Cli direct change link (#3878) Remove `/blast-radius` suffix from change URLs in CLI output to provide direct links to changes. --- Linear Issue: [ENG-2479](https://linear.app/overmind/issue/ENG-2479/the-cli-should-show-direct-link-instead-of-blast-radius)

Open in Cursor Open in Web

--- > [!NOTE] > **Low Risk** > Simple URL formatting change in CLI output; no API, auth, or data-handling behavior is modified. > > **Overview** > CLI output links for newly created/updated Changes now point to the Change page directly (e.g. `/changes/`) instead of the blast radius view (`/changes//blast-radius`) in both `changes submit-plan` and `terraform plan` flows. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8cf47c471f78672ae5f1d7cf472125796b86bab7. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor Agent Co-authored-by: Dylan GitOrigin-RevId: 5ca2000d75f65f6ece56c3ce849bdc5319cb9707 --- cmd/changes_submit_plan.go | 2 +- cmd/terraform_plan.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index f47647d4..66f9a8b0 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -305,7 +305,7 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { } app, _ = strings.CutSuffix(app, "/") - changeUrl := fmt.Sprintf("%v/changes/%v/blast-radius", app, changeUUID) + changeUrl := fmt.Sprintf("%v/changes/%v", app, changeUUID) log.WithContext(ctx).WithFields(lf).WithField("change-url", changeUrl).Info("Change ready") fmt.Println(changeUrl) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index b6e10be1..ace728c4 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -327,7 +327,7 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI uploadPlannedChange.Success("Uploaded planned changes: Done") changeUrl := *oi.FrontendUrl - changeUrl.Path = fmt.Sprintf("%v/changes/%v/blast-radius", changeUrl.Path, changeUuid) + changeUrl.Path = fmt.Sprintf("%v/changes/%v", changeUrl.Path, changeUuid) log.WithField("change-url", changeUrl.String()).Info("Change ready") /////////////////////////////////////////////////////////////////// From 1b1038d9d73896d7e21058403816c9212b81e6fb Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Wed, 18 Feb 2026 11:28:53 +0100 Subject: [PATCH 28/51] [Tests] Remove ExpectedBlastPropagation from shared test helpers and all adapter tests (#3890) > [!NOTE] > **Medium Risk** > Mostly test-only churn, but the `compute-gallery-application-version` change can alter emitted linked-item queries (additional blob-container links), which may affect discovery graph behavior. > > **Overview** > **Linked-item query static tests no longer assert blast propagation.** `shared.QueryTest` drops `ExpectedBlastPropagation`, and `QueryTests.TestLinkedItems` stops comparing `LinkedItemQuery.BlastPropagation`. > > All impacted adapter tests and authoring docs are updated to remove `ExpectedBlastPropagation` expectations and adjust test case formatting/import ordering. Separately, `compute-gallery-application-version` fixes blob URI handling to always emit blob-container links even when the storage account link was already deduped. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b6f727548ca63304b210204e695ec9320d0d6488. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 30fcb25dee73310556972838172d8f3d923d10bd --- sources/azure/manual/README.md | 4 - .../manual/authorization-role-assignment.go | 2 - .../authorization-role-assignment_test.go | 21 +- sources/azure/manual/batch-batch-accounts.go | 4 +- .../azure/manual/batch-batch-accounts_test.go | 77 +------ .../manual/compute-availability-set_test.go | 21 +- ...compute-capacity-reservation-group_test.go | 36 ++-- .../compute-dedicated-host-group_test.go | 24 +-- .../azure/manual/compute-disk-access_test.go | 28 +-- .../compute-disk-encryption-set_test.go | 44 ---- sources/azure/manual/compute-disk_test.go | 133 ++---------- .../compute-gallery-application-version.go | 50 ++--- ...ompute-gallery-application-version_test.go | 38 ++-- sources/azure/manual/compute-image_test.go | 91 ++------ .../compute-proximity-placement-group.go | 2 +- .../compute-proximity-placement-group_test.go | 21 +- sources/azure/manual/compute-snapshot_test.go | 70 +------ .../compute-virtual-machine-extension_test.go | 7 +- ...ompute-virtual-machine-run-command_test.go | 7 +- .../compute-virtual-machine-scale-set_test.go | 161 ++------------ .../manual/compute-virtual-machine_test.go | 42 +--- .../manual/dbforpostgresql-database_test.go | 7 +- .../dbforpostgresql-flexible-server_test.go | 70 +------ .../documentdb-database-accounts_test.go | 56 +---- .../azure/manual/keyvault-managed-hsm_test.go | 84 ++------ sources/azure/manual/keyvault-secret_test.go | 21 +- sources/azure/manual/keyvault-vault.go | 1 - sources/azure/manual/keyvault-vault_test.go | 56 +---- ...gedidentity-user-assigned-identity_test.go | 7 +- .../network-application-gateway_test.go | 168 +++------------ sources/azure/manual/network-load-balancer.go | 5 +- .../manual/network-load-balancer_test.go | 77 +------ .../azure/manual/network-network-interface.go | 6 +- .../manual/network-network-interface_test.go | 70 +------ .../network-network-security-group_test.go | 49 +---- .../azure/manual/network-public-ip-address.go | 3 +- .../manual/network-public-ip-address_test.go | 35 +--- sources/azure/manual/network-route-table.go | 1 - .../azure/manual/network-route-table_test.go | 12 -- .../azure/manual/network-virtual-network.go | 4 +- .../manual/network-virtual-network_test.go | 49 +---- sources/azure/manual/network-zone_test.go | 42 +--- sources/azure/manual/sql-database_test.go | 35 +--- sources/azure/manual/sql-server_test.go | 196 +++--------------- sources/azure/manual/storage-account.go | 3 +- sources/azure/manual/storage-account_test.go | 63 +----- .../manual/storage-bucket-iam-policy_test.go | 18 +- sources/shared/testing.go | 25 +-- 48 files changed, 342 insertions(+), 1704 deletions(-) diff --git a/sources/azure/manual/README.md b/sources/azure/manual/README.md index fff9087b..ee64ddcc 100644 --- a/sources/azure/manual/README.md +++ b/sources/azure/manual/README.md @@ -110,10 +110,6 @@ t.Run("StaticTests", func(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, // ... more test cases } diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index 2c5a956f..f06584e2 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -14,7 +14,6 @@ import ( "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/go/discovery" ) - var AuthorizationRoleAssignmentLookupByName = shared.NewItemTypeLookup("name", azureshared.AuthorizationRoleAssignment) type authorizationRoleAssignmentWrapper struct { @@ -62,7 +61,6 @@ func (a authorizationRoleAssignmentWrapper) List(ctx context.Context, scope stri return items, nil } - func (a authorizationRoleAssignmentWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := a.ResourceGroupScopeFromScope(scope) if err != nil { diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index 6f719de0..ae5c5f92 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -74,12 +74,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "b24988ac-6180-42a0-ab88-20f7382dd24c", ExpectedScope: subscriptionID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -503,23 +498,13 @@ func TestAuthorizationRoleAssignment(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "b24988ac-6180-42a0-ab88-20f7382dd24c", ExpectedScope: subscriptionID, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Delegated Managed Identity link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index ddc8aceb..8f46c631 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/go/sdpcache" - "github.com/overmindtech/cli/go/discovery" ) var BatchAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchAccount) diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index 4c3622e4..8ecab3af 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -87,122 +87,67 @@ func TestBatchAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-account", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault link ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private Endpoint link ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // User Assigned Managed Identity link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Node Identity Reference link ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-node-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Applications (child resource) ExpectedType: azureshared.BatchBatchApplication.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Pools (child resource) ExpectedType: azureshared.BatchBatchPool.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Certificates (child resource) ExpectedType: azureshared.BatchBatchCertificate.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint Connections (child resource) ExpectedType: azureshared.BatchBatchPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Link Resources (child resource) ExpectedType: azureshared.BatchBatchPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Detectors (child resource) ExpectedType: azureshared.BatchBatchDetector.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-availability-set_test.go b/sources/azure/manual/compute-availability-set_test.go index f04d8f5c..12b22270 100644 --- a/sources/azure/manual/compute-availability-set_test.go +++ b/sources/azure/manual/compute-availability-set_test.go @@ -71,34 +71,19 @@ func TestComputeAvailabilitySet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ppg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.VirtualMachines[0].ID ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.VirtualMachines[1].ID ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go index 46afaff2..fa22c505 100644 --- a/sources/azure/manual/compute-capacity-reservation-group_test.go +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -91,32 +91,28 @@ func TestComputeCapacityReservationGroup(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ { - ExpectedType: azureshared.ComputeCapacityReservation.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: shared.CompositeLookupKey(groupName, "res-1"), - ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-1"), + ExpectedScope: scope, }, { - ExpectedType: azureshared.ComputeCapacityReservation.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: shared.CompositeLookupKey(groupName, "res-2"), - ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedType: azureshared.ComputeCapacityReservation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey(groupName, "res-2"), + ExpectedScope: scope, }, { - ExpectedType: azureshared.ComputeVirtualMachine.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "vm-1", - ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-1", + ExpectedScope: scope, }, { - ExpectedType: azureshared.ComputeVirtualMachine.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "vm-2", - ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedType: azureshared.ComputeVirtualMachine.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vm-2", + ExpectedScope: scope, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/azure/manual/compute-dedicated-host-group_test.go b/sources/azure/manual/compute-dedicated-host-group_test.go index a51042b0..5b24242f 100644 --- a/sources/azure/manual/compute-dedicated-host-group_test.go +++ b/sources/azure/manual/compute-dedicated-host-group_test.go @@ -95,22 +95,12 @@ func TestComputeDedicatedHostGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-1"), ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeDedicatedHost.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hostGroupName, "host-2"), ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) @@ -323,11 +313,11 @@ func createAzureDedicatedHostGroup(hostGroupName string) *armcompute.DedicatedHo "project": to.Ptr("testing"), }, Properties: &armcompute.DedicatedHostGroupProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), - SupportAutomaticPlacement: to.Ptr(false), - AdditionalCapabilities: nil, - Hosts: nil, - InstanceView: nil, + PlatformFaultDomainCount: to.Ptr(int32(2)), + SupportAutomaticPlacement: to.Ptr(false), + AdditionalCapabilities: nil, + Hosts: nil, + InstanceView: nil, }, } } diff --git a/sources/azure/manual/compute-disk-access_test.go b/sources/azure/manual/compute-disk-access_test.go index 213f68ed..9b042210 100644 --- a/sources/azure/manual/compute-disk-access_test.go +++ b/sources/azure/manual/compute-disk-access_test.go @@ -72,12 +72,7 @@ func TestComputeDiskAccess(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: diskAccessName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) @@ -107,34 +102,19 @@ func TestComputeDiskAccess(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: diskAccessName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Network Private Endpoint (same resource group) ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Network Private Endpoint (different resource group) ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-other-rg", ExpectedScope: subscriptionID + ".other-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) diff --git a/sources/azure/manual/compute-disk-encryption-set_test.go b/sources/azure/manual/compute-disk-encryption-set_test.go index e12ee497..2052400f 100644 --- a/sources/azure/manual/compute-disk-encryption-set_test.go +++ b/sources/azure/manual/compute-disk-encryption-set_test.go @@ -87,10 +87,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - Key Vault Key @@ -98,10 +94,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vault", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - DNS name @@ -109,10 +101,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Identity.UserAssignedIdentities[{id}] - User Assigned Identity @@ -120,10 +108,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, } @@ -155,10 +139,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - Key Vault Key @@ -166,10 +146,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vault", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.ActiveKey.KeyURL - DNS name @@ -177,10 +153,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Identity.UserAssignedIdentities[{id}] - User Assigned Identity @@ -188,10 +160,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].SourceVault.ID - Key Vault Vault @@ -199,10 +167,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-old-vault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].KeyURL - Key Vault Key @@ -210,10 +174,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-old-vault", "test-old-key"), ExpectedScope: subscriptionID + "." + resourceGroup, // Key Vault URI doesn't contain resource group, use DES scope - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Properties.PreviousKeys[].KeyURL - DNS name @@ -221,10 +181,6 @@ func TestComputeDiskEncryptionSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-old-vault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index eff2b580..e4666974 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -90,210 +90,115 @@ func TestComputeDisk(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ManagedByExtended[0] - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ShareInfo[0].VMURI - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm-3", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.DiskAccessID - Disk Access ExpectedType: azureshared.ComputeDiskAccess.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-access", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.Encryption.DiskEncryptionSetID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.SecurityProfile.SecureVMDiskEncryptionSetID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-secure-vm-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.SourceResourceID (Disk) - Source Disk ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.StorageAccountID - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-storage-account", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.ImageReference.ID - Image ExpectedType: azureshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-image", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.ID - Shared Gallery Image ExpectedType: azureshared.ComputeSharedGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-gallery", "test-gallery-image", "1.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.SharedGalleryImageID - Shared Gallery Image ExpectedType: azureshared.ComputeSharedGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-gallery-2", "test-gallery-image-2", "2.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.GalleryImageReference.CommunityGalleryImageID - Community Gallery Image ExpectedType: azureshared.ComputeCommunityGalleryImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-community-gallery", "test-community-image", "1.0.0"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.CreationData.ElasticSanResourceID - Elastic SAN Volume Snapshot ExpectedType: azureshared.ElasticSanVolumeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-elastic-san", "test-volume-group", "test-snapshot"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SourceVault.ID - Key Vault ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SecretURL - Key Vault Secret ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault", "test-secret"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].DiskEncryptionKey.SecretURL - DNS name ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.SourceVault.ID - Key Vault ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault-2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.KeyURL - Key Vault Key ExpectedType: azureshared.KeyVaultKey.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault-2", "test-key"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Properties.EncryptionSettingsCollection.EncryptionSettings[0].KeyEncryptionKey.KeyURL - DNS name ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault-2.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index 3aa5812a..711c4285 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -211,40 +211,40 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT } } if accountName := azureshared.ExtractStorageAccountNameFromBlobURI(link); accountName != "" { - if _, seen := seenStorageAccounts[accountName]; !seen { - seenStorageAccounts[accountName] = struct{}{} + if _, seen := seenStorageAccounts[accountName]; !seen { + seenStorageAccounts[accountName] = struct{}{} + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If storage account is unavailable → version artifact cannot be accessed (In: true) + Out: false, // If version is deleted → storage account remains (Out: false) + }, + }) + } + containerName := azureshared.ExtractContainerNameFromBlobURI(link) + if containerName != "" { + containerKey := shared.CompositeLookupKey(accountName, containerName) + if _, seen := seenBlobContainers[containerKey]; !seen { + seenBlobContainers[containerKey] = struct{}{} linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: azureshared.StorageAccount.String(), + Type: azureshared.StorageBlobContainer.String(), Method: sdp.QueryMethod_GET, - Query: accountName, + Query: containerKey, Scope: scope, }, BlastPropagation: &sdp.BlastPropagation{ - In: true, // If storage account is unavailable → version artifact cannot be accessed (In: true) - Out: false, // If version is deleted → storage account remains (Out: false) + In: true, // If blob container is unavailable → version artifact cannot be accessed (In: true) + Out: false, // If version is deleted → blob container remains (Out: false) }, }) } - containerName := azureshared.ExtractContainerNameFromBlobURI(link) - if containerName != "" { - containerKey := shared.CompositeLookupKey(accountName, containerName) - if _, seen := seenBlobContainers[containerKey]; !seen { - seenBlobContainers[containerKey] = struct{}{} - linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.StorageBlobContainer.String(), - Method: sdp.QueryMethod_GET, - Query: containerKey, - Scope: scope, - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If blob container is unavailable → version artifact cannot be accessed (In: true) - Out: false, // If version is deleted → blob container remains (Out: false) - }, - }) - } - } + } } } if src.MediaLink != nil && *src.MediaLink != "" { diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go index 28f1ad04..25716e24 100644 --- a/sources/azure/manual/compute-gallery-application-version_test.go +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -146,12 +146,12 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global"}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -177,15 +177,15 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "config"), ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/config/default.json", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}}, - {ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeGalleryApplication.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "mystorageaccount", ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "packages"), ExpectedScope: scope}, + {ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("mystorageaccount", "config"), ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/packages/app.zip", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "mystorageaccount.blob.core.windows.net", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://mystorageaccount.blob.core.windows.net/config/default.json", ExpectedScope: "global"}, + {ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", ExpectedScope: scope}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -425,12 +425,12 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { expected := map[shared.ItemType]bool{ azureshared.ComputeGallery: true, azureshared.ComputeGalleryApplication: true, - azureshared.ComputeDiskEncryptionSet: true, + azureshared.ComputeDiskEncryptionSet: true, azureshared.StorageAccount: true, azureshared.StorageBlobContainer: true, - stdlib.NetworkDNS: true, - stdlib.NetworkHTTP: true, - stdlib.NetworkIP: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, } for itemType, want := range expected { if got := links[itemType]; got != want { diff --git a/sources/azure/manual/compute-image_test.go b/sources/azure/manual/compute-image_test.go index 4d9b7035..dd3a0609 100644 --- a/sources/azure/manual/compute-image_test.go +++ b/sources/azure/manual/compute-image_test.go @@ -91,144 +91,79 @@ func TestComputeImage(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.Snapshot.ID - Compute Snapshot ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-snapshot", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.BlobURI - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // OSDisk.BlobURI - NetworkHTTP ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://teststorageaccount.blob.core.windows.net/vhds/osdisk.vhd", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OSDisk.BlobURI - NetworkDNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OSDisk.DiskEncryptionSet.ID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-os-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].ManagedDisk.ID - Compute Disk ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-disk-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].Snapshot.ID - Compute Snapshot ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-snapshot-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].BlobURI - Storage Account ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount2", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DataDisks[0].BlobURI - NetworkHTTP ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://teststorageaccount2.blob.core.windows.net/vhds/datadisk1.vhd", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DataDisks[0].BlobURI - NetworkDNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount2.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DataDisks[0].DiskEncryptionSet.ID - Disk Encryption Set ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-data-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SourceVirtualMachine.ID - Virtual Machine ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-source-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go index 5d07ffee..e6531884 100644 --- a/sources/azure/manual/compute-proximity-placement-group.go +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -13,11 +13,11 @@ import ( "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/go/discovery" ) - var ComputeProximityPlacementGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeProximityPlacementGroup) type computeProximityPlacementGroupWrapper struct { client clients.ProximityPlacementGroupsClient + *azureshared.MultiResourceGroupBase } diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go index a2663161..7e36dd32 100644 --- a/sources/azure/manual/compute-proximity-placement-group_test.go +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -70,32 +70,17 @@ func TestComputeProximityPlacementGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeAvailabilitySet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-avset", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeVirtualMachineScaleSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vmss", ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-snapshot_test.go b/sources/azure/manual/compute-snapshot_test.go index f0a52877..db9a434a 100644 --- a/sources/azure/manual/compute-snapshot_test.go +++ b/sources/azure/manual/compute-snapshot_test.go @@ -75,34 +75,19 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.ComputeDiskAccess.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-access", - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Properties.Encryption.DiskEncryptionSetID ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Properties.CreationData.SourceResourceID (disk) ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -134,12 +119,7 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-snapshot", - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -171,45 +151,25 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Properties.CreationData.SourceURI → Blob Container ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("teststorageaccount", "vhds"), - ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Properties.CreationData.SourceURI → HTTP ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd", - ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, + ExpectedScope: "global", }, { // Properties.CreationData.SourceURI → DNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", - ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, + ExpectedScope: "global", }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -241,23 +201,13 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://10.0.0.1/vhds/my-disk.vhd", - ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, + ExpectedScope: "global", }, { // Properties.CreationData.SourceURI → IP (host is IP address) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", - ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, + ExpectedScope: "global", }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index 5727a131..6b2dbf3c 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -70,12 +70,7 @@ func TestComputeVirtualMachineExtension(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vmName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-virtual-machine-run-command_test.go b/sources/azure/manual/compute-virtual-machine-run-command_test.go index 90d45c09..83b2c6af 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -150,12 +150,7 @@ func TestComputeVirtualMachineRunCommand(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vmName, ExpectedScope: scope, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-virtual-machine-scale-set_test.go b/sources/azure/manual/compute-virtual-machine-scale-set_test.go index 0495d589..62729dfa 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set_test.go @@ -75,254 +75,139 @@ func TestComputeVirtualMachineScaleSet(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(scaleSetName, "CustomScriptExtension"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // VM instances - always linked via SEARCH ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: scaleSetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Network Security Group ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Virtual Network ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vnet", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet - uses composite lookup key ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "default"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Public IP Prefix ExpectedType: azureshared.NetworkPublicIPPrefix.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pip-prefix", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer ExpectedType: azureshared.NetworkLoadBalancer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-lb", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer Backend Address Pool - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-backend-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Load Balancer Inbound NAT Pool - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerInboundNatPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-nat-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Application Gateway ExpectedType: azureshared.NetworkApplicationGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ag", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Application Gateway Backend Address Pool - uses composite lookup key ExpectedType: azureshared.NetworkApplicationGatewayBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-ag", "test-ag-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Application Security Group ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Load Balancer Health Probe - uses composite lookup key ExpectedType: azureshared.NetworkLoadBalancerProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-lb", "test-probe"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Disk Encryption Set (OS Disk) ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Disk Encryption Set (Data Disk) ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-encryption-set-data", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Image (custom image) ExpectedType: azureshared.ComputeImage.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-image", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Proximity Placement Group ExpectedType: azureshared.ComputeProximityPlacementGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ppg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Dedicated Host Group ExpectedType: azureshared.ComputeDedicatedHostGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-host-group", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Identity ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name (boot diagnostics storage URI) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Storage Account (boot diagnostics) ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (OS profile secrets) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (extension protected settings) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault-ext", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-virtual-machine_test.go b/sources/azure/manual/compute-virtual-machine_test.go index 3d8eb94a..a746365f 100644 --- a/sources/azure/manual/compute-virtual-machine_test.go +++ b/sources/azure/manual/compute-virtual-machine_test.go @@ -74,67 +74,37 @@ func TestComputeVirtualMachine(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "os-disk", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // dataDisks[0].managedDisk.id ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "data-disk-1", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // networkInterfaces[0].id ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // availabilitySet.id ExpectedType: azureshared.ComputeAvailabilitySet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-avset", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Resources[0] (VM Extension) - uses composite lookup key ExpectedType: azureshared.ComputeVirtualMachineExtension.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(vmName, "CustomScriptExtension"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Run commands - always linked via SEARCH ExpectedType: azureshared.ComputeVirtualMachineRunCommand.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vmName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/dbforpostgresql-database_test.go b/sources/azure/manual/dbforpostgresql-database_test.go index 322cc3d8..da9a097f 100644 --- a/sources/azure/manual/dbforpostgresql-database_test.go +++ b/sources/azure/manual/dbforpostgresql-database_test.go @@ -120,12 +120,7 @@ func TestDBforPostgreSQLDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index d21cd6b8..2850a92d 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -117,102 +117,52 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerAdministrator.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerReplica.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerMigration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerBackup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.DBforPostgreSQLFlexibleServerVirtualEndpoint.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index dfd3a1a2..4ebd8fb0 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -112,89 +112,49 @@ func TestDocumentDBDatabaseAccounts(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Key Vault (GET) ExpectedType: azureshared.KeyVaultVault.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-keyvault", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User-Assigned Managed Identity (SEARCH) - same resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: resourceGroup, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User-Assigned Managed Identity (SEARCH) - different resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "identity-rg", ExpectedScope: subscriptionID + ".identity-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index 287ecdac..af237d38 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -114,133 +114,73 @@ func TestKeyVaultManagedHSM(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hsmName, "test-pec-1"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // MHSM Private Endpoint Connection (GET) - child resource ExpectedType: azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(hsmName, "test-pec-2"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity (GET) - same resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity (GET) - different resource group ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity-diff-rg", ExpectedScope: subscriptionID + ".identity-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS (SEARCH) - from HsmURI ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: hsmName + ".managedhsm.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // HTTP (SEARCH) - from HsmURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://" + hsmName + ".managedhsm.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // IP (GET) - from NetworkACLs IPRules ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // IP (GET) - from NetworkACLs IPRules (CIDR range) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index a3201a34..0e8f23b1 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -123,34 +123,19 @@ func TestKeyVaultSecret(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: vaultName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → secret access and configuration are affected - Out: false, // If secret is deleted → Key Vault remains - }, - }, - { + }, { // stdlib.NetworkDNS from SecretURI hostname ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vaultName + ".vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkHTTP from SecretURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: fmt.Sprintf("https://%s.vault.azure.net/secrets/%s", vaultName, secretName), ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index 2f7f1a4c..c4fbaa93 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -65,7 +65,6 @@ func (k keyvaultVaultWrapper) List(ctx context.Context, scope string) ([]*sdp.It return items, nil } - func (k keyvaultVaultWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := k.ResourceGroupScopeFromScope(scope) if err != nil { diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index 1641d9a2..c4676778 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -113,89 +113,49 @@ func TestKeyVaultVault(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Private Endpoint (GET) - different resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-private-endpoint-diff-rg", ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet (GET) - same resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet (GET) - different resource group ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet-diff-rg", "test-subnet-diff-rg"), ExpectedScope: subscriptionID + ".different-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Managed HSM (GET) - different resource group ExpectedType: azureshared.KeyVaultManagedHSM.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-managed-hsm", ExpectedScope: subscriptionID + ".hsm-rg", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // stdlib.NetworkIP (GET) - from NetworkACLs IPRules ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "192.168.1.100", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkIP (GET) - from NetworkACLs IPRules (CIDR range) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.0/24", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // stdlib.NetworkHTTP (SEARCH) - from VaultURI ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://test-keyvault.vault.azure.net/", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/managedidentity-user-assigned-identity_test.go b/sources/azure/manual/managedidentity-user-assigned-identity_test.go index 59670209..219dd86f 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity_test.go @@ -71,12 +71,7 @@ func TestManagedIdentityUserAssignedIdentity(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: identityName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index da03b36a..17bd4d56 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -72,265 +72,145 @@ func TestNetworkApplicationGateway(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "gateway-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Subnet from GatewayIPConfiguration ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // VirtualNetwork from GatewayIPConfiguration subnet ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // FrontendIPConfiguration child resource ExpectedType: azureshared.NetworkApplicationGatewayFrontendIPConfiguration.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "frontend-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // PublicIPAddress external resource ExpectedType: azureshared.NetworkPublicIPAddress.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-public-ip", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private IP address link (standard library) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.2.0.5", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendAddressPool child resource ExpectedType: azureshared.NetworkApplicationGatewayBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "backend-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Backend IP address link (standard library) ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.1.4", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // HTTPListener child resource ExpectedType: azureshared.NetworkApplicationGatewayHTTPListener.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "http-listener"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendHTTPSettings child resource ExpectedType: azureshared.NetworkApplicationGatewayBackendHTTPSettings.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "backend-http-settings"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RequestRoutingRule child resource ExpectedType: azureshared.NetworkApplicationGatewayRequestRoutingRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "routing-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Probe child resource ExpectedType: azureshared.NetworkApplicationGatewayProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "health-probe"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // SSLCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewaySSLCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "ssl-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Key Vault Secret from SSLCertificate KeyVaultSecretID ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-keyvault", "test-secret"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name from SSLCertificate KeyVaultSecretID ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // URLPathMap child resource ExpectedType: azureshared.NetworkApplicationGatewayURLPathMap.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "url-path-map"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // AuthenticationCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewayAuthenticationCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "auth-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // TrustedRootCertificate child resource ExpectedType: azureshared.NetworkApplicationGatewayTrustedRootCertificate.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "trusted-root-cert"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Key Vault Secret from TrustedRootCertificate KeyVaultSecretID ExpectedType: azureshared.KeyVaultSecret.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-trusted-keyvault", "test-trusted-secret"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS name from TrustedRootCertificate KeyVaultSecretID ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "test-trusted-keyvault.vault.azure.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RewriteRuleSet child resource ExpectedType: azureshared.NetworkApplicationGatewayRewriteRuleSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "rewrite-rule-set"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // RedirectConfiguration child resource ExpectedType: azureshared.NetworkApplicationGatewayRedirectConfiguration.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(agName, "redirect-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // WAF Policy external resource ExpectedType: azureshared.NetworkApplicationGatewayWebApplicationFirewallPolicy.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-waf-policy", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // User Assigned Managed Identity external resource ExpectedType: azureshared.ManagedIdentityUserAssignedIdentity.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-identity", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index 804d8073..d5ba165d 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -7,14 +7,14 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/go/sdpcache" - "github.com/overmindtech/cli/go/discovery" ) var NetworkLoadBalancerLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkLoadBalancer) @@ -66,7 +66,6 @@ func (n networkLoadBalancerWrapper) List(ctx context.Context, scope string) ([]* return items, nil } - func (n networkLoadBalancerWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := n.ResourceGroupScopeFromScope(scope) if err != nil { diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index 70ebbe63..e20ead69 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -71,122 +71,67 @@ func TestNetworkLoadBalancer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "frontend-ip-config"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // PublicIPAddress external resource ExpectedType: azureshared.NetworkPublicIPAddress.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-public-ip", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet external resource ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Private IP address link (standard library) ExpectedType: "ip", ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.2.0.5", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // BackendAddressPool child resource ExpectedType: azureshared.NetworkLoadBalancerBackendAddressPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "backend-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // InboundNatRule child resource ExpectedType: azureshared.NetworkLoadBalancerInboundNatRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "inbound-nat-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkInterface via InboundNatRule BackendIPConfiguration ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // LoadBalancingRule child resource ExpectedType: azureshared.NetworkLoadBalancerLoadBalancingRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "lb-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Probe child resource ExpectedType: azureshared.NetworkLoadBalancerProbe.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "probe"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // OutboundRule child resource ExpectedType: azureshared.NetworkLoadBalancerOutboundRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "outbound-rule"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // InboundNatPool child resource ExpectedType: azureshared.NetworkLoadBalancerInboundNatPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(lbName, "nat-pool"), ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 91e3bf79..de75607c 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/go/sdpcache" - "github.com/overmindtech/cli/go/discovery" ) var NetworkNetworkInterfaceLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNetworkInterface) @@ -60,7 +60,6 @@ func (n networkNetworkInterfaceWrapper) List(ctx context.Context, scope string) return items, nil } - func (n networkNetworkInterfaceWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { rgScope, err := n.ResourceGroupScopeFromScope(scope) if err != nil { @@ -88,6 +87,7 @@ func (n networkNetworkInterfaceWrapper) ListStream(ctx context.Context, stream d } } } + // reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/network-interfaces/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP#response func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkInterface *armnetwork.Interface) (*sdp.Item, *sdp.QueryError) { if networkInterface.Name == nil { diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index 62cabf0c..a7f632c5 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -71,45 +71,25 @@ func TestNetworkNetworkInterface(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // ComputeVirtualMachine link ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // NetworkNetworkSecurityGroup link ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkNetworkInterfaceTapConfiguration link (child resource) ExpectedType: azureshared.NetworkNetworkInterfaceTapConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -137,62 +117,32 @@ func TestNetworkNetworkInterface(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-vm", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nsg", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNetworkInterfaceTapConfiguration.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nicName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "dns.internal", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index ff2e7bd5..e79071fd 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -71,78 +71,43 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(nsgName, "test-security-rule"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DefaultSecurityRule link ExpectedType: azureshared.NetworkDefaultSecurityRule.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(nsgName, "AllowVnetInBound"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Subnet link ExpectedType: azureshared.NetworkSubnet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkInterface link ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // ApplicationSecurityGroup link (from SecurityRule Source) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-source", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ApplicationSecurityGroup link (from SecurityRule Destination) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-dest", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // ApplicationSecurityGroup link (from DefaultSecurityRule Source) ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-asg-default-source", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index fc8be72a..85028dd1 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" - "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -94,6 +94,7 @@ func (n networkPublicIPAddressWrapper) ListStream(ctx context.Context, stream di } } } + // reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/public-ip-addresses/get?view=rest-virtualnetwork-2025-03-01&tabs=HTTP // GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/publicIPAddresses/{publicIpAddressName}?api-version=2025-03-01 func (n networkPublicIPAddressWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index c69f1680..f99f6bdb 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -71,56 +71,31 @@ func TestNetworkPublicIPAddress(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "203.0.113.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // NetworkNetworkInterface link (via IPConfiguration) ExpectedType: azureshared.NetworkNetworkInterface.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nic", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkPublicIPPrefix link ExpectedType: azureshared.NetworkPublicIPPrefix.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-prefix", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkNatGateway link ExpectedType: azureshared.NetworkNatGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nat-gateway", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // NetworkDdosProtectionPlan link ExpectedType: azureshared.NetworkDdosProtectionPlan.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-ddos-plan", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 0f55f8a1..2ec4d7bd 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -14,7 +14,6 @@ import ( "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/go/discovery" ) - var NetworkRouteTableLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkRouteTable) type networkRouteTableWrapper struct { diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 0c771346..56dbdb73 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -71,10 +71,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(routeTableName, "test-route"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { // Route with NextHopIPAddress link (IP address to stdlib) @@ -82,10 +78,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { // Subnet link (external resource) @@ -93,10 +85,6 @@ func TestNetworkRouteTable(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index b20da836..169f838a 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -5,14 +5,14 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" - "github.com/overmindtech/cli/go/sdpcache" - "github.com/overmindtech/cli/go/discovery" ) var NetworkVirtualNetworkLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetwork) diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index 6e849e89..880b3c33 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -71,23 +71,13 @@ func TestNetworkVirtualNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // NetworkVirtualNetworkPeering link ExpectedType: azureshared.NetworkVirtualNetworkPeering.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -118,52 +108,27 @@ func TestNetworkVirtualNetwork(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkVirtualNetworkPeering.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vnetName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.NetworkNatGateway.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-nat-gateway", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: stdlib.NetworkIP.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "10.0.0.1", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "dns.internal", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) }) diff --git a/sources/azure/manual/network-zone_test.go b/sources/azure/manual/network-zone_test.go index e75323bc..c6a8d399 100644 --- a/sources/azure/manual/network-zone_test.go +++ b/sources/azure/manual/network-zone_test.go @@ -72,67 +72,37 @@ func TestNetworkZone(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: zoneName, ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // Virtual Network from RegistrationVirtualNetworks ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-reg-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // Virtual Network from ResolutionVirtualNetworks ExpectedType: azureshared.NetworkVirtualNetwork.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-res-vnet", ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // DNS Record Set (child resource) ExpectedType: azureshared.NetworkDNSRecordSet.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: zoneName, ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS name server (standard library) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "ns1.example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS name server (standard library) ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "ns2.example.com", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index dba27b27..e043a58f 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -120,23 +120,13 @@ func TestSqlDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLDatabaseSchema child resource link ExpectedType: azureshared.SQLDatabaseSchema.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(serverName, databaseName), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -170,34 +160,19 @@ func TestSqlDatabase(t *testing.T) { ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLElasticPool link ExpectedType: azureshared.SQLElasticPool.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-pool", ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { // SQLDatabaseSchema child resource link ExpectedType: azureshared.SQLDatabaseSchema.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(serverName, databaseName), ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index 3f698a00..3f069802 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -118,283 +118,143 @@ func TestSqlServer(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLElasticPool.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerVirtualNetworkRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerKey.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerFailoverGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdministrator.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSyncGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSyncAgent.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerPrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAuditingSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerSecurityAlertPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerVulnerabilityAssessment.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerEncryptionProtector.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerBlobAuditingPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAutomaticTuning.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdvancedThreatProtectionSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerDnsAlias.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerUsage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerOperation.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerAdvisor.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerBackupLongTermRetentionPolicy.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerDevOpsAuditSetting.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerTrustGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerOutboundFirewallRule.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { ExpectedType: azureshared.SQLServerPrivateLinkResource.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, - }, - { + }, { ExpectedType: azureshared.SQLLongTermRetentionBackup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // DNS name link (from FullyQualifiedDomainName) ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: serverName + ".database.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index 7c076319..8430bfa0 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -14,7 +14,6 @@ import ( "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" ) - var StorageAccountLookupByName = shared.NewItemTypeLookup("name", azureshared.StorageAccount) type storageAccountWrapper struct { @@ -89,7 +88,7 @@ func (s storageAccountWrapper) ListStream(ctx context.Context, stream discovery. cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) stream.SendItem(item) } -} + } } func (s storageAccountWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 00e66b16..99a77918 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -69,100 +69,55 @@ func TestStorageAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage file share link ExpectedType: azureshared.StorageFileShare.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage table link ExpectedType: azureshared.StorageTable.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage queue link ExpectedType: azureshared.StorageQueue.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, - }, - { + }, { // Storage private endpoint connection link (child resource) ExpectedType: azureshared.StoragePrivateEndpointConnection.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Blob ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".blob.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Queue ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".queue.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.Table ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".table.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - { + }, { // DNS link from PrimaryEndpoints.File ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName + ".file.core.windows.net", ExpectedScope: "global", - ExpectedBlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }, - } + }} shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/gcp/manual/storage-bucket-iam-policy_test.go b/sources/gcp/manual/storage-bucket-iam-policy_test.go index 51da30ad..13263489 100644 --- a/sources/gcp/manual/storage-bucket-iam-policy_test.go +++ b/sources/gcp/manual/storage-bucket-iam-policy_test.go @@ -79,15 +79,13 @@ func TestStorageBucketIAMPolicy_Get(t *testing.T) { ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: bucketName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedScope: projectID, }, { ExpectedType: gcpshared.IAMServiceAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "siem-sa@test-project.iam.gserviceaccount.com", - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + ExpectedScope: projectID, }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -123,29 +121,25 @@ func TestStorageBucketIAMPolicy_Get_ProjectPrincipalMembers_Linked(t *testing.T) ExpectedType: gcpshared.StorageBucket.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: bucketName, - ExpectedScope: projectID, - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + ExpectedScope: projectID, }, { ExpectedType: gcpshared.ComputeProject.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "other-project", - ExpectedScope: "other-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + ExpectedScope: "other-project", }, { ExpectedType: gcpshared.ComputeProject.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "another-project", - ExpectedScope: "another-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + ExpectedScope: "another-project", }, { ExpectedType: gcpshared.ComputeProject.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "bucket-project", - ExpectedScope: "bucket-project", - ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}, + ExpectedScope: "bucket-project", }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) diff --git a/sources/shared/testing.go b/sources/shared/testing.go index d1898125..342c318f 100644 --- a/sources/shared/testing.go +++ b/sources/shared/testing.go @@ -62,11 +62,10 @@ func ValidateAdapter(t *testing.T, adapter discovery.Adapter) { // QueryTest is a struct that defines the expected properties of a linked item query. type QueryTest struct { - ExpectedType string - ExpectedMethod sdp.QueryMethod - ExpectedQuery string - ExpectedScope string - ExpectedBlastPropagation *sdp.BlastPropagation + ExpectedType string + ExpectedMethod sdp.QueryMethod + ExpectedQuery string + ExpectedScope string } type QueryTests []QueryTest @@ -112,22 +111,6 @@ func (i QueryTests) TestLinkedItems(t *testing.T, item *sdp.Item) { if test.ExpectedMethod != gotLiq.GetQuery().GetMethod() { t.Errorf("for the linked item query %s of %s, expected method %s, got %s", test.ExpectedQuery, test.ExpectedType, test.ExpectedMethod, gotLiq.GetQuery().GetMethod()) } - - if test.ExpectedBlastPropagation == nil { - continue - } - - if gotLiq.GetBlastPropagation() == nil { - t.Fatalf("for the linked item query %s of %s, expected blast propagation to be non-nil", test.ExpectedQuery, test.ExpectedType) - } - - if test.ExpectedBlastPropagation.GetIn() != gotLiq.GetBlastPropagation().GetIn() { - t.Errorf("for the linked item query %s of %s, expected blast propagation [IN] to be %v, got %v", test.ExpectedQuery, test.ExpectedType, test.ExpectedBlastPropagation.GetIn(), gotLiq.GetBlastPropagation().GetIn()) - } - - if test.ExpectedBlastPropagation.GetOut() != gotLiq.GetBlastPropagation().GetOut() { - t.Errorf("for the linked item query %s of %s, expected blast propagation [OUT] to be %v, got %v", test.ExpectedQuery, test.ExpectedType, test.ExpectedBlastPropagation.GetOut(), gotLiq.GetBlastPropagation().GetOut()) - } } } From a8ae45d0f76f6255d9b8fa20d4c88d5829190966 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Wed, 18 Feb 2026 13:03:41 +0000 Subject: [PATCH 29/51] Add EC2 transit gateway adapter and supporting helper adapters (#3898) image image --- > [!NOTE] > **Medium Risk** > Introduces multiple new EC2 discovery paths that can increase API usage and surface edge cases (notably TGW route listing is capped by AWS at 1000 results per table). Integration tests create real AWS networking resources and must be run with care to avoid cost/cleanup issues. > > **Overview** > Adds new EC2 Transit Gateway resource coverage by introducing adapters for `ec2-transit-gateway-route-table`, `ec2-transit-gateway-route-table-association`, `ec2-transit-gateway-route-table-propagation`, and `ec2-transit-gateway-route`, including composite-ID query parsing (supports both `|` and Terraform-style `_`) and graph linking between route tables, attachments, and related resources. > > Wires these adapters into `aws-source/proc` so they are initialized with other EC2 adapters, and adds a full integration-test suite that creates and tears down real TGW infrastructure (TGW, VPC/subnet, VPC attachment, static route). Updates `aws-source/README.md` with how to run the new integration tests and adds corresponding type documentation + metadata JSON entries (including documenting `ec2-managed-prefix-list`, `ec2-transit-gateway-attachment`, and `ec2-transit-gateway-route-table-announcement` link targets). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6efe1f2154555ce7dca02aef6968dae412f743ef. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor GitOrigin-RevId: ca50c901eb8128e28f2c45cd4d82787829812e88 --- ...transit-gateway-route-table-association.go | 261 +++++++++++++++ ...it-gateway-route-table-association_test.go | 64 ++++ ...transit-gateway-route-table-propagation.go | 259 +++++++++++++++ ...it-gateway-route-table-propagation_test.go | 60 ++++ .../ec2-transit-gateway-route-table.go | 125 +++++++ .../ec2-transit-gateway-route-table_test.go | 111 +++++++ .../adapters/ec2-transit-gateway-route.go | 309 ++++++++++++++++++ .../ec2-transit-gateway-route_test.go | 68 ++++ .../integration/ec2-transit-gateway/client.go | 17 + .../ec2-transit-gateway/main_test.go | 95 ++++++ .../integration/ec2-transit-gateway/setup.go | 243 ++++++++++++++ .../ec2-transit-gateway/teardown.go | 181 ++++++++++ ...it_gateway_route_table_association_test.go | 62 ++++ ...it_gateway_route_table_propagation_test.go | 62 ++++ .../transit_gateway_route_table_test.go | 111 +++++++ .../transit_gateway_route_test.go | 62 ++++ aws-source/proc/proc.go | 4 + .../aws/Types/ec2-managed-prefix-list.md | 18 + .../Types/ec2-transit-gateway-attachment.md | 20 ++ ...ransit-gateway-route-table-announcement.md | 12 + ...transit-gateway-route-table-association.md | 40 +++ ...transit-gateway-route-table-propagation.md | 44 +++ .../Types/ec2-transit-gateway-route-table.md | 36 ++ .../aws/Types/ec2-transit-gateway-route.md | 52 +++ .../sources/aws/Types/ec2-transit-gateway.md | 18 + .../sources/aws/Types/ec2-vpn-connection.md | 18 + ...ansit-gateway-route-table-association.json | 1 + ...ansit-gateway-route-table-propagation.json | 1 + .../data/ec2-transit-gateway-route-table.json | 1 + .../aws/data/ec2-transit-gateway-route.json | 1 + 30 files changed, 2356 insertions(+) create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table-association.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table-association_test.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table-propagation.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route-table_test.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route.go create mode 100644 aws-source/adapters/ec2-transit-gateway-route_test.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/client.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/main_test.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/setup.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/teardown.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go create mode 100644 aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md create mode 100644 docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json create mode 100644 docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-association.go b/aws-source/adapters/ec2-transit-gateway-route-table-association.go new file mode 100644 index 00000000..3c6c9255 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association.go @@ -0,0 +1,261 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then fetch associations per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - GetTransitGatewayRouteTableAssociations — list associations for a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTableAssociations.html + +// transitGatewayRouteTableAssociationItem holds an association plus its route table ID for unique identification. +type transitGatewayRouteTableAssociationItem struct { + RouteTableID string + Association types.TransitGatewayRouteTableAssociation +} + +const associationIDSep = "|" + +func transitGatewayRouteTableAssociationID(routeTableID, attachmentID string) string { + return routeTableID + associationIDSep + attachmentID +} + +// parseCompositeID splits query by the given separator; accepts both `|` and `_` (Terraform uses `_`). +// Returns (left, right); empty left means invalid. +func parseCompositeID(query, sep string) (string, string) { + parts := strings.SplitN(query, sep, 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "" + } + return parts[0], parts[1] +} + +func parseAssociationQuery(query string) (routeTableID, attachmentID string, err error) { + if a, b := parseCompositeID(query, associationIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|TransitGatewayAttachmentId") +} + +func getTransitGatewayRouteTableAssociation(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteTableAssociationItem, error) { + routeTableID, attachmentID, err := parseAssociationQuery(query) + if err != nil { + return nil, err + } + pg := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.Associations { + a := &out.Associations[i] + if a.TransitGatewayAttachmentId != nil && *a.TransitGatewayAttachmentId == attachmentID { + return &transitGatewayRouteTableAssociationItem{RouteTableID: routeTableID, Association: *a}, nil + } + } + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("association %s not found", query), + } +} + +func listTransitGatewayRouteTableAssociations(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteTableAssociationItem, error) { + // List all route tables, then get associations for each. + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteTableAssociationItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + assocPaginator := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &rtID, + }) + for assocPaginator.HasMorePages() { + assocOut, err := assocPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range assocOut.Associations { + items = append(items, &transitGatewayRouteTableAssociationItem{ + RouteTableID: rtID, + Association: assocOut.Associations[i], + }) + } + } + } + } + return items, nil +} + +// searchTransitGatewayRouteTableAssociations returns all associations for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRouteTableAssociations(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteTableAssociationItem, error) { + routeTableID := query + var items []*transitGatewayRouteTableAssociationItem + pg := ec2.NewGetTransitGatewayRouteTableAssociationsPaginator(client, &ec2.GetTransitGatewayRouteTableAssociationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.Associations { + items = append(items, &transitGatewayRouteTableAssociationItem{ + RouteTableID: routeTableID, + Association: out.Associations[i], + }) + } + } + return items, nil +} + +func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem *transitGatewayRouteTableAssociationItem) (*sdp.Item, error) { + a := &awsItem.Association + attrs, err := ToAttributesWithExclude(a, "") + if err != nil { + return nil, err + } + attachmentID := "" + if a.TransitGatewayAttachmentId != nil { + attachmentID = *a.TransitGatewayAttachmentId + } + uniqueVal := transitGatewayRouteTableAssociationID(awsItem.RouteTableID, attachmentID) + if err := attrs.Set("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route-table-association", + UniqueAttribute: "TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", + Scope: scope, + Attributes: attrs, + } + // Link to route table + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + if a.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *a.TransitGatewayAttachmentId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + if a.ResourceId != nil && *a.ResourceId != "" { + switch a.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *a.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these resource types; attachment link above is sufficient. + } + } + return item, nil +} + +func NewEC2TransitGatewayRouteTableAssociationAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteTableAssociationItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteTableAssociationItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route-table-association", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteTableAssociationAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRouteTableAssociation, + ListFunc: listTransitGatewayRouteTableAssociations, + SearchFunc: searchTransitGatewayRouteTableAssociations, + ItemMapper: transitGatewayRouteTableAssociationItemMapper, + } +} + +var transitGatewayRouteTableAssociationAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table-association", + DescriptiveName: "Transit Gateway Route Table Association", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId", + ListDescription: "List all route table associations", + SearchDescription: "Search by TransitGatewayRouteTableId to list associations for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-attachment", "ec2-vpc", "ec2-vpn-connection", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table_association.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go b/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go new file mode 100644 index 00000000..b6447a84 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go @@ -0,0 +1,64 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestParseAssociationQuery(t *testing.T) { + rt, att, err := parseAssociationQuery("tgw-rtb-1|tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 got %q, %q", rt, att) + } + // Terraform uses underscore as separator + rt, att, err = parseAssociationQuery("tgw-rtb-1_tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 (underscore) got %q, %q", rt, att) + } + _, _, err = parseAssociationQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteTableAssociationItemMapper(t *testing.T) { + item := &transitGatewayRouteTableAssociationItem{ + RouteTableID: "tgw-rtb-123", + Association: types.TransitGatewayRouteTableAssociation{ + TransitGatewayAttachmentId: PtrString("tgw-attach-456"), + ResourceId: PtrString("vpc-abc"), + ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, + State: types.TransitGatewayAssociationStateAssociated, + }, + } + sdpItem, err := transitGatewayRouteTableAssociationItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route-table-association" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } + uv, _ := sdpItem.GetAttributes().Get("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId") + if uv != "tgw-rtb-123|tgw-attach-456" { + t.Errorf("unexpected unique value %v", uv) + } +} + +func TestNewEC2TransitGatewayRouteTableAssociationAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteTableAssociationAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go new file mode 100644 index 00000000..916474f3 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go @@ -0,0 +1,259 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then fetch propagations per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - GetTransitGatewayRouteTablePropagations — list propagations for a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTablePropagations.html + +type transitGatewayRouteTablePropagationItem struct { + RouteTableID string + Propagation types.TransitGatewayRouteTablePropagation +} + +const propagationIDSep = "|" + +func transitGatewayRouteTablePropagationID(routeTableID, attachmentID string) string { + return routeTableID + propagationIDSep + attachmentID +} + +func parsePropagationQuery(query string) (routeTableID, attachmentID string, err error) { + if a, b := parseCompositeID(query, propagationIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|TransitGatewayAttachmentId") +} + +func getTransitGatewayRouteTablePropagation(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteTablePropagationItem, error) { + routeTableID, attachmentID, err := parsePropagationQuery(query) + if err != nil { + return nil, err + } + pg := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.TransitGatewayRouteTablePropagations { + p := &out.TransitGatewayRouteTablePropagations[i] + if p.TransitGatewayAttachmentId != nil && *p.TransitGatewayAttachmentId == attachmentID { + return &transitGatewayRouteTablePropagationItem{RouteTableID: routeTableID, Propagation: *p}, nil + } + } + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("propagation %s not found", query), + } +} + +func listTransitGatewayRouteTablePropagations(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteTablePropagationItem, error) { + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteTablePropagationItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + propPaginator := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &rtID, + }) + for propPaginator.HasMorePages() { + propOut, err := propPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range propOut.TransitGatewayRouteTablePropagations { + items = append(items, &transitGatewayRouteTablePropagationItem{ + RouteTableID: rtID, + Propagation: propOut.TransitGatewayRouteTablePropagations[i], + }) + } + } + } + } + return items, nil +} + +// searchTransitGatewayRouteTablePropagations returns all propagations for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRouteTablePropagations(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteTablePropagationItem, error) { + routeTableID := query + var items []*transitGatewayRouteTablePropagationItem + pg := ec2.NewGetTransitGatewayRouteTablePropagationsPaginator(client, &ec2.GetTransitGatewayRouteTablePropagationsInput{ + TransitGatewayRouteTableId: &routeTableID, + }) + for pg.HasMorePages() { + out, err := pg.NextPage(ctx) + if err != nil { + return nil, err + } + for i := range out.TransitGatewayRouteTablePropagations { + items = append(items, &transitGatewayRouteTablePropagationItem{ + RouteTableID: routeTableID, + Propagation: out.TransitGatewayRouteTablePropagations[i], + }) + } + } + return items, nil +} + +func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem *transitGatewayRouteTablePropagationItem) (*sdp.Item, error) { + p := &awsItem.Propagation + attrs, err := ToAttributesWithExclude(p, "") + if err != nil { + return nil, err + } + attachmentID := "" + if p.TransitGatewayAttachmentId != nil { + attachmentID = *p.TransitGatewayAttachmentId + } + uniqueVal := transitGatewayRouteTablePropagationID(awsItem.RouteTableID, attachmentID) + if err := attrs.Set("TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route-table-propagation", + UniqueAttribute: "TransitGatewayRouteTableIdWithTransitGatewayAttachmentId", + Scope: scope, + Attributes: attrs, + } + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + // Link to the route table association (same route table + attachment). + if p.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-association", + Method: sdp.QueryMethod_GET, + Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *p.TransitGatewayAttachmentId), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + if p.TransitGatewayAttachmentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *p.TransitGatewayAttachmentId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + if p.ResourceId != nil && *p.ResourceId != "" { + switch p.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *p.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these resource types; attachment link above is sufficient. + } + } + return item, nil +} + +func NewEC2TransitGatewayRouteTablePropagationAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteTablePropagationItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteTablePropagationItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route-table-propagation", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteTablePropagationAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRouteTablePropagation, + ListFunc: listTransitGatewayRouteTablePropagations, + SearchFunc: searchTransitGatewayRouteTablePropagations, + ItemMapper: transitGatewayRouteTablePropagationItemMapper, + } +} + +var transitGatewayRouteTablePropagationAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table-propagation", + DescriptiveName: "Transit Gateway Route Table Propagation", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId", + ListDescription: "List all route table propagations", + SearchDescription: "Search by TransitGatewayRouteTableId to list propagations for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-attachment", "ec2-vpc", "ec2-vpn-connection", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table_propagation.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go b/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go new file mode 100644 index 00000000..1d2e6b9f --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go @@ -0,0 +1,60 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestParsePropagationQuery(t *testing.T) { + rt, att, err := parsePropagationQuery("tgw-rtb-1|tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 got %q, %q", rt, att) + } + // Terraform uses underscore as separator + rt, att, err = parsePropagationQuery("tgw-rtb-1_tgw-attach-2") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || att != "tgw-attach-2" { + t.Errorf("expected tgw-rtb-1, tgw-attach-2 (underscore) got %q, %q", rt, att) + } + _, _, err = parsePropagationQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteTablePropagationItemMapper(t *testing.T) { + item := &transitGatewayRouteTablePropagationItem{ + RouteTableID: "tgw-rtb-123", + Propagation: types.TransitGatewayRouteTablePropagation{ + TransitGatewayAttachmentId: PtrString("tgw-attach-456"), + ResourceId: PtrString("vpc-abc"), + ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, + State: types.TransitGatewayPropagationStateEnabled, + }, + } + sdpItem, err := transitGatewayRouteTablePropagationItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route-table-propagation" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } +} + +func TestNewEC2TransitGatewayRouteTablePropagationAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteTablePropagationAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/ec2-transit-gateway-route-table.go b/aws-source/adapters/ec2-transit-gateway-route-table.go new file mode 100644 index 00000000..f9194d63 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table.go @@ -0,0 +1,125 @@ +package adapters + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list/describe transit gateway route tables. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html + +func transitGatewayRouteTableInputMapperGet(scope string, query string) (*ec2.DescribeTransitGatewayRouteTablesInput, error) { + return &ec2.DescribeTransitGatewayRouteTablesInput{ + TransitGatewayRouteTableIds: []string{ + query, + }, + }, nil +} + +func transitGatewayRouteTableInputMapperList(scope string) (*ec2.DescribeTransitGatewayRouteTablesInput, error) { + return &ec2.DescribeTransitGatewayRouteTablesInput{}, nil +} + +func transitGatewayRouteTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.DescribeTransitGatewayRouteTablesInput, output *ec2.DescribeTransitGatewayRouteTablesOutput) ([]*sdp.Item, error) { + items := make([]*sdp.Item, 0) + + for _, rt := range output.TransitGatewayRouteTables { + var err error + var attrs *sdp.ItemAttributes + attrs, err = ToAttributesWithExclude(rt, "tags") + + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: err.Error(), + Scope: scope, + } + } + + item := sdp.Item{ + Type: "ec2-transit-gateway-route-table", + UniqueAttribute: "TransitGatewayRouteTableId", + Scope: scope, + Attributes: attrs, + Tags: ec2TagsToMap(rt.Tags), + } + + if rt.TransitGatewayId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *rt.TransitGatewayId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, + Out: true, + }, + }) + } + + // Link to route table associations, propagations, and routes (Search by route table ID). + if rt.TransitGatewayRouteTableId != nil { + rtID := *rt.TransitGatewayRouteTableId + for _, linkType := range []string{"ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: linkType, + Method: sdp.QueryMethod_SEARCH, + Query: rtID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + } + + items = append(items, &item) + } + + return items, nil +} + +func NewEC2TransitGatewayRouteTableAdapter(client *ec2.Client, accountID string, region string, cache sdpcache.Cache) *DescribeOnlyAdapter[*ec2.DescribeTransitGatewayRouteTablesInput, *ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Client, *ec2.Options] { + return &DescribeOnlyAdapter[*ec2.DescribeTransitGatewayRouteTablesInput, *ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Client, *ec2.Options]{ + Region: region, + Client: client, + AccountID: accountID, + ItemType: "ec2-transit-gateway-route-table", + AdapterMetadata: transitGatewayRouteTableAdapterMetadata, + cache: cache, + DescribeFunc: func(ctx context.Context, client *ec2.Client, input *ec2.DescribeTransitGatewayRouteTablesInput) (*ec2.DescribeTransitGatewayRouteTablesOutput, error) { + return client.DescribeTransitGatewayRouteTables(ctx, input) + }, + InputMapperGet: transitGatewayRouteTableInputMapperGet, + InputMapperList: transitGatewayRouteTableInputMapperList, + PaginatorBuilder: func(client *ec2.Client, params *ec2.DescribeTransitGatewayRouteTablesInput) Paginator[*ec2.DescribeTransitGatewayRouteTablesOutput, *ec2.Options] { + return ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, params) + }, + OutputMapper: transitGatewayRouteTableOutputMapper, + } +} + +var transitGatewayRouteTableAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route-table", + DescriptiveName: "Transit Gateway Route Table", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get a transit gateway route table by ID", + ListDescription: "List all transit gateway route tables", + SearchDescription: "Search transit gateway route tables by ARN", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route_table.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route-table_test.go b/aws-source/adapters/ec2-transit-gateway-route-table_test.go new file mode 100644 index 00000000..2bf596fe --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route-table_test.go @@ -0,0 +1,111 @@ +package adapters + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestTransitGatewayRouteTableInputMapperGet(t *testing.T) { + input, err := transitGatewayRouteTableInputMapperGet("foo", "tgw-rtb-123") + + if err != nil { + t.Error(err) + } + + if len(input.TransitGatewayRouteTableIds) != 1 { + t.Fatalf("expected 1 TransitGatewayRouteTable ID, got %v", len(input.TransitGatewayRouteTableIds)) + } + + if input.TransitGatewayRouteTableIds[0] != "tgw-rtb-123" { + t.Errorf("expected TransitGatewayRouteTable ID to be tgw-rtb-123, got %v", input.TransitGatewayRouteTableIds[0]) + } +} + +func TestTransitGatewayRouteTableInputMapperList(t *testing.T) { + input, err := transitGatewayRouteTableInputMapperList("foo") + + if err != nil { + t.Error(err) + } + + if len(input.Filters) != 0 || len(input.TransitGatewayRouteTableIds) != 0 { + t.Errorf("non-empty input: %v", input) + } +} + +func TestTransitGatewayRouteTableOutputMapper(t *testing.T) { + output := &ec2.DescribeTransitGatewayRouteTablesOutput{ + TransitGatewayRouteTables: []types.TransitGatewayRouteTable{ + { + TransitGatewayRouteTableId: PtrString("tgw-rtb-0123456789abcdef0"), + TransitGatewayId: PtrString("tgw-0abc123"), + State: types.TransitGatewayRouteTableStateAvailable, + DefaultAssociationRouteTable: PtrBool(false), + DefaultPropagationRouteTable: PtrBool(false), + Tags: []types.Tag{ + {Key: PtrString("Name"), Value: PtrString("my-route-table")}, + }, + }, + }, + } + + items, err := transitGatewayRouteTableOutputMapper(context.Background(), nil, "foo", nil, output) + + if err != nil { + t.Fatal(err) + } + + for _, item := range items { + if err := item.Validate(); err != nil { + t.Error(err) + } + } + + if len(items) != 1 { + t.Fatalf("expected 1 item, got %v", len(items)) + } + + if items[0].GetUniqueAttribute() != "TransitGatewayRouteTableId" { + t.Errorf("expected UniqueAttribute TransitGatewayRouteTableId, got %v", items[0].GetUniqueAttribute()) + } + + // Should link to ec2-transit-gateway and to associations, propagations, routes (Search by route table ID) + links := items[0].GetLinkedItemQueries() + if len(links) != 4 { + t.Fatalf("expected 4 linked item queries (ec2-transit-gateway + 3 Search), got %v", len(links)) + } + if links[0].GetQuery().GetType() != "ec2-transit-gateway" { + t.Errorf("expected first link type ec2-transit-gateway, got %v", links[0].GetQuery().GetType()) + } + searchTypes := map[string]bool{} + for _, l := range links[1:] { + if l.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("expected Search method for link %s", l.GetQuery().GetType()) + } + searchTypes[l.GetQuery().GetType()] = true + } + for _, want := range []string{"ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + if !searchTypes[want] { + t.Errorf("expected Search link to %s", want) + } + } +} + +func TestNewEC2TransitGatewayRouteTableAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + + adapter := NewEC2TransitGatewayRouteTableAdapter(client, account, region, sdpcache.NewNoOpCache()) + + test := E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + } + + test.Run(t) +} diff --git a/aws-source/adapters/ec2-transit-gateway-route.go b/aws-source/adapters/ec2-transit-gateway-route.go new file mode 100644 index 00000000..904ee50d --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route.go @@ -0,0 +1,309 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" +) + +// APIs used: +// - DescribeTransitGatewayRouteTables — list route tables (to then search routes per table). +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html +// - SearchTransitGatewayRoutes — search routes in a route table. +// https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SearchTransitGatewayRoutes.html +// +// Note: SearchTransitGatewayRoutes does not support NextToken-based pagination. It returns +// at most 1000 routes per call; AdditionalRoutesAvailable indicates more exist but there is +// no API mechanism to fetch them (route tables can hold up to 10,000 routes). + +type transitGatewayRouteItem struct { + RouteTableID string + Route types.TransitGatewayRoute +} + +const routeIDSep = "|" +const routeDestPrefixList = "pl:" + +func transitGatewayRouteDestination(r *types.TransitGatewayRoute) string { + if r.PrefixListId != nil && *r.PrefixListId != "" { + return routeDestPrefixList + *r.PrefixListId + } + if r.DestinationCidrBlock != nil { + return *r.DestinationCidrBlock + } + return "" +} + +func transitGatewayRouteID(routeTableID, destination string) string { + return routeTableID + routeIDSep + destination +} + +func parseRouteQuery(query string) (routeTableID, destination string, err error) { + if a, b := parseCompositeID(query, routeIDSep); a != "" { + return a, b, nil + } + if a, b := parseCompositeID(query, "_"); a != "" { + return a, b, nil + } + return "", "", fmt.Errorf("query must be TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)") +} + +// searchRoutesFilter returns a filter that returns all routes (active and blackhole). +func searchRoutesFilter() []types.Filter { + return []types.Filter{ + {Name: PtrString("state"), Values: []string{"active", "blackhole"}}, + } +} + +// maxSearchRoutesResults is the maximum routes SearchTransitGatewayRoutes returns per call. +// The API does not support NextToken pagination when AdditionalRoutesAvailable is true. +const maxSearchRoutesResults = 1000 + +func getTransitGatewayRoute(ctx context.Context, client *ec2.Client, _, query string) (*transitGatewayRouteItem, error) { + routeTableID, destination, err := parseRouteQuery(query) + if err != nil { + return nil, err + } + out, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &routeTableID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + for i := range out.Routes { + r := &out.Routes[i] + if transitGatewayRouteDestination(r) == destination { + return &transitGatewayRouteItem{RouteTableID: routeTableID, Route: *r}, nil + } + } + errStr := fmt.Sprintf("route %s not found", query) + if out.AdditionalRoutesAvailable != nil && *out.AdditionalRoutesAvailable { + errStr = fmt.Sprintf("route %s not found in first %d routes; route table has additional routes that cannot be retrieved via this API", query, maxSearchRoutesResults) + } + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: errStr, + } +} + +func listTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _ string) ([]*transitGatewayRouteItem, error) { + rtPaginator := ec2.NewDescribeTransitGatewayRouteTablesPaginator(client, &ec2.DescribeTransitGatewayRouteTablesInput{}) + var items []*transitGatewayRouteItem + for rtPaginator.HasMorePages() { + rtOut, err := rtPaginator.NextPage(ctx) + if err != nil { + return nil, err + } + for _, rt := range rtOut.TransitGatewayRouteTables { + if rt.TransitGatewayRouteTableId == nil { + continue + } + rtID := *rt.TransitGatewayRouteTableId + // Single call per route table: SearchTransitGatewayRoutes returns at most 1000 routes + // and does not support NextToken pagination; AdditionalRoutesAvailable means more + // exist but cannot be fetched via this API. + routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &rtID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + for i := range routeOut.Routes { + items = append(items, &transitGatewayRouteItem{ + RouteTableID: rtID, + Route: routeOut.Routes[i], + }) + } + } + } + return items, nil +} + +// searchTransitGatewayRoutes returns all routes for a single route table. +// Query must be a TransitGatewayRouteTableId (e.g. tgw-rtb-xxxxx). +func searchTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _, query string) ([]*transitGatewayRouteItem, error) { + routeTableID := query + routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: &routeTableID, + Filters: searchRoutesFilter(), + MaxResults: PtrInt32(maxSearchRoutesResults), + }) + if err != nil { + return nil, err + } + items := make([]*transitGatewayRouteItem, 0, len(routeOut.Routes)) + for i := range routeOut.Routes { + items = append(items, &transitGatewayRouteItem{ + RouteTableID: routeTableID, + Route: routeOut.Routes[i], + }) + } + return items, nil +} + +func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayRouteItem) (*sdp.Item, error) { + r := &awsItem.Route + attrs, err := ToAttributesWithExclude(r, "") + if err != nil { + return nil, err + } + dest := transitGatewayRouteDestination(r) + uniqueVal := transitGatewayRouteID(awsItem.RouteTableID, dest) + if err := attrs.Set("TransitGatewayRouteTableIdWithDestination", uniqueVal); err != nil { + return nil, err + } + item := &sdp.Item{ + Type: "ec2-transit-gateway-route", + UniqueAttribute: "TransitGatewayRouteTableIdWithDestination", + Scope: scope, + Attributes: attrs, + } + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table", + Method: sdp.QueryMethod_GET, + Query: awsItem.RouteTableID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + for i := range r.TransitGatewayAttachments { + att := &r.TransitGatewayAttachments[i] + if att.TransitGatewayAttachmentId != nil && *att.TransitGatewayAttachmentId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-attachment", + Method: sdp.QueryMethod_GET, + Query: *att.TransitGatewayAttachmentId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + // Link to the route table association (same route table + attachment). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-association", + Method: sdp.QueryMethod_GET, + Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *att.TransitGatewayAttachmentId), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + if att.ResourceId != nil && *att.ResourceId != "" { + switch att.ResourceType { + case types.TransitGatewayAttachmentResourceTypeVpc: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpc", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpn: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-vpn-connection", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "directconnect-direct-connect-gateway", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypePeering, + types.TransitGatewayAttachmentResourceTypeTgwPeering: + // ResourceId is the peer transit gateway ID (e.g. tgw-xxxxx). + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway", + Method: sdp.QueryMethod_GET, + Query: *att.ResourceId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, + types.TransitGatewayAttachmentResourceTypeConnect, + types.TransitGatewayAttachmentResourceTypeNetworkFunction: + // No Overmind adapter for these; attachment link above is sufficient. + } + } + } + if r.PrefixListId != nil && *r.PrefixListId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-managed-prefix-list", + Method: sdp.QueryMethod_GET, + Query: *r.PrefixListId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + if r.TransitGatewayRouteTableAnnouncementId != nil && *r.TransitGatewayRouteTableAnnouncementId != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "ec2-transit-gateway-route-table-announcement", + Method: sdp.QueryMethod_GET, + Query: *r.TransitGatewayRouteTableAnnouncementId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, + }) + } + return item, nil +} + +func NewEC2TransitGatewayRouteAdapter(client *ec2.Client, accountID, region string, cache sdpcache.Cache) *GetListAdapter[*transitGatewayRouteItem, *ec2.Client, *ec2.Options] { + return &GetListAdapter[*transitGatewayRouteItem, *ec2.Client, *ec2.Options]{ + ItemType: "ec2-transit-gateway-route", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: transitGatewayRouteAdapterMetadata, + cache: cache, + GetFunc: getTransitGatewayRoute, + ListFunc: listTransitGatewayRoutes, + SearchFunc: searchTransitGatewayRoutes, + ItemMapper: transitGatewayRouteItemMapper, + } +} + +var transitGatewayRouteAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "ec2-transit-gateway-route", + DescriptiveName: "Transit Gateway Route", + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get by TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)", + ListDescription: "List all transit gateway routes", + SearchDescription: "Search by TransitGatewayRouteTableId to list routes for that route table", + }, + PotentialLinks: []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-attachment", "ec2-transit-gateway-route-table-announcement", "ec2-vpc", "ec2-vpn-connection", "ec2-managed-prefix-list", "directconnect-direct-connect-gateway"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_ec2_transit_gateway_route.id"}, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, +}) diff --git a/aws-source/adapters/ec2-transit-gateway-route_test.go b/aws-source/adapters/ec2-transit-gateway-route_test.go new file mode 100644 index 00000000..888edb22 --- /dev/null +++ b/aws-source/adapters/ec2-transit-gateway-route_test.go @@ -0,0 +1,68 @@ +package adapters + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/go/sdpcache" +) + +func TestTransitGatewayRouteDestination(t *testing.T) { + if transitGatewayRouteDestination(&types.TransitGatewayRoute{DestinationCidrBlock: PtrString("10.0.0.0/16")}) != "10.0.0.0/16" { + t.Error("expected CIDR destination") + } + if transitGatewayRouteDestination(&types.TransitGatewayRoute{PrefixListId: PtrString("pl-123")}) != "pl:pl-123" { + t.Error("expected prefix list destination") + } +} + +func TestParseRouteQuery(t *testing.T) { + rt, dest, err := parseRouteQuery("tgw-rtb-1|10.0.0.0/16") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || dest != "10.0.0.0/16" { + t.Errorf("expected tgw-rtb-1, 10.0.0.0/16 got %q, %q", rt, dest) + } + // Terraform uses underscore as separator + rt, dest, err = parseRouteQuery("tgw-rtb-1_10.0.0.0/16") + if err != nil { + t.Fatal(err) + } + if rt != "tgw-rtb-1" || dest != "10.0.0.0/16" { + t.Errorf("expected tgw-rtb-1, 10.0.0.0/16 (underscore) got %q, %q", rt, dest) + } + _, _, err = parseRouteQuery("bad") + if err == nil { + t.Error("expected error for bad query") + } +} + +func TestTransitGatewayRouteItemMapper(t *testing.T) { + item := &transitGatewayRouteItem{ + RouteTableID: "tgw-rtb-123", + Route: types.TransitGatewayRoute{ + DestinationCidrBlock: PtrString("10.0.0.0/16"), + State: types.TransitGatewayRouteStateActive, + Type: types.TransitGatewayRouteTypeStatic, + }, + } + sdpItem, err := transitGatewayRouteItemMapper("", "account|region", item) + if err != nil { + t.Fatal(err) + } + if err := sdpItem.Validate(); err != nil { + t.Error(err) + } + if sdpItem.GetType() != "ec2-transit-gateway-route" { + t.Errorf("unexpected type %s", sdpItem.GetType()) + } +} + +func TestNewEC2TransitGatewayRouteAdapter(t *testing.T) { + client, account, region := ec2GetAutoConfig(t) + adapter := NewEC2TransitGatewayRouteAdapter(client, account, region, sdpcache.NewNoOpCache()) + if err := adapter.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/client.go b/aws-source/adapters/integration/ec2-transit-gateway/client.go new file mode 100644 index 00000000..228bb164 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/client.go @@ -0,0 +1,17 @@ +package ec2transitgateway + +import ( + "context" + "fmt" + + awsec2 "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +func ec2Client(ctx context.Context) (*awsec2.Client, error) { + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get AWS settings: %w", err) + } + return awsec2.NewFromConfig(testAWSConfig.Config), nil +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/main_test.go b/aws-source/adapters/integration/ec2-transit-gateway/main_test.go new file mode 100644 index 00000000..7a267186 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/main_test.go @@ -0,0 +1,95 @@ +// Package ec2transitgateway runs integration tests for EC2 Transit Gateway adapters +// (transit gateway route table, route table association, route table propagation, +// and route). Setup creates a transit gateway, VPC, subnet, TGW VPC attachment, +// and a static route so each adapter returns items; Teardown deletes them in order. +// +// All created resources are tagged with name and test-id "integration-test" so they +// are easy to spot in the console and so Teardown can discover them by tag. You can +// run Setup once, re-run the test subtests as needed, then run Teardown once; or run +// Teardown alone to clean up any stale resources from a previous run. +// +// Run integration tests only when RUN_INTEGRATION_TESTS=true. Example CLI commands: +// +// # Setup only (create resources) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/Setup$' +// +// # Teardown only (delete resources by tag; idempotent) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/Teardown$' +// +// # Run a single adapter test (e.g. after Setup, re-run as needed) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$/TransitGatewayRouteTable$' +// +// # Run the full suite (Setup, all adapter tests, Teardown) +// RUN_INTEGRATION_TESTS=true go test ./aws-source/adapters/integration/ec2-transit-gateway -v -count=1 -run '^TestIntegrationEC2TransitGateway$' +// +// Cost: a few cents per run. Setup creates a Transit Gateway, a VPC, a subnet, and +// one TGW VPC attachment so that association, propagation, and route adapters +// return items. AWS charges for the TGW and ~$0.05/hour per VPC attachment; with +// teardown within minutes, cost remains low. See https://aws.amazon.com/transit-gateway/pricing/ +// +// Per-adapter cost: route table, association, propagation, and route tests do not +// create additional resources; they list/get from the same TGW and its default +// route table (one attachment, one static route), so they add no extra cost. +// +// To inspect the infrastructure created by the tests: +// +// - AWS CLI (replace [REGION] and [ROUTE_TABLE_ID] as needed): +// +// aws ec2 describe-transit-gateways [--region [REGION]] +// aws ec2 describe-transit-gateway-route-tables [--region [REGION]] +// aws ec2 get-transit-gateway-route-table-associations --transit-gateway-route-table-id [ROUTE_TABLE_ID] [--region [REGION]] +// aws ec2 get-transit-gateway-route-table-propagations --transit-gateway-route-table-id [ROUTE_TABLE_ID] [--region [REGION]] +// aws ec2 search-transit-gateway-routes --transit-gateway-route-table-id [ROUTE_TABLE_ID] --filters "Name=state,Values=active,blackhole" [--region [REGION]] +// +// - AWS Console: EC2 → Network & Security → Transit gateways → select a transit gateway +// `https://eu-west-2.console.aws.amazon.com/vpcconsole/home?region=eu-west-2#TransitGateways:` other resources are displayed on the left hand pane. +package ec2transitgateway + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestMain(m *testing.M) { + if integration.ShouldRunIntegrationTests() { + fmt.Println("Running EC2 Transit Gateway integration tests") + os.Exit(m.Run()) + } else { + fmt.Println("Skipping EC2 Transit Gateway integration tests, set RUN_INTEGRATION_TESTS=true to run them") + os.Exit(0) + } +} + +func TestIntegrationEC2TransitGateway(t *testing.T) { + // Setup creates resources tagged integration-test; Teardown is idempotent and discovers by tag. + t.Run("Setup", Setup) + t.Run("TransitGatewayRouteTable", TransitGatewayRouteTable) + t.Run("TransitGatewayRouteTableAssociation", TransitGatewayRouteTableAssociation) + t.Run("TransitGatewayRouteTablePropagation", TransitGatewayRouteTablePropagation) + t.Run("TransitGatewayRoute", TransitGatewayRoute) + t.Run("Teardown", Teardown) +} + +func listSync(adapter discovery.ListStreamableAdapter, ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + stream := discovery.NewRecordingQueryResultStream() + adapter.ListStream(ctx, scope, ignoreCache, stream) + if errs := stream.GetErrors(); len(errs) > 0 { + return nil, fmt.Errorf("failed to list: %v", errs) + } + return stream.GetItems(), nil +} + +func searchSync(adapter discovery.SearchStreamableAdapter, ctx context.Context, scope, query string, ignoreCache bool) ([]*sdp.Item, error) { + stream := discovery.NewRecordingQueryResultStream() + adapter.SearchStream(ctx, scope, query, ignoreCache, stream) + if errs := stream.GetErrors(); len(errs) > 0 { + return nil, fmt.Errorf("failed to search: %v", errs) + } + return stream.GetItems(), nil +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/setup.go b/aws-source/adapters/integration/ec2-transit-gateway/setup.go new file mode 100644 index 00000000..ec86afd7 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/setup.go @@ -0,0 +1,243 @@ +package ec2transitgateway + +import ( + "context" + "errors" + "log/slog" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +// integrationTestName is the fixed tag value and name for all resources created by +// this suite. Teardown discovers and deletes resources by tag test-id=, so +// it can be run alone to clean stale resources from previous runs. +const integrationTestName = "integration-test" + +// Package-level state set by Setup and used by tests and Teardown. +var ( + createdTransitGatewayID string + createdRouteTableID string + createdVpcID string + createdSubnetID string + createdAttachmentID string + createdRouteDestination = "10.88.0.0/16" // static route we create (distinct from VPC CIDR) +) + +func Setup(t *testing.T) { + ctx := context.Background() + logger := slog.Default() + + client, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + if err := setup(ctx, logger, client); err != nil { + t.Fatalf("Setup failed: %v", err) + } +} + +func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { + out, err := client.CreateTransitGateway(ctx, &ec2.CreateTransitGatewayInput{ + Description: ptr("Overmind " + integrationTestName), + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeTransitGateway, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + + if out.TransitGateway == nil || out.TransitGateway.TransitGatewayId == nil { + return errors.New("CreateTransitGateway returned nil transit gateway or id") + } + + tgwID := *out.TransitGateway.TransitGatewayId + createdTransitGatewayID = tgwID + logger.InfoContext(ctx, "Created transit gateway, waiting for available", "id", tgwID) + + // Wait for transit gateway to become available (creates default route table). + const waitTimeout = 5 * time.Minute + deadline := time.Now().Add(waitTimeout) + tgwAvailable := false + for time.Now().Before(deadline) { + desc, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + TransitGatewayIds: []string{tgwID}, + }) + if err != nil { + return err + } + if len(desc.TransitGateways) == 0 { + time.Sleep(10 * time.Second) + continue + } + state := desc.TransitGateways[0].State + if state == types.TransitGatewayStateAvailable { + tgwAvailable = true + break + } + if state == types.TransitGatewayStateDeleted || state == types.TransitGatewayStateDeleting { + return errors.New("transit gateway entered deleted/deleting state") + } + time.Sleep(10 * time.Second) + } + if !tgwAvailable { + return errors.New("timeout waiting for transit gateway to become available") + } + + // Resolve default route table for this TGW (needed for attachment and static route). + rtOut, err := client.DescribeTransitGatewayRouteTables(ctx, &ec2.DescribeTransitGatewayRouteTablesInput{ + Filters: []types.Filter{ + {Name: ptr("transit-gateway-id"), Values: []string{tgwID}}, + }, + }) + if err != nil { + return err + } + for i := range rtOut.TransitGatewayRouteTables { + rt := &rtOut.TransitGatewayRouteTables[i] + if rt.TransitGatewayRouteTableId != nil && rt.DefaultAssociationRouteTable != nil && *rt.DefaultAssociationRouteTable { + createdRouteTableID = *rt.TransitGatewayRouteTableId + break + } + } + if createdRouteTableID == "" { + return errors.New("could not find default route table for transit gateway") + } + + // Create VPC and subnet so we can create a VPC attachment (association + propagation + route target). + vpcOut, err := client.CreateVpc(ctx, &ec2.CreateVpcInput{ + CidrBlock: ptr("10.99.0.0/16"), + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeVpc, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if vpcOut.Vpc == nil || vpcOut.Vpc.VpcId == nil { + return errors.New("CreateVpc returned nil vpc or id") + } + createdVpcID = *vpcOut.Vpc.VpcId + logger.InfoContext(ctx, "Created VPC for TGW attachment", "id", createdVpcID) + + // Pick one AZ for the subnet. + azOut, err := client.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{ + Filters: []types.Filter{ + {Name: ptr("state"), Values: []string{"available"}}, + }, + }) + if err != nil || len(azOut.AvailabilityZones) == 0 { + return errors.New("could not describe availability zones") + } + az := azOut.AvailabilityZones[0].ZoneName + + subOut, err := client.CreateSubnet(ctx, &ec2.CreateSubnetInput{ + VpcId: &createdVpcID, + CidrBlock: ptr("10.99.1.0/24"), + AvailabilityZone: az, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeSubnet, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if subOut.Subnet == nil || subOut.Subnet.SubnetId == nil { + return errors.New("CreateSubnet returned nil subnet or id") + } + createdSubnetID = *subOut.Subnet.SubnetId + logger.InfoContext(ctx, "Created subnet for TGW attachment", "id", createdSubnetID) + + attachOut, err := client.CreateTransitGatewayVpcAttachment(ctx, &ec2.CreateTransitGatewayVpcAttachmentInput{ + TransitGatewayId: &tgwID, + VpcId: &createdVpcID, + SubnetIds: []string{createdSubnetID}, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeTransitGatewayAttachment, + Tags: []types.Tag{ + {Key: ptr(integration.TagTestKey), Value: ptr(integration.TagTestValue)}, + {Key: ptr(integration.TagTestIDKey), Value: ptr(integrationTestName)}, + {Key: ptr("Name"), Value: ptr(integrationTestName)}, + }, + }, + }, + }) + if err != nil { + return err + } + if attachOut.TransitGatewayVpcAttachment == nil || attachOut.TransitGatewayVpcAttachment.TransitGatewayAttachmentId == nil { + return errors.New("CreateTransitGatewayVpcAttachment returned nil attachment or id") + } + createdAttachmentID = *attachOut.TransitGatewayVpcAttachment.TransitGatewayAttachmentId + logger.InfoContext(ctx, "Created TGW VPC attachment, waiting for available", "id", createdAttachmentID) + + // Wait for attachment to become available so we can create a route and so associations/propagations appear. + attachDeadline := time.Now().Add(waitTimeout) + attachmentAvailable := false + for time.Now().Before(attachDeadline) { + desc, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + TransitGatewayAttachmentIds: []string{createdAttachmentID}, + }) + if err != nil { + return err + } + if len(desc.TransitGatewayVpcAttachments) == 0 { + time.Sleep(10 * time.Second) + continue + } + state := desc.TransitGatewayVpcAttachments[0].State + if state == types.TransitGatewayAttachmentStateAvailable { + attachmentAvailable = true + break + } + if state == types.TransitGatewayAttachmentStateDeleted || state == types.TransitGatewayAttachmentStateDeleting { + return errors.New("transit gateway VPC attachment entered deleted/deleting state") + } + time.Sleep(10 * time.Second) + } + if !attachmentAvailable { + return errors.New("timeout waiting for transit gateway VPC attachment to become available") + } + + // Add a static route so the route adapter returns at least one item. + _, err = client.CreateTransitGatewayRoute(ctx, &ec2.CreateTransitGatewayRouteInput{ + TransitGatewayRouteTableId: &createdRouteTableID, + DestinationCidrBlock: &createdRouteDestination, + TransitGatewayAttachmentId: &createdAttachmentID, + }) + if err != nil { + return err + } + logger.InfoContext(ctx, "Created static TGW route", "destination", createdRouteDestination) + + return nil +} + +func ptr(s string) *string { return &s } diff --git a/aws-source/adapters/integration/ec2-transit-gateway/teardown.go b/aws-source/adapters/integration/ec2-transit-gateway/teardown.go new file mode 100644 index 00000000..61e94270 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/teardown.go @@ -0,0 +1,181 @@ +package ec2transitgateway + +import ( + "context" + "errors" + "log/slog" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/overmindtech/cli/aws-source/adapters/integration" +) + +// integrationTestTagFilters returns filters to discover resources created by this suite. +func integrationTestTagFilters() []types.Filter { + return []types.Filter{ + {Name: ptr("tag:" + integration.TagTestKey), Values: []string{integration.TagTestValue}}, + {Name: ptr("tag:" + integration.TagTestIDKey), Values: []string{integrationTestName}}, + } +} + +// getIntegrationTestTransitGatewayID returns the transit gateway ID for the integration-test +// resources. If Setup ran in this process, it uses the package-level ID; otherwise it +// discovers the TGW by tag so tests work when run after a separate Setup (e.g. a day later). +// Returns an error if no tagged TGW is found (e.g. after Teardown). +func getIntegrationTestTransitGatewayID(ctx context.Context, client *ec2.Client) (string, error) { + if createdTransitGatewayID != "" { + return createdTransitGatewayID, nil + } + tgwOut, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + Filters: integrationTestTagFilters(), + }) + if err != nil { + return "", err + } + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId != nil && tgw.State != types.TransitGatewayStateDeleted && tgw.State != types.TransitGatewayStateDeleting { + return *tgw.TransitGatewayId, nil + } + } + return "", errors.New("no transit gateway found with integration-test tag (run Setup first or ensure Teardown has not deleted resources)") +} + +func Teardown(t *testing.T) { + ctx := context.Background() + logger := slog.Default() + + client, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + if err := teardown(ctx, logger, client); err != nil { + t.Fatalf("Teardown failed: %v", err) + } +} + +func teardown(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { + tagFilters := integrationTestTagFilters() + + // 1. Discover transit gateways by tag. + tgwOut, err := client.DescribeTransitGateways(ctx, &ec2.DescribeTransitGatewaysInput{ + Filters: tagFilters, + }) + if err != nil { + return err + } + if len(tgwOut.TransitGateways) == 0 { + logger.InfoContext(ctx, "No transit gateways found with integration-test tag") + clearPackageState() + return nil + } + + // 2. For each TGW: delete static route, then VPC attachments, and wait for attachments to be deleted. + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId == nil || tgw.State == types.TransitGatewayStateDeleted || tgw.State == types.TransitGatewayStateDeleting { + continue + } + tgwID := *tgw.TransitGatewayId + + // Resolve default route table and delete our static route. + rtOut, err := client.DescribeTransitGatewayRouteTables(ctx, &ec2.DescribeTransitGatewayRouteTablesInput{ + Filters: []types.Filter{{Name: ptr("transit-gateway-id"), Values: []string{tgwID}}}, + }) + if err != nil { + return err + } + var defaultRouteTableID string + for i := range rtOut.TransitGatewayRouteTables { + rt := &rtOut.TransitGatewayRouteTables[i] + if rt.TransitGatewayRouteTableId != nil && rt.DefaultAssociationRouteTable != nil && *rt.DefaultAssociationRouteTable { + defaultRouteTableID = *rt.TransitGatewayRouteTableId + break + } + } + if defaultRouteTableID != "" { + _, _ = client.DeleteTransitGatewayRoute(ctx, &ec2.DeleteTransitGatewayRouteInput{ + TransitGatewayRouteTableId: &defaultRouteTableID, + DestinationCidrBlock: &createdRouteDestination, + }) + } + + // List VPC attachments for this TGW and delete each. + attachOut, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + Filters: []types.Filter{{Name: ptr("transit-gateway-id"), Values: []string{tgwID}}}, + }) + if err != nil { + return err + } + for _, att := range attachOut.TransitGatewayVpcAttachments { + if att.TransitGatewayAttachmentId == nil || att.State == types.TransitGatewayAttachmentStateDeleted || att.State == types.TransitGatewayAttachmentStateDeleting { + continue + } + attID := *att.TransitGatewayAttachmentId + _, _ = client.DeleteTransitGatewayVpcAttachment(ctx, &ec2.DeleteTransitGatewayVpcAttachmentInput{ + TransitGatewayAttachmentId: &attID, + }) + logger.InfoContext(ctx, "Deleted TGW VPC attachment, waiting for deleted", "id", attID) + deadline := time.Now().Add(5 * time.Minute) + for time.Now().Before(deadline) { + desc, err := client.DescribeTransitGatewayVpcAttachments(ctx, &ec2.DescribeTransitGatewayVpcAttachmentsInput{ + TransitGatewayAttachmentIds: []string{attID}, + }) + if err != nil || len(desc.TransitGatewayVpcAttachments) == 0 { + break + } + if desc.TransitGatewayVpcAttachments[0].State == types.TransitGatewayAttachmentStateDeleted { + break + } + time.Sleep(10 * time.Second) + } + } + } + + // 3. Delete subnets by tag. + subOut, err := client.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{Filters: tagFilters}) + if err != nil { + return err + } + for _, sub := range subOut.Subnets { + if sub.SubnetId != nil { + _, _ = client.DeleteSubnet(ctx, &ec2.DeleteSubnetInput{SubnetId: sub.SubnetId}) + } + } + + // 4. Delete VPCs by tag. + vpcOut, err := client.DescribeVpcs(ctx, &ec2.DescribeVpcsInput{Filters: tagFilters}) + if err != nil { + return err + } + for _, vpc := range vpcOut.Vpcs { + if vpc.VpcId != nil { + _, _ = client.DeleteVpc(ctx, &ec2.DeleteVpcInput{VpcId: vpc.VpcId}) + } + } + + // 5. Delete transit gateways. + for _, tgw := range tgwOut.TransitGateways { + if tgw.TransitGatewayId == nil || tgw.State == types.TransitGatewayStateDeleted || tgw.State == types.TransitGatewayStateDeleting { + continue + } + tgwID := *tgw.TransitGatewayId + _, err := client.DeleteTransitGateway(ctx, &ec2.DeleteTransitGatewayInput{TransitGatewayId: &tgwID}) + if err != nil { + return err + } + logger.InfoContext(ctx, "Deleted transit gateway", "id", tgwID) + } + + clearPackageState() + return nil +} + +func clearPackageState() { + createdTransitGatewayID = "" + createdRouteTableID = "" + createdVpcID = "" + createdSubnetID = "" + createdAttachmentID = "" +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go new file mode 100644 index 00000000..c05194ba --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_association_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTableAssociation runs the integration test for the route table association adapter. +// Setup creates a TGW VPC attachment, so the default route table has at least one association. +func TransitGatewayRouteTableAssociation(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteTableAssociationAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route table associations: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one association (Setup creates a TGW VPC attachment); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get association %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → association link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search associations by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one association for route table %s (Setup creates one); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go new file mode 100644 index 00000000..8c62b735 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_propagation_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTablePropagation runs the integration test for the route table propagation adapter. +// Setup creates a TGW VPC attachment (propagated to the default route table), so we get at least one propagation. +func TransitGatewayRouteTablePropagation(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteTablePropagationAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route table propagations: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one propagation (Setup creates a TGW VPC attachment); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get propagation %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → propagation link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search propagations by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one propagation for route table %s (Setup creates one); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go new file mode 100644 index 00000000..1bbb6ba3 --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_table_test.go @@ -0,0 +1,111 @@ +package ec2transitgateway + +import ( + "context" + "fmt" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRouteTable runs the integration test for the transit gateway route table adapter. +// +// AWS CLI – list route tables (same data this test lists/gets/searches): +// +// aws ec2 describe-transit-gateway-route-tables [--region REGION] +// +// AWS Console – Transit Gateway route tables: +// +// https://[REGION].console.aws.amazon.com/ec2/home?region=[REGION]#TransitGatewayRouteTables: +// +// Overmind – In the app, open your AWS source and search for type ec2-transit-gateway-route-table +// or navigate to the resource type in the source. +func TransitGatewayRouteTable(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + accountID := testAWSConfig.AccountID + scope := adapters.FormatScope(accountID, testAWSConfig.Region) + + adapter := adapters.NewEC2TransitGatewayRouteTableAdapter(testClient, accountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate transit gateway route table adapter: %v", err) + } + + items, err := listSync(adapter, ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway route tables: %v", err) + } + + tgwID, err := getIntegrationTestTransitGatewayID(ctx, testClient) + if err != nil { + t.Fatalf("failed to get integration-test transit gateway ID: %v", err) + } + + // Find the route table for the transit gateway created in Setup (or discovered by tag). + var routeTableID string + for _, item := range items { + tgwIDVal, _ := item.GetAttributes().Get("TransitGatewayId") + if tgwIDVal != nil { + if id, ok := tgwIDVal.(string); ok && id == tgwID { + routeTableID = item.UniqueAttributeValue() + break + } + } + } + if routeTableID == "" { + t.Fatalf("no route table found for transit gateway %s (created in Setup)", tgwID) + } + + got, err := adapter.Get(ctx, scope, routeTableID, true) + if err != nil { + t.Fatalf("failed to get transit gateway route table %s: %v", routeTableID, err) + } + + if got.UniqueAttributeValue() != routeTableID { + t.Fatalf("expected route table ID %s from Get, got %s", routeTableID, got.UniqueAttributeValue()) + } + + arn := fmt.Sprintf("arn:aws:ec2:%s:%s:transit-gateway-route-table/%s", testAWSConfig.Region, accountID, routeTableID) + searchItems, err := searchSync(adapter, ctx, scope, arn, true) + if err != nil { + t.Fatalf("failed to search transit gateway route table by ARN: %v", err) + } + + if len(searchItems) == 0 { + t.Fatalf("search by ARN returned no items") + } + + if searchItems[0].UniqueAttributeValue() != routeTableID { + t.Fatalf("expected route table ID %s from Search, got %s", routeTableID, searchItems[0].UniqueAttributeValue()) + } + + // Route table links to associations, propagations, and routes (Search by route table ID). + links := got.GetLinkedItemQueries() + if len(links) < 4 { + t.Fatalf("expected at least 4 linked item queries (ec2-transit-gateway + 3 Search links); got %d", len(links)) + } + linkTypes := make(map[string]bool) + for _, l := range links { + if l.GetQuery() != nil { + linkTypes[l.GetQuery().GetType()] = true + } + } + for _, want := range []string{"ec2-transit-gateway", "ec2-transit-gateway-route-table-association", "ec2-transit-gateway-route-table-propagation", "ec2-transit-gateway-route"} { + if !linkTypes[want] { + t.Errorf("expected route table to link to %s", want) + } + } +} diff --git a/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go new file mode 100644 index 00000000..1458a88b --- /dev/null +++ b/aws-source/adapters/integration/ec2-transit-gateway/transit_gateway_route_test.go @@ -0,0 +1,62 @@ +package ec2transitgateway + +import ( + "context" + "testing" + + "github.com/overmindtech/cli/aws-source/adapters" + "github.com/overmindtech/cli/aws-source/adapters/integration" + "github.com/overmindtech/cli/go/sdpcache" +) + +// TransitGatewayRoute runs the integration test for the transit gateway route adapter. +// Setup creates a static route in the default route table, so we get at least one route. +func TransitGatewayRoute(t *testing.T) { + ctx := context.Background() + + testClient, err := ec2Client(ctx) + if err != nil { + t.Fatalf("Failed to create EC2 client: %v", err) + } + + testAWSConfig, err := integration.AWSSettings(ctx) + if err != nil { + t.Fatalf("Failed to get AWS settings: %v", err) + } + + scope := adapters.FormatScope(testAWSConfig.AccountID, testAWSConfig.Region) + adapter := adapters.NewEC2TransitGatewayRouteAdapter(testClient, testAWSConfig.AccountID, testAWSConfig.Region, sdpcache.NewNoOpCache()) + + if err := adapter.Validate(); err != nil { + t.Fatalf("failed to validate adapter: %v", err) + } + + items, err := adapter.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list transit gateway routes: %v", err) + } + + if len(items) == 0 { + t.Fatalf("expected at least one route (Setup creates a static TGW route); got 0") + } + + query := items[0].UniqueAttributeValue() + got, err := adapter.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get route %s: %v", query, err) + } + if got.UniqueAttributeValue() != query { + t.Fatalf("expected %s, got %s", query, got.UniqueAttributeValue()) + } + + // Search by route table ID (used by route table → route link). + if createdRouteTableID != "" { + searchItems, err := adapter.Search(ctx, scope, createdRouteTableID, true) + if err != nil { + t.Fatalf("failed to search routes by route table ID %s: %v", createdRouteTableID, err) + } + if len(searchItems) == 0 { + t.Fatalf("expected at least one route for route table %s (Setup creates a static route); got 0", createdRouteTableID) + } + } +} diff --git a/aws-source/proc/proc.go b/aws-source/proc/proc.go index 9a9c6c3c..6f0b646c 100644 --- a/aws-source/proc/proc.go +++ b/aws-source/proc/proc.go @@ -435,6 +435,10 @@ func InitializeAwsSourceAdapters(ctx context.Context, e *discovery.Engine, confi adapters.NewEC2PlacementGroupAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2ReservedInstanceAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2RouteTableAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTableAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTableAssociationAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteTablePropagationAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), + adapters.NewEC2TransitGatewayRouteAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SecurityGroupRuleAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SecurityGroupAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), adapters.NewEC2SnapshotAdapter(ec2Client, *callerID.Account, cfg.Region, sharedCache), diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md new file mode 100644 index 00000000..39681af9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-managed-prefix-list.md @@ -0,0 +1,18 @@ +--- +title: Managed Prefix List +sidebar_label: ec2-managed-prefix-list +--- + +A managed prefix list is a set of one or more CIDR blocks that you can reference in security group rules, route table routes, and other network configuration. Transit gateway routes can use a prefix list as the destination instead of a single CIDR. + +Official API documentation: [DescribeManagedPrefixLists](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeManagedPrefixLists.html) + +**Terraform Mappings:** + +- `aws_ec2_managed_prefix_list.id` + +## Supported Methods + +- `GET`: Get a managed prefix list by ID +- `LIST`: List all managed prefix lists +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md new file mode 100644 index 00000000..5b706d45 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-attachment.md @@ -0,0 +1,20 @@ +--- +title: Transit Gateway Attachment +sidebar_label: ec2-transit-gateway-attachment +--- + +A Transit Gateway attachment connects a resource (VPC, VPN connection, Direct Connect gateway, peering connection, or Connect attachment) to a transit gateway. Attachments are associated with route tables and can have routes propagated to them. + +Official API documentation: [DescribeTransitGatewayAttachments](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayAttachments.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_vpc_attachment.id` (VPC) +- `aws_ec2_transit_gateway_vpn_attachment.id` (VPN) +- Other attachment types have corresponding Terraform resources. + +## Supported Methods + +- `GET`: Get a transit gateway attachment by ID +- `LIST`: List all transit gateway attachments +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md new file mode 100644 index 00000000..878175d5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-announcement.md @@ -0,0 +1,12 @@ +--- +title: Transit Gateway Route Table Announcement +sidebar_label: ec2-transit-gateway-route-table-announcement +--- + +A Transit Gateway Route Table Announcement represents the advertisement of a transit gateway route table to a peer—for example, to another transit gateway (peering) or to an AWS Network Manager core network. Routes that originate from such an announcement appear in the route table with a `TransitGatewayRouteTableAnnouncementId`, and Overmind links those [ec2-transit-gateway-route](/sources/aws/Types/ec2-transit-gateway-route) items to this type. + +Official API documentation: [DescribeTransitGatewayRouteTableAnnouncements](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTableAnnouncements.html) + +## Note + +Overmind does not currently provide a dedicated adapter for `ec2-transit-gateway-route-table-announcement`. This type is documented because [ec2-transit-gateway-route](/sources/aws/Types/ec2-transit-gateway-route) items can link to it when a route originates from a route table announcement (`TransitGatewayRouteTableAnnouncementId`). diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md new file mode 100644 index 00000000..e5828353 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-association.md @@ -0,0 +1,40 @@ +--- +title: Transit Gateway Route Table Association +sidebar_label: ec2-transit-gateway-route-table-association +--- + +An association links a transit gateway attachment (VPC, VPN, Direct Connect gateway, peering, or Connect) to a transit gateway route table. Traffic for that attachment is routed according to the route table. + +Official API documentation: [GetTransitGatewayRouteTableAssociations](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTableAssociations.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table_association.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|TransitGatewayAttachmentId` +- `LIST`: List all route table associations (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all associations for that route table (used by the route table’s link to associations) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that the attachment is associated with. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +The transit gateway attachment that is associated with the route table. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When the attachment resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When the attachment resource type is VPN, the linked VPN connection. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When the attachment resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md new file mode 100644 index 00000000..ed68c34f --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table-propagation.md @@ -0,0 +1,44 @@ +--- +title: Transit Gateway Route Table Propagation +sidebar_label: ec2-transit-gateway-route-table-propagation +--- + +A propagation enables a transit gateway route table to automatically learn routes from an attachment (VPC, VPN, Direct Connect gateway, peering, or Connect). When propagation is enabled, routes from that attachment appear in the route table. + +Official API documentation: [GetTransitGatewayRouteTablePropagations](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetTransitGatewayRouteTablePropagations.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table_propagation.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|TransitGatewayAttachmentId` +- `LIST`: List all route table propagations (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all propagations for that route table (used by the route table’s link to propagations) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that is propagating routes from the attachment. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +The route table association for the same route table and attachment (same composite ID). Links propagation and association in the graph. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +The attachment whose routes are being propagated into the route table. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When the attachment resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When the attachment resource type is VPN, the linked VPN connection. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When the attachment resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md new file mode 100644 index 00000000..f0efd09c --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route-table.md @@ -0,0 +1,36 @@ +--- +title: Transit Gateway Route Table +sidebar_label: ec2-transit-gateway-route-table +--- + +A Transit Gateway Route Table determines how traffic is routed for attachments (VPCs, VPNs, Direct Connect gateways, peering connections, or Connect attachments) that are associated with it. Each transit gateway has a default route table; you can create additional route tables to control which attachments can reach which routes. + +Official API documentation: [DescribeTransitGatewayRouteTables](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGatewayRouteTables.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route_table.id` + +## Supported Methods + +- `GET`: Get a transit gateway route table by ID +- `LIST`: List all transit gateway route tables +- `SEARCH`: Search transit gateway route tables by ARN + +## Possible Links + +### [`ec2-transit-gateway`](/sources/aws/Types/ec2-transit-gateway) + +Each transit gateway route table belongs to a single transit gateway. The route table controls routing for attachments that are associated with it. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +Associations for this route table (Search by route table ID). Each association links an attachment to this route table. + +### [`ec2-transit-gateway-route-table-propagation`](/sources/aws/Types/ec2-transit-gateway-route-table-propagation) + +Propagations for this route table (Search by route table ID). Each propagation enables the route table to learn routes from an attachment. + +### [`ec2-transit-gateway-route`](/sources/aws/Types/ec2-transit-gateway-route) + +Routes in this route table (Search by route table ID). diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md new file mode 100644 index 00000000..361596ce --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway-route.md @@ -0,0 +1,52 @@ +--- +title: Transit Gateway Route +sidebar_label: ec2-transit-gateway-route +--- + +A route in a transit gateway route table. Each route has a destination (CIDR or prefix list) and a target (attachment or resource). Routes can be static or propagated from attachments. + +Official API documentation: [SearchTransitGatewayRoutes](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SearchTransitGatewayRoutes.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway_route.id` + +## Supported Methods + +- `GET`: Get by composite ID `TransitGatewayRouteTableId|Destination`, where Destination is a CIDR (e.g. `10.0.0.0/16`) or prefix list (e.g. `pl:PrefixListId`) +- `LIST`: List all transit gateway routes (across all route tables in the scope) +- `SEARCH`: Search by `TransitGatewayRouteTableId` to list all routes in that route table (used by the route table’s link to routes) + +## Possible Links + +### [`ec2-transit-gateway-route-table`](/sources/aws/Types/ec2-transit-gateway-route-table) + +The route table that contains this route. + +### [`ec2-transit-gateway-route-table-association`](/sources/aws/Types/ec2-transit-gateway-route-table-association) + +For each attachment that this route targets, the corresponding route table association (same route table and attachment). Links routes and associations in the graph. + +### [`ec2-transit-gateway-attachment`](/sources/aws/Types/ec2-transit-gateway-attachment) + +Each attachment that this route targets (from the route’s `TransitGatewayAttachments`). + +### [`ec2-transit-gateway-route-table-announcement`](/sources/aws/Types/ec2-transit-gateway-route-table-announcement) + +When the route originates from a route table announcement, the linked transit gateway route table announcement. + +### [`ec2-vpc`](/sources/aws/Types/ec2-vpc) + +When a route attachment’s resource type is VPC, the linked VPC. + +### [`ec2-vpn-connection`](/sources/aws/Types/ec2-vpn-connection) + +When a route attachment’s resource type is VPN, the linked VPN connection. + +### [`ec2-managed-prefix-list`](/sources/aws/Types/ec2-managed-prefix-list) + +When the route destination is a prefix list (instead of a CIDR), the managed prefix list. + +### [`directconnect-direct-connect-gateway`](/sources/aws/Types/directconnect-direct-connect-gateway) + +When a route attachment’s resource type is Direct Connect gateway, the linked Direct Connect gateway. diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md new file mode 100644 index 00000000..70ade350 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-transit-gateway.md @@ -0,0 +1,18 @@ +--- +title: Transit Gateway +sidebar_label: ec2-transit-gateway +--- + +An AWS Transit Gateway is a network transit hub that you use to interconnect your VPCs and on-premises networks. Each transit gateway has a default route table and can have additional route tables to control routing for attachments (VPCs, VPNs, Direct Connect gateways, peering, Connect). + +Official API documentation: [DescribeTransitGateways](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeTransitGateways.html) + +**Terraform Mappings:** + +- `aws_ec2_transit_gateway.id` + +## Supported Methods + +- `GET`: Get a transit gateway by ID +- `LIST`: List all transit gateways +- `SEARCH`: Search transit gateways by ARN diff --git a/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md new file mode 100644 index 00000000..46bf1cdc --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/Types/ec2-vpn-connection.md @@ -0,0 +1,18 @@ +--- +title: VPN Connection +sidebar_label: ec2-vpn-connection +--- + +An AWS Site-to-Site VPN connection links your on-premises network to your VPC (or to a transit gateway) over an encrypted IPsec tunnel. VPN connections can be attached to a transit gateway for use in a hub-and-spoke topology. + +Official API documentation: [DescribeVpnConnections](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpnConnections.html) + +**Terraform Mappings:** + +- `aws_vpn_connection.id` + +## Supported Methods + +- `GET`: Get a VPN connection by ID +- `LIST`: List all VPN connections +- `SEARCH`: Search by ARN diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json new file mode 100644 index 00000000..295f0267 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-association.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table-association","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-attachment","ec2-vpc","ec2-vpn-connection","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route Table Association","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId","list":true,"listDescription":"List all route table associations","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list associations for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table_association.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json new file mode 100644 index 00000000..f7f37b4d --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table-propagation.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table-propagation","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-route-table-association","ec2-transit-gateway-attachment","ec2-vpc","ec2-vpn-connection","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route Table Propagation","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|TransitGatewayAttachmentId","list":true,"listDescription":"List all route table propagations","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list propagations for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table_propagation.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json new file mode 100644 index 00000000..d0dad238 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route-table.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route-table","category":3,"potentialLinks":["ec2-transit-gateway","ec2-transit-gateway-route-table-association","ec2-transit-gateway-route-table-propagation","ec2-transit-gateway-route"],"descriptiveName":"Transit Gateway Route Table","supportedQueryMethods":{"get":true,"getDescription":"Get a transit gateway route table by ID","list":true,"listDescription":"List all transit gateway route tables","search":true,"searchDescription":"Search transit gateway route tables by ARN"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route_table.id"}]} diff --git a/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json new file mode 100644 index 00000000..01c80f1b --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/data/ec2-transit-gateway-route.json @@ -0,0 +1 @@ +{"type":"ec2-transit-gateway-route","category":3,"potentialLinks":["ec2-transit-gateway-route-table","ec2-transit-gateway-route-table-association","ec2-transit-gateway-attachment","ec2-transit-gateway-route-table-announcement","ec2-vpc","ec2-vpn-connection","ec2-managed-prefix-list","directconnect-direct-connect-gateway"],"descriptiveName":"Transit Gateway Route","supportedQueryMethods":{"get":true,"getDescription":"Get by TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)","list":true,"listDescription":"List all transit gateway routes","search":true,"searchDescription":"Search by TransitGatewayRouteTableId to list routes for that route table"},"terraformMappings":[{"terraformQueryMap":"aws_ec2_transit_gateway_route.id"}]} From 8a3624a8494c4894346dfbfebbaaeb1886562734 Mon Sep 17 00:00:00 2001 From: carabasdaniel Date: Wed, 18 Feb 2026 15:40:33 +0200 Subject: [PATCH 30/51] Knowledge feature proto changes (#3922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new protobuf messages and fields for the Knowledge feature and regenerate code to support backend, CLI, and frontend development. --- Linear Issue: [ENG-2615](https://linear.app/overmind/issue/ENG-2615/sdp-proto-changes-for-knowledge-feature)

Open in Cursor Open in Web

--- > [!NOTE] > **Low Risk** > Generated protobuf code changes that primarily add new optional/repeated fields; main risk is wire compatibility for clients/servers that haven’t regenerated against the updated schema. > > **Overview** > Adds new protobuf messages `Knowledge` and `KnowledgeReference` and regenerates `changes.pb.go` accordingly. > > Extends `StartChangeAnalysisRequest` with a repeated `knowledge` field to supply knowledge inputs, and extends `HypothesesDetails` with `knowledgeUsed` so responses can reference which knowledge was used during investigation; remaining edits are generated index/descriptor renumbering. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 631134cf6a2a7f50efca5082bcbebd8b0f9b360f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor Agent Co-authored-by: carabasdaniel GitOrigin-RevId: c3275ef08ce8b86b85f50248af629250aeecd3dd --- go/sdp-go/changes.pb.go | 1184 ++++++++++++++++++++++----------------- 1 file changed, 668 insertions(+), 516 deletions(-) diff --git a/go/sdp-go/changes.pb.go b/go/sdp-go/changes.pb.go index 5e3f6c43..a728ee87 100644 --- a/go/sdp-go/changes.pb.go +++ b/go/sdp-go/changes.pb.go @@ -531,7 +531,7 @@ func (x StartChangeResponse_State) Number() protoreflect.EnumNumber { // Deprecated: Use StartChangeResponse_State.Descriptor instead. func (StartChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84, 0} + return file_changes_proto_rawDescGZIP(), []int{86, 0} } type EndChangeResponse_State int32 @@ -587,7 +587,7 @@ func (x EndChangeResponse_State) Number() protoreflect.EnumNumber { // Deprecated: Use EndChangeResponse_State.Descriptor instead. func (EndChangeResponse_State) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86, 0} + return file_changes_proto_rawDescGZIP(), []int{88, 0} } type Risk_Severity int32 @@ -639,7 +639,7 @@ func (x Risk_Severity) Number() protoreflect.EnumNumber { // Deprecated: Use Risk_Severity.Descriptor instead. func (Risk_Severity) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89, 0} + return file_changes_proto_rawDescGZIP(), []int{91, 0} } type ChangeAnalysisStatus_Status int32 @@ -694,7 +694,7 @@ func (x ChangeAnalysisStatus_Status) Number() protoreflect.EnumNumber { // Deprecated: Use ChangeAnalysisStatus_Status.Descriptor instead. func (ChangeAnalysisStatus_Status) EnumDescriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90, 0} + return file_changes_proto_rawDescGZIP(), []int{92, 0} } type LabelRule struct { @@ -1523,6 +1523,126 @@ func (x *ReapplyLabelRuleInTimeRangeResponse) GetChangeUUID() [][]byte { return nil } +type KnowledgeReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *KnowledgeReference) Reset() { + *x = KnowledgeReference{} + mi := &file_changes_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *KnowledgeReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KnowledgeReference) ProtoMessage() {} + +func (x *KnowledgeReference) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KnowledgeReference.ProtoReflect.Descriptor instead. +func (*KnowledgeReference) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{17} +} + +func (x *KnowledgeReference) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *KnowledgeReference) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + +type Knowledge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + FileName string `protobuf:"bytes,4,opt,name=fileName,proto3" json:"fileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Knowledge) Reset() { + *x = Knowledge{} + mi := &file_changes_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Knowledge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Knowledge) ProtoMessage() {} + +func (x *Knowledge) ProtoReflect() protoreflect.Message { + mi := &file_changes_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Knowledge.ProtoReflect.Descriptor instead. +func (*Knowledge) Descriptor() ([]byte, []int) { + return file_changes_proto_rawDescGZIP(), []int{18} +} + +func (x *Knowledge) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Knowledge) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Knowledge) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Knowledge) GetFileName() string { + if x != nil { + return x.FileName + } + return "" +} + type GetHypothesesDetailsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` @@ -1532,7 +1652,7 @@ type GetHypothesesDetailsRequest struct { func (x *GetHypothesesDetailsRequest) Reset() { *x = GetHypothesesDetailsRequest{} - mi := &file_changes_proto_msgTypes[17] + mi := &file_changes_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1544,7 +1664,7 @@ func (x *GetHypothesesDetailsRequest) String() string { func (*GetHypothesesDetailsRequest) ProtoMessage() {} func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[17] + mi := &file_changes_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1557,7 +1677,7 @@ func (x *GetHypothesesDetailsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHypothesesDetailsRequest.ProtoReflect.Descriptor instead. func (*GetHypothesesDetailsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{17} + return file_changes_proto_rawDescGZIP(), []int{19} } func (x *GetHypothesesDetailsRequest) GetChangeUUID() []byte { @@ -1576,7 +1696,7 @@ type GetHypothesesDetailsResponse struct { func (x *GetHypothesesDetailsResponse) Reset() { *x = GetHypothesesDetailsResponse{} - mi := &file_changes_proto_msgTypes[18] + mi := &file_changes_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1588,7 +1708,7 @@ func (x *GetHypothesesDetailsResponse) String() string { func (*GetHypothesesDetailsResponse) ProtoMessage() {} func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[18] + mi := &file_changes_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1601,7 +1721,7 @@ func (x *GetHypothesesDetailsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetHypothesesDetailsResponse.ProtoReflect.Descriptor instead. func (*GetHypothesesDetailsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{18} + return file_changes_proto_rawDescGZIP(), []int{20} } func (x *GetHypothesesDetailsResponse) GetHypotheses() []*HypothesesDetails { @@ -1623,13 +1743,15 @@ type HypothesesDetails struct { Status HypothesisStatus `protobuf:"varint,4,opt,name=status,proto3,enum=changes.HypothesisStatus" json:"status,omitempty"` // The results of the investigation of the hypothesis InvestigationResults string `protobuf:"bytes,5,opt,name=investigationResults,proto3" json:"investigationResults,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Knowledge used when investigating this hypothesis + KnowledgeUsed []*KnowledgeReference `protobuf:"bytes,6,rep,name=knowledgeUsed,proto3" json:"knowledgeUsed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HypothesesDetails) Reset() { *x = HypothesesDetails{} - mi := &file_changes_proto_msgTypes[19] + mi := &file_changes_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1641,7 +1763,7 @@ func (x *HypothesesDetails) String() string { func (*HypothesesDetails) ProtoMessage() {} func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[19] + mi := &file_changes_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1654,7 +1776,7 @@ func (x *HypothesesDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use HypothesesDetails.ProtoReflect.Descriptor instead. func (*HypothesesDetails) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{19} + return file_changes_proto_rawDescGZIP(), []int{21} } func (x *HypothesesDetails) GetTitle() string { @@ -1692,6 +1814,13 @@ func (x *HypothesesDetails) GetInvestigationResults() string { return "" } +func (x *HypothesesDetails) GetKnowledgeUsed() []*KnowledgeReference { + if x != nil { + return x.KnowledgeUsed + } + return nil +} + type GetChangeTimelineV2Request struct { state protoimpl.MessageState `protogen:"open.v1"` ChangeUUID []byte `protobuf:"bytes,1,opt,name=changeUUID,proto3" json:"changeUUID,omitempty"` @@ -1701,7 +1830,7 @@ type GetChangeTimelineV2Request struct { func (x *GetChangeTimelineV2Request) Reset() { *x = GetChangeTimelineV2Request{} - mi := &file_changes_proto_msgTypes[20] + mi := &file_changes_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1713,7 +1842,7 @@ func (x *GetChangeTimelineV2Request) String() string { func (*GetChangeTimelineV2Request) ProtoMessage() {} func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[20] + mi := &file_changes_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1726,7 +1855,7 @@ func (x *GetChangeTimelineV2Request) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeTimelineV2Request.ProtoReflect.Descriptor instead. func (*GetChangeTimelineV2Request) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{20} + return file_changes_proto_rawDescGZIP(), []int{22} } func (x *GetChangeTimelineV2Request) GetChangeUUID() []byte { @@ -1746,7 +1875,7 @@ type GetChangeTimelineV2Response struct { func (x *GetChangeTimelineV2Response) Reset() { *x = GetChangeTimelineV2Response{} - mi := &file_changes_proto_msgTypes[21] + mi := &file_changes_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1758,7 +1887,7 @@ func (x *GetChangeTimelineV2Response) String() string { func (*GetChangeTimelineV2Response) ProtoMessage() {} func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[21] + mi := &file_changes_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1771,7 +1900,7 @@ func (x *GetChangeTimelineV2Response) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeTimelineV2Response.ProtoReflect.Descriptor instead. func (*GetChangeTimelineV2Response) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{21} + return file_changes_proto_rawDescGZIP(), []int{23} } func (x *GetChangeTimelineV2Response) GetEntries() []*ChangeTimelineEntryV2 { @@ -1829,7 +1958,7 @@ type ChangeTimelineEntryV2 struct { func (x *ChangeTimelineEntryV2) Reset() { *x = ChangeTimelineEntryV2{} - mi := &file_changes_proto_msgTypes[22] + mi := &file_changes_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1841,7 +1970,7 @@ func (x *ChangeTimelineEntryV2) String() string { func (*ChangeTimelineEntryV2) ProtoMessage() {} func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[22] + mi := &file_changes_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1854,7 +1983,7 @@ func (x *ChangeTimelineEntryV2) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeTimelineEntryV2.ProtoReflect.Descriptor instead. func (*ChangeTimelineEntryV2) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{22} + return file_changes_proto_rawDescGZIP(), []int{24} } func (x *ChangeTimelineEntryV2) GetName() string { @@ -2094,7 +2223,7 @@ type EmptyContent struct { func (x *EmptyContent) Reset() { *x = EmptyContent{} - mi := &file_changes_proto_msgTypes[23] + mi := &file_changes_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2106,7 +2235,7 @@ func (x *EmptyContent) String() string { func (*EmptyContent) ProtoMessage() {} func (x *EmptyContent) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[23] + mi := &file_changes_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2119,7 +2248,7 @@ func (x *EmptyContent) ProtoReflect() protoreflect.Message { // Deprecated: Use EmptyContent.ProtoReflect.Descriptor instead. func (*EmptyContent) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{23} + return file_changes_proto_rawDescGZIP(), []int{25} } // Per-item summary for timeline display - only what the UI needs @@ -2137,7 +2266,7 @@ type MappedItemTimelineSummary struct { func (x *MappedItemTimelineSummary) Reset() { *x = MappedItemTimelineSummary{} - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2149,7 +2278,7 @@ func (x *MappedItemTimelineSummary) String() string { func (*MappedItemTimelineSummary) ProtoMessage() {} func (x *MappedItemTimelineSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[24] + mi := &file_changes_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2162,7 +2291,7 @@ func (x *MappedItemTimelineSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemTimelineSummary.ProtoReflect.Descriptor instead. func (*MappedItemTimelineSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{24} + return file_changes_proto_rawDescGZIP(), []int{26} } func (x *MappedItemTimelineSummary) GetDisplayName() string { @@ -2202,7 +2331,7 @@ type MappedItemsTimelineEntry struct { func (x *MappedItemsTimelineEntry) Reset() { *x = MappedItemsTimelineEntry{} - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2214,7 +2343,7 @@ func (x *MappedItemsTimelineEntry) String() string { func (*MappedItemsTimelineEntry) ProtoMessage() {} func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[25] + mi := &file_changes_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2227,7 +2356,7 @@ func (x *MappedItemsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemsTimelineEntry.ProtoReflect.Descriptor instead. func (*MappedItemsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{25} + return file_changes_proto_rawDescGZIP(), []int{27} } // Deprecated: Marked as deprecated in changes.proto. @@ -2255,7 +2384,7 @@ type CalculatedBlastRadiusTimelineEntry struct { func (x *CalculatedBlastRadiusTimelineEntry) Reset() { *x = CalculatedBlastRadiusTimelineEntry{} - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2267,7 +2396,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) String() string { func (*CalculatedBlastRadiusTimelineEntry) ProtoMessage() {} func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[26] + mi := &file_changes_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2280,7 +2409,7 @@ func (x *CalculatedBlastRadiusTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use CalculatedBlastRadiusTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedBlastRadiusTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{26} + return file_changes_proto_rawDescGZIP(), []int{28} } func (x *CalculatedBlastRadiusTimelineEntry) GetNumItems() uint32 { @@ -2308,7 +2437,7 @@ type RecordObservationsTimelineEntry struct { func (x *RecordObservationsTimelineEntry) Reset() { *x = RecordObservationsTimelineEntry{} - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2320,7 +2449,7 @@ func (x *RecordObservationsTimelineEntry) String() string { func (*RecordObservationsTimelineEntry) ProtoMessage() {} func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[27] + mi := &file_changes_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2333,7 +2462,7 @@ func (x *RecordObservationsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordObservationsTimelineEntry.ProtoReflect.Descriptor instead. func (*RecordObservationsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{27} + return file_changes_proto_rawDescGZIP(), []int{29} } func (x *RecordObservationsTimelineEntry) GetNumObservations() uint32 { @@ -2356,7 +2485,7 @@ type FormHypothesesTimelineEntry struct { func (x *FormHypothesesTimelineEntry) Reset() { *x = FormHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2368,7 +2497,7 @@ func (x *FormHypothesesTimelineEntry) String() string { func (*FormHypothesesTimelineEntry) ProtoMessage() {} func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[28] + mi := &file_changes_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2381,7 +2510,7 @@ func (x *FormHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FormHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*FormHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{28} + return file_changes_proto_rawDescGZIP(), []int{30} } func (x *FormHypothesesTimelineEntry) GetNumHypotheses() uint32 { @@ -2414,7 +2543,7 @@ type InvestigateHypothesesTimelineEntry struct { func (x *InvestigateHypothesesTimelineEntry) Reset() { *x = InvestigateHypothesesTimelineEntry{} - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2426,7 +2555,7 @@ func (x *InvestigateHypothesesTimelineEntry) String() string { func (*InvestigateHypothesesTimelineEntry) ProtoMessage() {} func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[29] + mi := &file_changes_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2439,7 +2568,7 @@ func (x *InvestigateHypothesesTimelineEntry) ProtoReflect() protoreflect.Message // Deprecated: Use InvestigateHypothesesTimelineEntry.ProtoReflect.Descriptor instead. func (*InvestigateHypothesesTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{29} + return file_changes_proto_rawDescGZIP(), []int{31} } func (x *InvestigateHypothesesTimelineEntry) GetNumProven() uint32 { @@ -2488,7 +2617,7 @@ type HypothesisSummary struct { func (x *HypothesisSummary) Reset() { *x = HypothesisSummary{} - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2500,7 +2629,7 @@ func (x *HypothesisSummary) String() string { func (*HypothesisSummary) ProtoMessage() {} func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[30] + mi := &file_changes_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2513,7 +2642,7 @@ func (x *HypothesisSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use HypothesisSummary.ProtoReflect.Descriptor instead. func (*HypothesisSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{30} + return file_changes_proto_rawDescGZIP(), []int{32} } func (x *HypothesisSummary) GetStatus() HypothesisStatus { @@ -2546,7 +2675,7 @@ type CalculatedRisksTimelineEntry struct { func (x *CalculatedRisksTimelineEntry) Reset() { *x = CalculatedRisksTimelineEntry{} - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2558,7 +2687,7 @@ func (x *CalculatedRisksTimelineEntry) String() string { func (*CalculatedRisksTimelineEntry) ProtoMessage() {} func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[31] + mi := &file_changes_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2571,7 +2700,7 @@ func (x *CalculatedRisksTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedRisksTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedRisksTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{31} + return file_changes_proto_rawDescGZIP(), []int{33} } func (x *CalculatedRisksTimelineEntry) GetRisks() []*Risk { @@ -2591,7 +2720,7 @@ type CalculatedLabelsTimelineEntry struct { func (x *CalculatedLabelsTimelineEntry) Reset() { *x = CalculatedLabelsTimelineEntry{} - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2603,7 +2732,7 @@ func (x *CalculatedLabelsTimelineEntry) String() string { func (*CalculatedLabelsTimelineEntry) ProtoMessage() {} func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[32] + mi := &file_changes_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2616,7 +2745,7 @@ func (x *CalculatedLabelsTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CalculatedLabelsTimelineEntry.ProtoReflect.Descriptor instead. func (*CalculatedLabelsTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{32} + return file_changes_proto_rawDescGZIP(), []int{34} } func (x *CalculatedLabelsTimelineEntry) GetLabels() []*Label { @@ -2639,7 +2768,7 @@ type ChangeValidationTimelineEntry struct { func (x *ChangeValidationTimelineEntry) Reset() { *x = ChangeValidationTimelineEntry{} - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2651,7 +2780,7 @@ func (x *ChangeValidationTimelineEntry) String() string { func (*ChangeValidationTimelineEntry) ProtoMessage() {} func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[33] + mi := &file_changes_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2664,7 +2793,7 @@ func (x *ChangeValidationTimelineEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationTimelineEntry.ProtoReflect.Descriptor instead. func (*ChangeValidationTimelineEntry) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{33} + return file_changes_proto_rawDescGZIP(), []int{35} } func (x *ChangeValidationTimelineEntry) GetBriefAnalysis() string { @@ -2696,7 +2825,7 @@ type ChangeValidationCategory struct { func (x *ChangeValidationCategory) Reset() { *x = ChangeValidationCategory{} - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2708,7 +2837,7 @@ func (x *ChangeValidationCategory) String() string { func (*ChangeValidationCategory) ProtoMessage() {} func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[34] + mi := &file_changes_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2721,7 +2850,7 @@ func (x *ChangeValidationCategory) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeValidationCategory.ProtoReflect.Descriptor instead. func (*ChangeValidationCategory) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{34} + return file_changes_proto_rawDescGZIP(), []int{36} } func (x *ChangeValidationCategory) GetTitle() string { @@ -2747,7 +2876,7 @@ type GetDiffRequest struct { func (x *GetDiffRequest) Reset() { *x = GetDiffRequest{} - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2759,7 +2888,7 @@ func (x *GetDiffRequest) String() string { func (*GetDiffRequest) ProtoMessage() {} func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[35] + mi := &file_changes_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2772,7 +2901,7 @@ func (x *GetDiffRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffRequest.ProtoReflect.Descriptor instead. func (*GetDiffRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{35} + return file_changes_proto_rawDescGZIP(), []int{37} } func (x *GetDiffRequest) GetChangeUUID() []byte { @@ -2797,7 +2926,7 @@ type GetDiffResponse struct { func (x *GetDiffResponse) Reset() { *x = GetDiffResponse{} - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2809,7 +2938,7 @@ func (x *GetDiffResponse) String() string { func (*GetDiffResponse) ProtoMessage() {} func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[36] + mi := &file_changes_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2822,7 +2951,7 @@ func (x *GetDiffResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDiffResponse.ProtoReflect.Descriptor instead. func (*GetDiffResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{36} + return file_changes_proto_rawDescGZIP(), []int{38} } func (x *GetDiffResponse) GetExpectedItems() []*ItemDiff { @@ -2862,7 +2991,7 @@ type ListChangingItemsSummaryRequest struct { func (x *ListChangingItemsSummaryRequest) Reset() { *x = ListChangingItemsSummaryRequest{} - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2874,7 +3003,7 @@ func (x *ListChangingItemsSummaryRequest) String() string { func (*ListChangingItemsSummaryRequest) ProtoMessage() {} func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[37] + mi := &file_changes_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2887,7 +3016,7 @@ func (x *ListChangingItemsSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryRequest.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{37} + return file_changes_proto_rawDescGZIP(), []int{39} } func (x *ListChangingItemsSummaryRequest) GetChangeUUID() []byte { @@ -2906,7 +3035,7 @@ type ListChangingItemsSummaryResponse struct { func (x *ListChangingItemsSummaryResponse) Reset() { *x = ListChangingItemsSummaryResponse{} - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2918,7 +3047,7 @@ func (x *ListChangingItemsSummaryResponse) String() string { func (*ListChangingItemsSummaryResponse) ProtoMessage() {} func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[38] + mi := &file_changes_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2931,7 +3060,7 @@ func (x *ListChangingItemsSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangingItemsSummaryResponse.ProtoReflect.Descriptor instead. func (*ListChangingItemsSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{38} + return file_changes_proto_rawDescGZIP(), []int{40} } func (x *ListChangingItemsSummaryResponse) GetItems() []*ItemDiffSummary { @@ -2961,7 +3090,7 @@ type MappedItemDiff struct { func (x *MappedItemDiff) Reset() { *x = MappedItemDiff{} - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2973,7 +3102,7 @@ func (x *MappedItemDiff) String() string { func (*MappedItemDiff) ProtoMessage() {} func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[39] + mi := &file_changes_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2986,7 +3115,7 @@ func (x *MappedItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use MappedItemDiff.ProtoReflect.Descriptor instead. func (*MappedItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{39} + return file_changes_proto_rawDescGZIP(), []int{41} } func (x *MappedItemDiff) GetItem() *ItemDiff { @@ -3033,13 +3162,15 @@ type StartChangeAnalysisRequest struct { RoutineChangesConfigOverride *RoutineChangesConfig `protobuf:"bytes,5,opt,name=routineChangesConfigOverride,proto3,oneof" json:"routineChangesConfigOverride,omitempty"` // github organisation profile to use for this change GithubOrganisationProfileOverride *GithubOrganisationProfile `protobuf:"bytes,6,opt,name=githubOrganisationProfileOverride,proto3,oneof" json:"githubOrganisationProfileOverride,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Knowledge to be used for change analysis + Knowledge []*Knowledge `protobuf:"bytes,7,rep,name=knowledge,proto3" json:"knowledge,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StartChangeAnalysisRequest) Reset() { *x = StartChangeAnalysisRequest{} - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3051,7 +3182,7 @@ func (x *StartChangeAnalysisRequest) String() string { func (*StartChangeAnalysisRequest) ProtoMessage() {} func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[40] + mi := &file_changes_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3064,7 +3195,7 @@ func (x *StartChangeAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisRequest.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{40} + return file_changes_proto_rawDescGZIP(), []int{42} } func (x *StartChangeAnalysisRequest) GetChangeUUID() []byte { @@ -3102,6 +3233,13 @@ func (x *StartChangeAnalysisRequest) GetGithubOrganisationProfileOverride() *Git return nil } +func (x *StartChangeAnalysisRequest) GetKnowledge() []*Knowledge { + if x != nil { + return x.Knowledge + } + return nil +} + // StartChangeAnalysisResponse is used to signal that the change analysis has been successfully started // we use HTTP response codes to signal errors type StartChangeAnalysisResponse struct { @@ -3112,7 +3250,7 @@ type StartChangeAnalysisResponse struct { func (x *StartChangeAnalysisResponse) Reset() { *x = StartChangeAnalysisResponse{} - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3124,7 +3262,7 @@ func (x *StartChangeAnalysisResponse) String() string { func (*StartChangeAnalysisResponse) ProtoMessage() {} func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[41] + mi := &file_changes_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3137,7 +3275,7 @@ func (x *StartChangeAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeAnalysisResponse.ProtoReflect.Descriptor instead. func (*StartChangeAnalysisResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{41} + return file_changes_proto_rawDescGZIP(), []int{43} } type ListHomeChangesRequest struct { @@ -3150,7 +3288,7 @@ type ListHomeChangesRequest struct { func (x *ListHomeChangesRequest) Reset() { *x = ListHomeChangesRequest{} - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3162,7 +3300,7 @@ func (x *ListHomeChangesRequest) String() string { func (*ListHomeChangesRequest) ProtoMessage() {} func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[42] + mi := &file_changes_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3175,7 +3313,7 @@ func (x *ListHomeChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesRequest.ProtoReflect.Descriptor instead. func (*ListHomeChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{42} + return file_changes_proto_rawDescGZIP(), []int{44} } func (x *ListHomeChangesRequest) GetPagination() *PaginationRequest { @@ -3209,7 +3347,7 @@ type ChangeFiltersRequest struct { func (x *ChangeFiltersRequest) Reset() { *x = ChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3221,7 +3359,7 @@ func (x *ChangeFiltersRequest) String() string { func (*ChangeFiltersRequest) ProtoMessage() {} func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[43] + mi := &file_changes_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3234,7 +3372,7 @@ func (x *ChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*ChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{43} + return file_changes_proto_rawDescGZIP(), []int{45} } func (x *ChangeFiltersRequest) GetRepos() []string { @@ -3289,7 +3427,7 @@ type ListHomeChangesResponse struct { func (x *ListHomeChangesResponse) Reset() { *x = ListHomeChangesResponse{} - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3301,7 +3439,7 @@ func (x *ListHomeChangesResponse) String() string { func (*ListHomeChangesResponse) ProtoMessage() {} func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[44] + mi := &file_changes_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3314,7 +3452,7 @@ func (x *ListHomeChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHomeChangesResponse.ProtoReflect.Descriptor instead. func (*ListHomeChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{44} + return file_changes_proto_rawDescGZIP(), []int{46} } func (x *ListHomeChangesResponse) GetChanges() []*ChangeSummary { @@ -3339,7 +3477,7 @@ type PopulateChangeFiltersRequest struct { func (x *PopulateChangeFiltersRequest) Reset() { *x = PopulateChangeFiltersRequest{} - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3351,7 +3489,7 @@ func (x *PopulateChangeFiltersRequest) String() string { func (*PopulateChangeFiltersRequest) ProtoMessage() {} func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[45] + mi := &file_changes_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3364,7 +3502,7 @@ func (x *PopulateChangeFiltersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersRequest.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{45} + return file_changes_proto_rawDescGZIP(), []int{47} } type PopulateChangeFiltersResponse struct { @@ -3377,7 +3515,7 @@ type PopulateChangeFiltersResponse struct { func (x *PopulateChangeFiltersResponse) Reset() { *x = PopulateChangeFiltersResponse{} - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3389,7 +3527,7 @@ func (x *PopulateChangeFiltersResponse) String() string { func (*PopulateChangeFiltersResponse) ProtoMessage() {} func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[46] + mi := &file_changes_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3402,7 +3540,7 @@ func (x *PopulateChangeFiltersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PopulateChangeFiltersResponse.ProtoReflect.Descriptor instead. func (*PopulateChangeFiltersResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{46} + return file_changes_proto_rawDescGZIP(), []int{48} } func (x *PopulateChangeFiltersResponse) GetRepos() []string { @@ -3433,7 +3571,7 @@ type ItemDiffSummary struct { func (x *ItemDiffSummary) Reset() { *x = ItemDiffSummary{} - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3445,7 +3583,7 @@ func (x *ItemDiffSummary) String() string { func (*ItemDiffSummary) ProtoMessage() {} func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[47] + mi := &file_changes_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3458,7 +3596,7 @@ func (x *ItemDiffSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiffSummary.ProtoReflect.Descriptor instead. func (*ItemDiffSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{47} + return file_changes_proto_rawDescGZIP(), []int{49} } func (x *ItemDiffSummary) GetItem() *Reference { @@ -3500,7 +3638,7 @@ type ItemDiff struct { func (x *ItemDiff) Reset() { *x = ItemDiff{} - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3512,7 +3650,7 @@ func (x *ItemDiff) String() string { func (*ItemDiff) ProtoMessage() {} func (x *ItemDiff) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[48] + mi := &file_changes_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3525,7 +3663,7 @@ func (x *ItemDiff) ProtoReflect() protoreflect.Message { // Deprecated: Use ItemDiff.ProtoReflect.Descriptor instead. func (*ItemDiff) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{48} + return file_changes_proto_rawDescGZIP(), []int{50} } func (x *ItemDiff) GetItem() *Reference { @@ -3572,7 +3710,7 @@ type EnrichedTags struct { func (x *EnrichedTags) Reset() { *x = EnrichedTags{} - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3584,7 +3722,7 @@ func (x *EnrichedTags) String() string { func (*EnrichedTags) ProtoMessage() {} func (x *EnrichedTags) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[49] + mi := &file_changes_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3597,7 +3735,7 @@ func (x *EnrichedTags) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrichedTags.ProtoReflect.Descriptor instead. func (*EnrichedTags) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{49} + return file_changes_proto_rawDescGZIP(), []int{51} } func (x *EnrichedTags) GetTagValue() map[string]*TagValue { @@ -3622,7 +3760,7 @@ type TagValue struct { func (x *TagValue) Reset() { *x = TagValue{} - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3634,7 +3772,7 @@ func (x *TagValue) String() string { func (*TagValue) ProtoMessage() {} func (x *TagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[50] + mi := &file_changes_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3647,7 +3785,7 @@ func (x *TagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use TagValue.ProtoReflect.Descriptor instead. func (*TagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{50} + return file_changes_proto_rawDescGZIP(), []int{52} } func (x *TagValue) GetValue() isTagValue_Value { @@ -3701,7 +3839,7 @@ type UserTagValue struct { func (x *UserTagValue) Reset() { *x = UserTagValue{} - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3713,7 +3851,7 @@ func (x *UserTagValue) String() string { func (*UserTagValue) ProtoMessage() {} func (x *UserTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[51] + mi := &file_changes_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3726,7 +3864,7 @@ func (x *UserTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use UserTagValue.ProtoReflect.Descriptor instead. func (*UserTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{51} + return file_changes_proto_rawDescGZIP(), []int{53} } func (x *UserTagValue) GetValue() string { @@ -3748,7 +3886,7 @@ type AutoTagValue struct { func (x *AutoTagValue) Reset() { *x = AutoTagValue{} - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3760,7 +3898,7 @@ func (x *AutoTagValue) String() string { func (*AutoTagValue) ProtoMessage() {} func (x *AutoTagValue) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[52] + mi := &file_changes_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3773,7 +3911,7 @@ func (x *AutoTagValue) ProtoReflect() protoreflect.Message { // Deprecated: Use AutoTagValue.ProtoReflect.Descriptor instead. func (*AutoTagValue) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{52} + return file_changes_proto_rawDescGZIP(), []int{54} } func (x *AutoTagValue) GetValue() string { @@ -3816,7 +3954,7 @@ type Label struct { func (x *Label) Reset() { *x = Label{} - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3828,7 +3966,7 @@ func (x *Label) String() string { func (*Label) ProtoMessage() {} func (x *Label) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[53] + mi := &file_changes_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3841,7 +3979,7 @@ func (x *Label) ProtoReflect() protoreflect.Message { // Deprecated: Use Label.ProtoReflect.Descriptor instead. func (*Label) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{53} + return file_changes_proto_rawDescGZIP(), []int{55} } func (x *Label) GetType() LabelType { @@ -3940,7 +4078,7 @@ type ChangeSummary struct { func (x *ChangeSummary) Reset() { *x = ChangeSummary{} - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3952,7 +4090,7 @@ func (x *ChangeSummary) String() string { func (*ChangeSummary) ProtoMessage() {} func (x *ChangeSummary) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[54] + mi := &file_changes_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3965,7 +4103,7 @@ func (x *ChangeSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeSummary.ProtoReflect.Descriptor instead. func (*ChangeSummary) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{54} + return file_changes_proto_rawDescGZIP(), []int{56} } func (x *ChangeSummary) GetUUID() []byte { @@ -4108,7 +4246,7 @@ type Change struct { func (x *Change) Reset() { *x = Change{} - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4120,7 +4258,7 @@ func (x *Change) String() string { func (*Change) ProtoMessage() {} func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[55] + mi := &file_changes_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4133,7 +4271,7 @@ func (x *Change) ProtoReflect() protoreflect.Message { // Deprecated: Use Change.ProtoReflect.Descriptor instead. func (*Change) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{55} + return file_changes_proto_rawDescGZIP(), []int{57} } func (x *Change) GetMetadata() *ChangeMetadata { @@ -4201,7 +4339,7 @@ type ChangeMetadata struct { func (x *ChangeMetadata) Reset() { *x = ChangeMetadata{} - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4213,7 +4351,7 @@ func (x *ChangeMetadata) String() string { func (*ChangeMetadata) ProtoMessage() {} func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[56] + mi := &file_changes_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4226,7 +4364,7 @@ func (x *ChangeMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMetadata.ProtoReflect.Descriptor instead. func (*ChangeMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56} + return file_changes_proto_rawDescGZIP(), []int{58} } func (x *ChangeMetadata) GetUUID() []byte { @@ -4431,7 +4569,7 @@ type ChangeProperties struct { func (x *ChangeProperties) Reset() { *x = ChangeProperties{} - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4443,7 +4581,7 @@ func (x *ChangeProperties) String() string { func (*ChangeProperties) ProtoMessage() {} func (x *ChangeProperties) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[57] + mi := &file_changes_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4456,7 +4594,7 @@ func (x *ChangeProperties) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeProperties.ProtoReflect.Descriptor instead. func (*ChangeProperties) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{57} + return file_changes_proto_rawDescGZIP(), []int{59} } func (x *ChangeProperties) GetTitle() string { @@ -4590,7 +4728,7 @@ type GithubChangeInfo struct { func (x *GithubChangeInfo) Reset() { *x = GithubChangeInfo{} - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4602,7 +4740,7 @@ func (x *GithubChangeInfo) String() string { func (*GithubChangeInfo) ProtoMessage() {} func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[58] + mi := &file_changes_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4615,7 +4753,7 @@ func (x *GithubChangeInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use GithubChangeInfo.ProtoReflect.Descriptor instead. func (*GithubChangeInfo) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{58} + return file_changes_proto_rawDescGZIP(), []int{60} } func (x *GithubChangeInfo) GetAuthorUsername() string { @@ -4655,7 +4793,7 @@ type ListChangesRequest struct { func (x *ListChangesRequest) Reset() { *x = ListChangesRequest{} - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4667,7 +4805,7 @@ func (x *ListChangesRequest) String() string { func (*ListChangesRequest) ProtoMessage() {} func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[59] + mi := &file_changes_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4680,7 +4818,7 @@ func (x *ListChangesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesRequest.ProtoReflect.Descriptor instead. func (*ListChangesRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{59} + return file_changes_proto_rawDescGZIP(), []int{61} } type ListChangesResponse struct { @@ -4692,7 +4830,7 @@ type ListChangesResponse struct { func (x *ListChangesResponse) Reset() { *x = ListChangesResponse{} - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4704,7 +4842,7 @@ func (x *ListChangesResponse) String() string { func (*ListChangesResponse) ProtoMessage() {} func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[60] + mi := &file_changes_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4717,7 +4855,7 @@ func (x *ListChangesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesResponse.ProtoReflect.Descriptor instead. func (*ListChangesResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{60} + return file_changes_proto_rawDescGZIP(), []int{62} } func (x *ListChangesResponse) GetChanges() []*Change { @@ -4737,7 +4875,7 @@ type ListChangesByStatusRequest struct { func (x *ListChangesByStatusRequest) Reset() { *x = ListChangesByStatusRequest{} - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4749,7 +4887,7 @@ func (x *ListChangesByStatusRequest) String() string { func (*ListChangesByStatusRequest) ProtoMessage() {} func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[61] + mi := &file_changes_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4762,7 +4900,7 @@ func (x *ListChangesByStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusRequest.ProtoReflect.Descriptor instead. func (*ListChangesByStatusRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{61} + return file_changes_proto_rawDescGZIP(), []int{63} } func (x *ListChangesByStatusRequest) GetStatus() ChangeStatus { @@ -4781,7 +4919,7 @@ type ListChangesByStatusResponse struct { func (x *ListChangesByStatusResponse) Reset() { *x = ListChangesByStatusResponse{} - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4793,7 +4931,7 @@ func (x *ListChangesByStatusResponse) String() string { func (*ListChangesByStatusResponse) ProtoMessage() {} func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[62] + mi := &file_changes_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4806,7 +4944,7 @@ func (x *ListChangesByStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesByStatusResponse.ProtoReflect.Descriptor instead. func (*ListChangesByStatusResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{62} + return file_changes_proto_rawDescGZIP(), []int{64} } func (x *ListChangesByStatusResponse) GetChanges() []*Change { @@ -4826,7 +4964,7 @@ type CreateChangeRequest struct { func (x *CreateChangeRequest) Reset() { *x = CreateChangeRequest{} - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4838,7 +4976,7 @@ func (x *CreateChangeRequest) String() string { func (*CreateChangeRequest) ProtoMessage() {} func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[63] + mi := &file_changes_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4851,7 +4989,7 @@ func (x *CreateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeRequest.ProtoReflect.Descriptor instead. func (*CreateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{63} + return file_changes_proto_rawDescGZIP(), []int{65} } func (x *CreateChangeRequest) GetProperties() *ChangeProperties { @@ -4870,7 +5008,7 @@ type CreateChangeResponse struct { func (x *CreateChangeResponse) Reset() { *x = CreateChangeResponse{} - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4882,7 +5020,7 @@ func (x *CreateChangeResponse) String() string { func (*CreateChangeResponse) ProtoMessage() {} func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[64] + mi := &file_changes_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4895,7 +5033,7 @@ func (x *CreateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChangeResponse.ProtoReflect.Descriptor instead. func (*CreateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{64} + return file_changes_proto_rawDescGZIP(), []int{66} } func (x *CreateChangeResponse) GetChange() *Change { @@ -4920,7 +5058,7 @@ type GetChangeRequest struct { func (x *GetChangeRequest) Reset() { *x = GetChangeRequest{} - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4932,7 +5070,7 @@ func (x *GetChangeRequest) String() string { func (*GetChangeRequest) ProtoMessage() {} func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[65] + mi := &file_changes_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4945,7 +5083,7 @@ func (x *GetChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRequest.ProtoReflect.Descriptor instead. func (*GetChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{65} + return file_changes_proto_rawDescGZIP(), []int{67} } func (x *GetChangeRequest) GetUUID() []byte { @@ -4971,7 +5109,7 @@ type GetChangeByTicketLinkRequest struct { func (x *GetChangeByTicketLinkRequest) Reset() { *x = GetChangeByTicketLinkRequest{} - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4983,7 +5121,7 @@ func (x *GetChangeByTicketLinkRequest) String() string { func (*GetChangeByTicketLinkRequest) ProtoMessage() {} func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[66] + mi := &file_changes_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4996,7 +5134,7 @@ func (x *GetChangeByTicketLinkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeByTicketLinkRequest.ProtoReflect.Descriptor instead. func (*GetChangeByTicketLinkRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{66} + return file_changes_proto_rawDescGZIP(), []int{68} } func (x *GetChangeByTicketLinkRequest) GetTicketLink() string { @@ -5026,7 +5164,7 @@ type GetChangeSummaryRequest struct { func (x *GetChangeSummaryRequest) Reset() { *x = GetChangeSummaryRequest{} - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5038,7 +5176,7 @@ func (x *GetChangeSummaryRequest) String() string { func (*GetChangeSummaryRequest) ProtoMessage() {} func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[67] + mi := &file_changes_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5051,7 +5189,7 @@ func (x *GetChangeSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryRequest.ProtoReflect.Descriptor instead. func (*GetChangeSummaryRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{67} + return file_changes_proto_rawDescGZIP(), []int{69} } func (x *GetChangeSummaryRequest) GetUUID() []byte { @@ -5098,7 +5236,7 @@ type GetChangeSummaryResponse struct { func (x *GetChangeSummaryResponse) Reset() { *x = GetChangeSummaryResponse{} - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5110,7 +5248,7 @@ func (x *GetChangeSummaryResponse) String() string { func (*GetChangeSummaryResponse) ProtoMessage() {} func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[68] + mi := &file_changes_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5123,7 +5261,7 @@ func (x *GetChangeSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSummaryResponse.ProtoReflect.Descriptor instead. func (*GetChangeSummaryResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{68} + return file_changes_proto_rawDescGZIP(), []int{70} } func (x *GetChangeSummaryResponse) GetChange() string { @@ -5144,7 +5282,7 @@ type GetChangeSignalsRequest struct { func (x *GetChangeSignalsRequest) Reset() { *x = GetChangeSignalsRequest{} - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5156,7 +5294,7 @@ func (x *GetChangeSignalsRequest) String() string { func (*GetChangeSignalsRequest) ProtoMessage() {} func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[69] + mi := &file_changes_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5169,7 +5307,7 @@ func (x *GetChangeSignalsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsRequest.ProtoReflect.Descriptor instead. func (*GetChangeSignalsRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{69} + return file_changes_proto_rawDescGZIP(), []int{71} } func (x *GetChangeSignalsRequest) GetUUID() []byte { @@ -5195,7 +5333,7 @@ type GetChangeSignalsResponse struct { func (x *GetChangeSignalsResponse) Reset() { *x = GetChangeSignalsResponse{} - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5207,7 +5345,7 @@ func (x *GetChangeSignalsResponse) String() string { func (*GetChangeSignalsResponse) ProtoMessage() {} func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[70] + mi := &file_changes_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5220,7 +5358,7 @@ func (x *GetChangeSignalsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeSignalsResponse.ProtoReflect.Descriptor instead. func (*GetChangeSignalsResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{70} + return file_changes_proto_rawDescGZIP(), []int{72} } func (x *GetChangeSignalsResponse) GetSignals() string { @@ -5239,7 +5377,7 @@ type GetChangeResponse struct { func (x *GetChangeResponse) Reset() { *x = GetChangeResponse{} - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5251,7 +5389,7 @@ func (x *GetChangeResponse) String() string { func (*GetChangeResponse) ProtoMessage() {} func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[71] + mi := &file_changes_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5264,7 +5402,7 @@ func (x *GetChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeResponse.ProtoReflect.Descriptor instead. func (*GetChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{71} + return file_changes_proto_rawDescGZIP(), []int{73} } func (x *GetChangeResponse) GetChange() *Change { @@ -5284,7 +5422,7 @@ type GetChangeRisksRequest struct { func (x *GetChangeRisksRequest) Reset() { *x = GetChangeRisksRequest{} - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5296,7 +5434,7 @@ func (x *GetChangeRisksRequest) String() string { func (*GetChangeRisksRequest) ProtoMessage() {} func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[72] + mi := &file_changes_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5309,7 +5447,7 @@ func (x *GetChangeRisksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksRequest.ProtoReflect.Descriptor instead. func (*GetChangeRisksRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{72} + return file_changes_proto_rawDescGZIP(), []int{74} } func (x *GetChangeRisksRequest) GetUUID() []byte { @@ -5337,7 +5475,7 @@ type ChangeRiskMetadata struct { func (x *ChangeRiskMetadata) Reset() { *x = ChangeRiskMetadata{} - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5349,7 +5487,7 @@ func (x *ChangeRiskMetadata) String() string { func (*ChangeRiskMetadata) ProtoMessage() {} func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[73] + mi := &file_changes_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5362,7 +5500,7 @@ func (x *ChangeRiskMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeRiskMetadata.ProtoReflect.Descriptor instead. func (*ChangeRiskMetadata) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{73} + return file_changes_proto_rawDescGZIP(), []int{75} } func (x *ChangeRiskMetadata) GetChangeAnalysisStatus() *ChangeAnalysisStatus { @@ -5409,7 +5547,7 @@ type GetChangeRisksResponse struct { func (x *GetChangeRisksResponse) Reset() { *x = GetChangeRisksResponse{} - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5421,7 +5559,7 @@ func (x *GetChangeRisksResponse) String() string { func (*GetChangeRisksResponse) ProtoMessage() {} func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[74] + mi := &file_changes_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5434,7 +5572,7 @@ func (x *GetChangeRisksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetChangeRisksResponse.ProtoReflect.Descriptor instead. func (*GetChangeRisksResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{74} + return file_changes_proto_rawDescGZIP(), []int{76} } func (x *GetChangeRisksResponse) GetChangeRiskMetadata() *ChangeRiskMetadata { @@ -5455,7 +5593,7 @@ type UpdateChangeRequest struct { func (x *UpdateChangeRequest) Reset() { *x = UpdateChangeRequest{} - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5467,7 +5605,7 @@ func (x *UpdateChangeRequest) String() string { func (*UpdateChangeRequest) ProtoMessage() {} func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[75] + mi := &file_changes_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5480,7 +5618,7 @@ func (x *UpdateChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeRequest.ProtoReflect.Descriptor instead. func (*UpdateChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{75} + return file_changes_proto_rawDescGZIP(), []int{77} } func (x *UpdateChangeRequest) GetUUID() []byte { @@ -5506,7 +5644,7 @@ type UpdateChangeResponse struct { func (x *UpdateChangeResponse) Reset() { *x = UpdateChangeResponse{} - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5518,7 +5656,7 @@ func (x *UpdateChangeResponse) String() string { func (*UpdateChangeResponse) ProtoMessage() {} func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[76] + mi := &file_changes_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5531,7 +5669,7 @@ func (x *UpdateChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChangeResponse.ProtoReflect.Descriptor instead. func (*UpdateChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{76} + return file_changes_proto_rawDescGZIP(), []int{78} } func (x *UpdateChangeResponse) GetChange() *Change { @@ -5551,7 +5689,7 @@ type DeleteChangeRequest struct { func (x *DeleteChangeRequest) Reset() { *x = DeleteChangeRequest{} - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5563,7 +5701,7 @@ func (x *DeleteChangeRequest) String() string { func (*DeleteChangeRequest) ProtoMessage() {} func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[77] + mi := &file_changes_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5576,7 +5714,7 @@ func (x *DeleteChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeRequest.ProtoReflect.Descriptor instead. func (*DeleteChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{77} + return file_changes_proto_rawDescGZIP(), []int{79} } func (x *DeleteChangeRequest) GetUUID() []byte { @@ -5596,7 +5734,7 @@ type ListChangesBySnapshotUUIDRequest struct { func (x *ListChangesBySnapshotUUIDRequest) Reset() { *x = ListChangesBySnapshotUUIDRequest{} - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5608,7 +5746,7 @@ func (x *ListChangesBySnapshotUUIDRequest) String() string { func (*ListChangesBySnapshotUUIDRequest) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[78] + mi := &file_changes_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5621,7 +5759,7 @@ func (x *ListChangesBySnapshotUUIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChangesBySnapshotUUIDRequest.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{78} + return file_changes_proto_rawDescGZIP(), []int{80} } func (x *ListChangesBySnapshotUUIDRequest) GetUUID() []byte { @@ -5640,7 +5778,7 @@ type ListChangesBySnapshotUUIDResponse struct { func (x *ListChangesBySnapshotUUIDResponse) Reset() { *x = ListChangesBySnapshotUUIDResponse{} - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5652,7 +5790,7 @@ func (x *ListChangesBySnapshotUUIDResponse) String() string { func (*ListChangesBySnapshotUUIDResponse) ProtoMessage() {} func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[79] + mi := &file_changes_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5665,7 +5803,7 @@ func (x *ListChangesBySnapshotUUIDResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListChangesBySnapshotUUIDResponse.ProtoReflect.Descriptor instead. func (*ListChangesBySnapshotUUIDResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{79} + return file_changes_proto_rawDescGZIP(), []int{81} } func (x *ListChangesBySnapshotUUIDResponse) GetChanges() []*Change { @@ -5683,7 +5821,7 @@ type DeleteChangeResponse struct { func (x *DeleteChangeResponse) Reset() { *x = DeleteChangeResponse{} - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5695,7 +5833,7 @@ func (x *DeleteChangeResponse) String() string { func (*DeleteChangeResponse) ProtoMessage() {} func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[80] + mi := &file_changes_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5708,7 +5846,7 @@ func (x *DeleteChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteChangeResponse.ProtoReflect.Descriptor instead. func (*DeleteChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{80} + return file_changes_proto_rawDescGZIP(), []int{82} } type RefreshStateRequest struct { @@ -5719,7 +5857,7 @@ type RefreshStateRequest struct { func (x *RefreshStateRequest) Reset() { *x = RefreshStateRequest{} - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5731,7 +5869,7 @@ func (x *RefreshStateRequest) String() string { func (*RefreshStateRequest) ProtoMessage() {} func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[81] + mi := &file_changes_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5744,7 +5882,7 @@ func (x *RefreshStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateRequest.ProtoReflect.Descriptor instead. func (*RefreshStateRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{81} + return file_changes_proto_rawDescGZIP(), []int{83} } type RefreshStateResponse struct { @@ -5755,7 +5893,7 @@ type RefreshStateResponse struct { func (x *RefreshStateResponse) Reset() { *x = RefreshStateResponse{} - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5767,7 +5905,7 @@ func (x *RefreshStateResponse) String() string { func (*RefreshStateResponse) ProtoMessage() {} func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[82] + mi := &file_changes_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5780,7 +5918,7 @@ func (x *RefreshStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshStateResponse.ProtoReflect.Descriptor instead. func (*RefreshStateResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{82} + return file_changes_proto_rawDescGZIP(), []int{84} } type StartChangeRequest struct { @@ -5792,7 +5930,7 @@ type StartChangeRequest struct { func (x *StartChangeRequest) Reset() { *x = StartChangeRequest{} - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5804,7 +5942,7 @@ func (x *StartChangeRequest) String() string { func (*StartChangeRequest) ProtoMessage() {} func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[83] + mi := &file_changes_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5817,7 +5955,7 @@ func (x *StartChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeRequest.ProtoReflect.Descriptor instead. func (*StartChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{83} + return file_changes_proto_rawDescGZIP(), []int{85} } func (x *StartChangeRequest) GetChangeUUID() []byte { @@ -5838,7 +5976,7 @@ type StartChangeResponse struct { func (x *StartChangeResponse) Reset() { *x = StartChangeResponse{} - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5850,7 +5988,7 @@ func (x *StartChangeResponse) String() string { func (*StartChangeResponse) ProtoMessage() {} func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[84] + mi := &file_changes_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5863,7 +6001,7 @@ func (x *StartChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeResponse.ProtoReflect.Descriptor instead. func (*StartChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{84} + return file_changes_proto_rawDescGZIP(), []int{86} } func (x *StartChangeResponse) GetState() StartChangeResponse_State { @@ -5896,7 +6034,7 @@ type EndChangeRequest struct { func (x *EndChangeRequest) Reset() { *x = EndChangeRequest{} - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5908,7 +6046,7 @@ func (x *EndChangeRequest) String() string { func (*EndChangeRequest) ProtoMessage() {} func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[85] + mi := &file_changes_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5921,7 +6059,7 @@ func (x *EndChangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeRequest.ProtoReflect.Descriptor instead. func (*EndChangeRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{85} + return file_changes_proto_rawDescGZIP(), []int{87} } func (x *EndChangeRequest) GetChangeUUID() []byte { @@ -5942,7 +6080,7 @@ type EndChangeResponse struct { func (x *EndChangeResponse) Reset() { *x = EndChangeResponse{} - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5954,7 +6092,7 @@ func (x *EndChangeResponse) String() string { func (*EndChangeResponse) ProtoMessage() {} func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[86] + mi := &file_changes_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5967,7 +6105,7 @@ func (x *EndChangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeResponse.ProtoReflect.Descriptor instead. func (*EndChangeResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{86} + return file_changes_proto_rawDescGZIP(), []int{88} } func (x *EndChangeResponse) GetState() EndChangeResponse_State { @@ -5999,7 +6137,7 @@ type StartChangeSimpleResponse struct { func (x *StartChangeSimpleResponse) Reset() { *x = StartChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6011,7 +6149,7 @@ func (x *StartChangeSimpleResponse) String() string { func (*StartChangeSimpleResponse) ProtoMessage() {} func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[87] + mi := &file_changes_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6024,7 +6162,7 @@ func (x *StartChangeSimpleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartChangeSimpleResponse.ProtoReflect.Descriptor instead. func (*StartChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{87} + return file_changes_proto_rawDescGZIP(), []int{89} } type EndChangeSimpleResponse struct { @@ -6039,7 +6177,7 @@ type EndChangeSimpleResponse struct { func (x *EndChangeSimpleResponse) Reset() { *x = EndChangeSimpleResponse{} - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6051,7 +6189,7 @@ func (x *EndChangeSimpleResponse) String() string { func (*EndChangeSimpleResponse) ProtoMessage() {} func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[88] + mi := &file_changes_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6064,7 +6202,7 @@ func (x *EndChangeSimpleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndChangeSimpleResponse.ProtoReflect.Descriptor instead. func (*EndChangeSimpleResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{88} + return file_changes_proto_rawDescGZIP(), []int{90} } func (x *EndChangeSimpleResponse) GetQueued() bool { @@ -6094,7 +6232,7 @@ type Risk struct { func (x *Risk) Reset() { *x = Risk{} - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6106,7 +6244,7 @@ func (x *Risk) String() string { func (*Risk) ProtoMessage() {} func (x *Risk) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[89] + mi := &file_changes_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6119,7 +6257,7 @@ func (x *Risk) ProtoReflect() protoreflect.Message { // Deprecated: Use Risk.ProtoReflect.Descriptor instead. func (*Risk) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{89} + return file_changes_proto_rawDescGZIP(), []int{91} } func (x *Risk) GetUUID() []byte { @@ -6166,7 +6304,7 @@ type ChangeAnalysisStatus struct { func (x *ChangeAnalysisStatus) Reset() { *x = ChangeAnalysisStatus{} - mi := &file_changes_proto_msgTypes[90] + mi := &file_changes_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6178,7 +6316,7 @@ func (x *ChangeAnalysisStatus) String() string { func (*ChangeAnalysisStatus) ProtoMessage() {} func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[90] + mi := &file_changes_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6191,7 +6329,7 @@ func (x *ChangeAnalysisStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeAnalysisStatus.ProtoReflect.Descriptor instead. func (*ChangeAnalysisStatus) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{90} + return file_changes_proto_rawDescGZIP(), []int{92} } func (x *ChangeAnalysisStatus) GetStatus() ChangeAnalysisStatus_Status { @@ -6212,7 +6350,7 @@ type GenerateRiskFixRequest struct { func (x *GenerateRiskFixRequest) Reset() { *x = GenerateRiskFixRequest{} - mi := &file_changes_proto_msgTypes[91] + mi := &file_changes_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6224,7 +6362,7 @@ func (x *GenerateRiskFixRequest) String() string { func (*GenerateRiskFixRequest) ProtoMessage() {} func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[91] + mi := &file_changes_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6237,7 +6375,7 @@ func (x *GenerateRiskFixRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixRequest.ProtoReflect.Descriptor instead. func (*GenerateRiskFixRequest) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{91} + return file_changes_proto_rawDescGZIP(), []int{93} } func (x *GenerateRiskFixRequest) GetRiskUUID() []byte { @@ -6257,7 +6395,7 @@ type GenerateRiskFixResponse struct { func (x *GenerateRiskFixResponse) Reset() { *x = GenerateRiskFixResponse{} - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6269,7 +6407,7 @@ func (x *GenerateRiskFixResponse) String() string { func (*GenerateRiskFixResponse) ProtoMessage() {} func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[92] + mi := &file_changes_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6282,7 +6420,7 @@ func (x *GenerateRiskFixResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateRiskFixResponse.ProtoReflect.Descriptor instead. func (*GenerateRiskFixResponse) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{92} + return file_changes_proto_rawDescGZIP(), []int{94} } func (x *GenerateRiskFixResponse) GetFixSuggestion() string { @@ -6312,7 +6450,7 @@ type ChangeMetadata_HealthChange struct { func (x *ChangeMetadata_HealthChange) Reset() { *x = ChangeMetadata_HealthChange{} - mi := &file_changes_proto_msgTypes[95] + mi := &file_changes_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6324,7 +6462,7 @@ func (x *ChangeMetadata_HealthChange) String() string { func (*ChangeMetadata_HealthChange) ProtoMessage() {} func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { - mi := &file_changes_proto_msgTypes[95] + mi := &file_changes_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6337,7 +6475,7 @@ func (x *ChangeMetadata_HealthChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMetadata_HealthChange.ProtoReflect.Descriptor instead. func (*ChangeMetadata_HealthChange) Descriptor() ([]byte, []int) { - return file_changes_proto_rawDescGZIP(), []int{56, 0} + return file_changes_proto_rawDescGZIP(), []int{58, 0} } func (x *ChangeMetadata_HealthChange) GetAdded() int32 { @@ -6423,7 +6561,15 @@ const file_changes_proto_rawDesc = "" + "#ReapplyLabelRuleInTimeRangeResponse\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x03(\fR\n" + - "changeUUID\"=\n" + + "changeUUID\"D\n" + + "\x12KnowledgeReference\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + + "\bfileName\x18\x02 \x01(\tR\bfileName\"w\n" + + "\tKnowledge\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x1a\n" + + "\bfileName\x18\x04 \x01(\tR\bfileName\"=\n" + "\x1bGetHypothesesDetailsRequest\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6431,13 +6577,14 @@ const file_changes_proto_rawDesc = "" + "\x1cGetHypothesesDetailsResponse\x12:\n" + "\n" + "hypotheses\x18\x01 \x03(\v2\x1a.changes.HypothesesDetailsR\n" + - "hypotheses\"\xd2\x01\n" + + "hypotheses\"\x95\x02\n" + "\x11HypothesesDetails\x12\x14\n" + "\x05title\x18\x01 \x01(\tR\x05title\x12(\n" + "\x0fnumObservations\x18\x02 \x01(\rR\x0fnumObservations\x12\x16\n" + "\x06detail\x18\x03 \x01(\tR\x06detail\x121\n" + "\x06status\x18\x04 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x122\n" + - "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\"<\n" + + "\x14investigationResults\x18\x05 \x01(\tR\x14investigationResults\x12A\n" + + "\rknowledgeUsed\x18\x06 \x03(\v2\x1b.changes.KnowledgeReferenceR\rknowledgeUsed\"<\n" + "\x1aGetChangeTimelineV2Request\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6529,7 +6676,7 @@ const file_changes_proto_rawDesc = "" + "\x0emapping_status\x18\x04 \x01(\x0e2 .changes.MappedItemMappingStatusH\x02R\rmappingStatus\x88\x01\x01B\x0f\n" + "\r_mappingQueryB\x0f\n" + "\r_mappingErrorB\x11\n" + - "\x0f_mapping_status\"\xa1\x04\n" + + "\x0f_mapping_status\"\xd3\x04\n" + "\x1aStartChangeAnalysisRequest\x12\x1e\n" + "\n" + "changeUUID\x18\x01 \x01(\fR\n" + @@ -6537,7 +6684,8 @@ const file_changes_proto_rawDesc = "" + "\rchangingItems\x18\x02 \x03(\v2\x17.changes.MappedItemDiffR\rchangingItems\x12\\\n" + "\x19blastRadiusConfigOverride\x18\x03 \x01(\v2\x19.config.BlastRadiusConfigH\x00R\x19blastRadiusConfigOverride\x88\x01\x01\x12e\n" + "\x1croutineChangesConfigOverride\x18\x05 \x01(\v2\x1c.config.RoutineChangesConfigH\x01R\x1croutineChangesConfigOverride\x88\x01\x01\x12t\n" + - "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01B\x1c\n" + + "!githubOrganisationProfileOverride\x18\x06 \x01(\v2!.config.GithubOrganisationProfileH\x02R!githubOrganisationProfileOverride\x88\x01\x01\x120\n" + + "\tknowledge\x18\a \x03(\v2\x12.changes.KnowledgeR\tknowledgeB\x1c\n" + "\x1a_blastRadiusConfigOverrideB\x1f\n" + "\x1d_routineChangesConfigOverrideB$\n" + "\"_githubOrganisationProfileOverrideJ\x04\b\x04\x10\x05\"\x1d\n" + @@ -6904,7 +7052,7 @@ func file_changes_proto_rawDescGZIP() []byte { } var file_changes_proto_enumTypes = make([]protoimpl.EnumInfo, 12) -var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 97) +var file_changes_proto_msgTypes = make([]protoimpl.MessageInfo, 99) var file_changes_proto_goTypes = []any{ (MappedItemTimelineStatus)(0), // 0: changes.MappedItemTimelineStatus (MappedItemMappingStatus)(0), // 1: changes.MappedItemMappingStatus @@ -6935,105 +7083,107 @@ var file_changes_proto_goTypes = []any{ (*TestLabelRuleResponse)(nil), // 26: changes.TestLabelRuleResponse (*ReapplyLabelRuleInTimeRangeRequest)(nil), // 27: changes.ReapplyLabelRuleInTimeRangeRequest (*ReapplyLabelRuleInTimeRangeResponse)(nil), // 28: changes.ReapplyLabelRuleInTimeRangeResponse - (*GetHypothesesDetailsRequest)(nil), // 29: changes.GetHypothesesDetailsRequest - (*GetHypothesesDetailsResponse)(nil), // 30: changes.GetHypothesesDetailsResponse - (*HypothesesDetails)(nil), // 31: changes.HypothesesDetails - (*GetChangeTimelineV2Request)(nil), // 32: changes.GetChangeTimelineV2Request - (*GetChangeTimelineV2Response)(nil), // 33: changes.GetChangeTimelineV2Response - (*ChangeTimelineEntryV2)(nil), // 34: changes.ChangeTimelineEntryV2 - (*EmptyContent)(nil), // 35: changes.EmptyContent - (*MappedItemTimelineSummary)(nil), // 36: changes.MappedItemTimelineSummary - (*MappedItemsTimelineEntry)(nil), // 37: changes.MappedItemsTimelineEntry - (*CalculatedBlastRadiusTimelineEntry)(nil), // 38: changes.CalculatedBlastRadiusTimelineEntry - (*RecordObservationsTimelineEntry)(nil), // 39: changes.RecordObservationsTimelineEntry - (*FormHypothesesTimelineEntry)(nil), // 40: changes.FormHypothesesTimelineEntry - (*InvestigateHypothesesTimelineEntry)(nil), // 41: changes.InvestigateHypothesesTimelineEntry - (*HypothesisSummary)(nil), // 42: changes.HypothesisSummary - (*CalculatedRisksTimelineEntry)(nil), // 43: changes.CalculatedRisksTimelineEntry - (*CalculatedLabelsTimelineEntry)(nil), // 44: changes.CalculatedLabelsTimelineEntry - (*ChangeValidationTimelineEntry)(nil), // 45: changes.ChangeValidationTimelineEntry - (*ChangeValidationCategory)(nil), // 46: changes.ChangeValidationCategory - (*GetDiffRequest)(nil), // 47: changes.GetDiffRequest - (*GetDiffResponse)(nil), // 48: changes.GetDiffResponse - (*ListChangingItemsSummaryRequest)(nil), // 49: changes.ListChangingItemsSummaryRequest - (*ListChangingItemsSummaryResponse)(nil), // 50: changes.ListChangingItemsSummaryResponse - (*MappedItemDiff)(nil), // 51: changes.MappedItemDiff - (*StartChangeAnalysisRequest)(nil), // 52: changes.StartChangeAnalysisRequest - (*StartChangeAnalysisResponse)(nil), // 53: changes.StartChangeAnalysisResponse - (*ListHomeChangesRequest)(nil), // 54: changes.ListHomeChangesRequest - (*ChangeFiltersRequest)(nil), // 55: changes.ChangeFiltersRequest - (*ListHomeChangesResponse)(nil), // 56: changes.ListHomeChangesResponse - (*PopulateChangeFiltersRequest)(nil), // 57: changes.PopulateChangeFiltersRequest - (*PopulateChangeFiltersResponse)(nil), // 58: changes.PopulateChangeFiltersResponse - (*ItemDiffSummary)(nil), // 59: changes.ItemDiffSummary - (*ItemDiff)(nil), // 60: changes.ItemDiff - (*EnrichedTags)(nil), // 61: changes.EnrichedTags - (*TagValue)(nil), // 62: changes.TagValue - (*UserTagValue)(nil), // 63: changes.UserTagValue - (*AutoTagValue)(nil), // 64: changes.AutoTagValue - (*Label)(nil), // 65: changes.Label - (*ChangeSummary)(nil), // 66: changes.ChangeSummary - (*Change)(nil), // 67: changes.Change - (*ChangeMetadata)(nil), // 68: changes.ChangeMetadata - (*ChangeProperties)(nil), // 69: changes.ChangeProperties - (*GithubChangeInfo)(nil), // 70: changes.GithubChangeInfo - (*ListChangesRequest)(nil), // 71: changes.ListChangesRequest - (*ListChangesResponse)(nil), // 72: changes.ListChangesResponse - (*ListChangesByStatusRequest)(nil), // 73: changes.ListChangesByStatusRequest - (*ListChangesByStatusResponse)(nil), // 74: changes.ListChangesByStatusResponse - (*CreateChangeRequest)(nil), // 75: changes.CreateChangeRequest - (*CreateChangeResponse)(nil), // 76: changes.CreateChangeResponse - (*GetChangeRequest)(nil), // 77: changes.GetChangeRequest - (*GetChangeByTicketLinkRequest)(nil), // 78: changes.GetChangeByTicketLinkRequest - (*GetChangeSummaryRequest)(nil), // 79: changes.GetChangeSummaryRequest - (*GetChangeSummaryResponse)(nil), // 80: changes.GetChangeSummaryResponse - (*GetChangeSignalsRequest)(nil), // 81: changes.GetChangeSignalsRequest - (*GetChangeSignalsResponse)(nil), // 82: changes.GetChangeSignalsResponse - (*GetChangeResponse)(nil), // 83: changes.GetChangeResponse - (*GetChangeRisksRequest)(nil), // 84: changes.GetChangeRisksRequest - (*ChangeRiskMetadata)(nil), // 85: changes.ChangeRiskMetadata - (*GetChangeRisksResponse)(nil), // 86: changes.GetChangeRisksResponse - (*UpdateChangeRequest)(nil), // 87: changes.UpdateChangeRequest - (*UpdateChangeResponse)(nil), // 88: changes.UpdateChangeResponse - (*DeleteChangeRequest)(nil), // 89: changes.DeleteChangeRequest - (*ListChangesBySnapshotUUIDRequest)(nil), // 90: changes.ListChangesBySnapshotUUIDRequest - (*ListChangesBySnapshotUUIDResponse)(nil), // 91: changes.ListChangesBySnapshotUUIDResponse - (*DeleteChangeResponse)(nil), // 92: changes.DeleteChangeResponse - (*RefreshStateRequest)(nil), // 93: changes.RefreshStateRequest - (*RefreshStateResponse)(nil), // 94: changes.RefreshStateResponse - (*StartChangeRequest)(nil), // 95: changes.StartChangeRequest - (*StartChangeResponse)(nil), // 96: changes.StartChangeResponse - (*EndChangeRequest)(nil), // 97: changes.EndChangeRequest - (*EndChangeResponse)(nil), // 98: changes.EndChangeResponse - (*StartChangeSimpleResponse)(nil), // 99: changes.StartChangeSimpleResponse - (*EndChangeSimpleResponse)(nil), // 100: changes.EndChangeSimpleResponse - (*Risk)(nil), // 101: changes.Risk - (*ChangeAnalysisStatus)(nil), // 102: changes.ChangeAnalysisStatus - (*GenerateRiskFixRequest)(nil), // 103: changes.GenerateRiskFixRequest - (*GenerateRiskFixResponse)(nil), // 104: changes.GenerateRiskFixResponse - nil, // 105: changes.EnrichedTags.TagValueEntry - nil, // 106: changes.ChangeSummary.TagsEntry - (*ChangeMetadata_HealthChange)(nil), // 107: changes.ChangeMetadata.HealthChange - nil, // 108: changes.ChangeProperties.TagsEntry - (*timestamppb.Timestamp)(nil), // 109: google.protobuf.Timestamp - (*Edge)(nil), // 110: Edge - (*Query)(nil), // 111: Query - (*QueryError)(nil), // 112: QueryError - (*BlastRadiusConfig)(nil), // 113: config.BlastRadiusConfig - (*RoutineChangesConfig)(nil), // 114: config.RoutineChangesConfig - (*GithubOrganisationProfile)(nil), // 115: config.GithubOrganisationProfile - (*PaginationRequest)(nil), // 116: PaginationRequest - (SortOrder)(0), // 117: SortOrder - (*PaginationResponse)(nil), // 118: PaginationResponse - (*Reference)(nil), // 119: Reference - (Health)(0), // 120: Health - (*Item)(nil), // 121: Item + (*KnowledgeReference)(nil), // 29: changes.KnowledgeReference + (*Knowledge)(nil), // 30: changes.Knowledge + (*GetHypothesesDetailsRequest)(nil), // 31: changes.GetHypothesesDetailsRequest + (*GetHypothesesDetailsResponse)(nil), // 32: changes.GetHypothesesDetailsResponse + (*HypothesesDetails)(nil), // 33: changes.HypothesesDetails + (*GetChangeTimelineV2Request)(nil), // 34: changes.GetChangeTimelineV2Request + (*GetChangeTimelineV2Response)(nil), // 35: changes.GetChangeTimelineV2Response + (*ChangeTimelineEntryV2)(nil), // 36: changes.ChangeTimelineEntryV2 + (*EmptyContent)(nil), // 37: changes.EmptyContent + (*MappedItemTimelineSummary)(nil), // 38: changes.MappedItemTimelineSummary + (*MappedItemsTimelineEntry)(nil), // 39: changes.MappedItemsTimelineEntry + (*CalculatedBlastRadiusTimelineEntry)(nil), // 40: changes.CalculatedBlastRadiusTimelineEntry + (*RecordObservationsTimelineEntry)(nil), // 41: changes.RecordObservationsTimelineEntry + (*FormHypothesesTimelineEntry)(nil), // 42: changes.FormHypothesesTimelineEntry + (*InvestigateHypothesesTimelineEntry)(nil), // 43: changes.InvestigateHypothesesTimelineEntry + (*HypothesisSummary)(nil), // 44: changes.HypothesisSummary + (*CalculatedRisksTimelineEntry)(nil), // 45: changes.CalculatedRisksTimelineEntry + (*CalculatedLabelsTimelineEntry)(nil), // 46: changes.CalculatedLabelsTimelineEntry + (*ChangeValidationTimelineEntry)(nil), // 47: changes.ChangeValidationTimelineEntry + (*ChangeValidationCategory)(nil), // 48: changes.ChangeValidationCategory + (*GetDiffRequest)(nil), // 49: changes.GetDiffRequest + (*GetDiffResponse)(nil), // 50: changes.GetDiffResponse + (*ListChangingItemsSummaryRequest)(nil), // 51: changes.ListChangingItemsSummaryRequest + (*ListChangingItemsSummaryResponse)(nil), // 52: changes.ListChangingItemsSummaryResponse + (*MappedItemDiff)(nil), // 53: changes.MappedItemDiff + (*StartChangeAnalysisRequest)(nil), // 54: changes.StartChangeAnalysisRequest + (*StartChangeAnalysisResponse)(nil), // 55: changes.StartChangeAnalysisResponse + (*ListHomeChangesRequest)(nil), // 56: changes.ListHomeChangesRequest + (*ChangeFiltersRequest)(nil), // 57: changes.ChangeFiltersRequest + (*ListHomeChangesResponse)(nil), // 58: changes.ListHomeChangesResponse + (*PopulateChangeFiltersRequest)(nil), // 59: changes.PopulateChangeFiltersRequest + (*PopulateChangeFiltersResponse)(nil), // 60: changes.PopulateChangeFiltersResponse + (*ItemDiffSummary)(nil), // 61: changes.ItemDiffSummary + (*ItemDiff)(nil), // 62: changes.ItemDiff + (*EnrichedTags)(nil), // 63: changes.EnrichedTags + (*TagValue)(nil), // 64: changes.TagValue + (*UserTagValue)(nil), // 65: changes.UserTagValue + (*AutoTagValue)(nil), // 66: changes.AutoTagValue + (*Label)(nil), // 67: changes.Label + (*ChangeSummary)(nil), // 68: changes.ChangeSummary + (*Change)(nil), // 69: changes.Change + (*ChangeMetadata)(nil), // 70: changes.ChangeMetadata + (*ChangeProperties)(nil), // 71: changes.ChangeProperties + (*GithubChangeInfo)(nil), // 72: changes.GithubChangeInfo + (*ListChangesRequest)(nil), // 73: changes.ListChangesRequest + (*ListChangesResponse)(nil), // 74: changes.ListChangesResponse + (*ListChangesByStatusRequest)(nil), // 75: changes.ListChangesByStatusRequest + (*ListChangesByStatusResponse)(nil), // 76: changes.ListChangesByStatusResponse + (*CreateChangeRequest)(nil), // 77: changes.CreateChangeRequest + (*CreateChangeResponse)(nil), // 78: changes.CreateChangeResponse + (*GetChangeRequest)(nil), // 79: changes.GetChangeRequest + (*GetChangeByTicketLinkRequest)(nil), // 80: changes.GetChangeByTicketLinkRequest + (*GetChangeSummaryRequest)(nil), // 81: changes.GetChangeSummaryRequest + (*GetChangeSummaryResponse)(nil), // 82: changes.GetChangeSummaryResponse + (*GetChangeSignalsRequest)(nil), // 83: changes.GetChangeSignalsRequest + (*GetChangeSignalsResponse)(nil), // 84: changes.GetChangeSignalsResponse + (*GetChangeResponse)(nil), // 85: changes.GetChangeResponse + (*GetChangeRisksRequest)(nil), // 86: changes.GetChangeRisksRequest + (*ChangeRiskMetadata)(nil), // 87: changes.ChangeRiskMetadata + (*GetChangeRisksResponse)(nil), // 88: changes.GetChangeRisksResponse + (*UpdateChangeRequest)(nil), // 89: changes.UpdateChangeRequest + (*UpdateChangeResponse)(nil), // 90: changes.UpdateChangeResponse + (*DeleteChangeRequest)(nil), // 91: changes.DeleteChangeRequest + (*ListChangesBySnapshotUUIDRequest)(nil), // 92: changes.ListChangesBySnapshotUUIDRequest + (*ListChangesBySnapshotUUIDResponse)(nil), // 93: changes.ListChangesBySnapshotUUIDResponse + (*DeleteChangeResponse)(nil), // 94: changes.DeleteChangeResponse + (*RefreshStateRequest)(nil), // 95: changes.RefreshStateRequest + (*RefreshStateResponse)(nil), // 96: changes.RefreshStateResponse + (*StartChangeRequest)(nil), // 97: changes.StartChangeRequest + (*StartChangeResponse)(nil), // 98: changes.StartChangeResponse + (*EndChangeRequest)(nil), // 99: changes.EndChangeRequest + (*EndChangeResponse)(nil), // 100: changes.EndChangeResponse + (*StartChangeSimpleResponse)(nil), // 101: changes.StartChangeSimpleResponse + (*EndChangeSimpleResponse)(nil), // 102: changes.EndChangeSimpleResponse + (*Risk)(nil), // 103: changes.Risk + (*ChangeAnalysisStatus)(nil), // 104: changes.ChangeAnalysisStatus + (*GenerateRiskFixRequest)(nil), // 105: changes.GenerateRiskFixRequest + (*GenerateRiskFixResponse)(nil), // 106: changes.GenerateRiskFixResponse + nil, // 107: changes.EnrichedTags.TagValueEntry + nil, // 108: changes.ChangeSummary.TagsEntry + (*ChangeMetadata_HealthChange)(nil), // 109: changes.ChangeMetadata.HealthChange + nil, // 110: changes.ChangeProperties.TagsEntry + (*timestamppb.Timestamp)(nil), // 111: google.protobuf.Timestamp + (*Edge)(nil), // 112: Edge + (*Query)(nil), // 113: Query + (*QueryError)(nil), // 114: QueryError + (*BlastRadiusConfig)(nil), // 115: config.BlastRadiusConfig + (*RoutineChangesConfig)(nil), // 116: config.RoutineChangesConfig + (*GithubOrganisationProfile)(nil), // 117: config.GithubOrganisationProfile + (*PaginationRequest)(nil), // 118: PaginationRequest + (SortOrder)(0), // 119: SortOrder + (*PaginationResponse)(nil), // 120: PaginationResponse + (*Reference)(nil), // 121: Reference + (Health)(0), // 122: Health + (*Item)(nil), // 123: Item } var file_changes_proto_depIdxs = []int32{ 13, // 0: changes.LabelRule.metadata:type_name -> changes.LabelRuleMetadata 14, // 1: changes.LabelRule.properties:type_name -> changes.LabelRuleProperties - 109, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 111, // 2: changes.LabelRuleMetadata.createdAt:type_name -> google.protobuf.Timestamp + 111, // 3: changes.LabelRuleMetadata.updatedAt:type_name -> google.protobuf.Timestamp 12, // 4: changes.ListLabelRulesResponse.rules:type_name -> changes.LabelRule 14, // 5: changes.CreateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties 12, // 6: changes.CreateLabelRuleResponse.rule:type_name -> changes.LabelRule @@ -7041,174 +7191,176 @@ var file_changes_proto_depIdxs = []int32{ 14, // 8: changes.UpdateLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties 12, // 9: changes.UpdateLabelRuleResponse.rule:type_name -> changes.LabelRule 14, // 10: changes.TestLabelRuleRequest.properties:type_name -> changes.LabelRuleProperties - 65, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label - 109, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp - 109, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp - 31, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails + 67, // 11: changes.TestLabelRuleResponse.label:type_name -> changes.Label + 111, // 12: changes.ReapplyLabelRuleInTimeRangeRequest.startAt:type_name -> google.protobuf.Timestamp + 111, // 13: changes.ReapplyLabelRuleInTimeRangeRequest.endAt:type_name -> google.protobuf.Timestamp + 33, // 14: changes.GetHypothesesDetailsResponse.hypotheses:type_name -> changes.HypothesesDetails 2, // 15: changes.HypothesesDetails.status:type_name -> changes.HypothesisStatus - 34, // 16: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 - 3, // 17: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus - 109, // 18: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp - 109, // 19: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp - 37, // 20: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry - 38, // 21: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry - 43, // 22: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry - 35, // 23: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent - 45, // 24: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry - 44, // 25: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry - 40, // 26: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry - 41, // 27: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry - 39, // 28: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry - 0, // 29: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus - 51, // 30: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff - 36, // 31: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary - 42, // 32: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 42, // 33: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary - 2, // 34: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus - 101, // 35: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk - 65, // 36: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label - 46, // 37: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory - 60, // 38: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff - 60, // 39: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff - 110, // 40: changes.GetDiffResponse.edges:type_name -> Edge - 60, // 41: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff - 59, // 42: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary - 60, // 43: changes.MappedItemDiff.item:type_name -> changes.ItemDiff - 111, // 44: changes.MappedItemDiff.mappingQuery:type_name -> Query - 112, // 45: changes.MappedItemDiff.mappingError:type_name -> QueryError - 1, // 46: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus - 51, // 47: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff - 113, // 48: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig - 114, // 49: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig - 115, // 50: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile - 116, // 51: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest - 55, // 52: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest - 10, // 53: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity - 7, // 54: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus - 117, // 55: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder - 66, // 56: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary - 118, // 57: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse - 119, // 58: changes.ItemDiffSummary.item:type_name -> Reference - 4, // 59: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus - 120, // 60: changes.ItemDiffSummary.healthAfter:type_name -> Health - 119, // 61: changes.ItemDiff.item:type_name -> Reference - 4, // 62: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus - 121, // 63: changes.ItemDiff.before:type_name -> Item - 121, // 64: changes.ItemDiff.after:type_name -> Item - 105, // 65: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry - 63, // 66: changes.TagValue.userTagValue:type_name -> changes.UserTagValue - 64, // 67: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue - 6, // 68: changes.Label.type:type_name -> changes.LabelType - 7, // 69: changes.ChangeSummary.status:type_name -> changes.ChangeStatus - 109, // 70: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp - 106, // 71: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry - 61, // 72: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags - 65, // 73: changes.ChangeSummary.labels:type_name -> changes.Label - 70, // 74: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo - 68, // 75: changes.Change.metadata:type_name -> changes.ChangeMetadata - 69, // 76: changes.Change.properties:type_name -> changes.ChangeProperties - 109, // 77: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp - 109, // 78: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp - 7, // 79: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus - 107, // 80: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 81: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 82: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 83: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 107, // 84: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange - 70, // 85: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo - 102, // 86: changes.ChangeMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus - 60, // 87: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff - 108, // 88: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry - 61, // 89: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags - 65, // 90: changes.ChangeProperties.labels:type_name -> changes.Label - 67, // 91: changes.ListChangesResponse.changes:type_name -> changes.Change - 7, // 92: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus - 67, // 93: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change - 69, // 94: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 95: changes.CreateChangeResponse.change:type_name -> changes.Change - 5, // 96: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 10, // 97: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity - 5, // 98: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat - 67, // 99: changes.GetChangeResponse.change:type_name -> changes.Change - 102, // 100: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus - 101, // 101: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk - 85, // 102: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata - 69, // 103: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties - 67, // 104: changes.UpdateChangeResponse.change:type_name -> changes.Change - 67, // 105: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change - 8, // 106: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State - 9, // 107: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State - 10, // 108: changes.Risk.severity:type_name -> changes.Risk.Severity - 119, // 109: changes.Risk.relatedItems:type_name -> Reference - 11, // 110: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status - 62, // 111: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue - 71, // 112: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest - 73, // 113: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest - 75, // 114: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest - 77, // 115: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest - 78, // 116: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest - 79, // 117: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest - 32, // 118: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request - 84, // 119: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest - 87, // 120: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest - 89, // 121: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest - 90, // 122: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest - 93, // 123: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest - 95, // 124: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest - 97, // 125: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest - 95, // 126: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest - 97, // 127: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest - 54, // 128: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest - 52, // 129: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest - 49, // 130: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest - 47, // 131: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest - 57, // 132: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest - 103, // 133: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest - 29, // 134: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest - 81, // 135: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest - 15, // 136: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest - 17, // 137: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest - 19, // 138: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest - 21, // 139: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest - 23, // 140: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest - 25, // 141: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest - 27, // 142: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest - 72, // 143: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse - 74, // 144: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse - 76, // 145: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse - 83, // 146: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse - 83, // 147: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse - 80, // 148: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse - 33, // 149: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response - 86, // 150: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse - 88, // 151: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse - 92, // 152: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse - 91, // 153: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse - 94, // 154: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse - 96, // 155: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse - 98, // 156: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse - 99, // 157: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse - 100, // 158: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse - 56, // 159: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse - 53, // 160: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse - 50, // 161: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse - 48, // 162: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse - 58, // 163: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse - 104, // 164: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse - 30, // 165: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse - 82, // 166: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse - 16, // 167: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse - 18, // 168: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse - 20, // 169: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse - 22, // 170: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse - 24, // 171: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse - 26, // 172: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse - 28, // 173: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse - 143, // [143:174] is the sub-list for method output_type - 112, // [112:143] is the sub-list for method input_type - 112, // [112:112] is the sub-list for extension type_name - 112, // [112:112] is the sub-list for extension extendee - 0, // [0:112] is the sub-list for field type_name + 29, // 16: changes.HypothesesDetails.knowledgeUsed:type_name -> changes.KnowledgeReference + 36, // 17: changes.GetChangeTimelineV2Response.entries:type_name -> changes.ChangeTimelineEntryV2 + 3, // 18: changes.ChangeTimelineEntryV2.status:type_name -> changes.ChangeTimelineEntryStatus + 111, // 19: changes.ChangeTimelineEntryV2.startedAt:type_name -> google.protobuf.Timestamp + 111, // 20: changes.ChangeTimelineEntryV2.endedAt:type_name -> google.protobuf.Timestamp + 39, // 21: changes.ChangeTimelineEntryV2.mappedItems:type_name -> changes.MappedItemsTimelineEntry + 40, // 22: changes.ChangeTimelineEntryV2.calculatedBlastRadius:type_name -> changes.CalculatedBlastRadiusTimelineEntry + 45, // 23: changes.ChangeTimelineEntryV2.calculatedRisks:type_name -> changes.CalculatedRisksTimelineEntry + 37, // 24: changes.ChangeTimelineEntryV2.empty:type_name -> changes.EmptyContent + 47, // 25: changes.ChangeTimelineEntryV2.changeValidation:type_name -> changes.ChangeValidationTimelineEntry + 46, // 26: changes.ChangeTimelineEntryV2.calculatedLabels:type_name -> changes.CalculatedLabelsTimelineEntry + 42, // 27: changes.ChangeTimelineEntryV2.formHypotheses:type_name -> changes.FormHypothesesTimelineEntry + 43, // 28: changes.ChangeTimelineEntryV2.investigateHypotheses:type_name -> changes.InvestigateHypothesesTimelineEntry + 41, // 29: changes.ChangeTimelineEntryV2.recordObservations:type_name -> changes.RecordObservationsTimelineEntry + 0, // 30: changes.MappedItemTimelineSummary.status:type_name -> changes.MappedItemTimelineStatus + 53, // 31: changes.MappedItemsTimelineEntry.mappedItems:type_name -> changes.MappedItemDiff + 38, // 32: changes.MappedItemsTimelineEntry.items:type_name -> changes.MappedItemTimelineSummary + 44, // 33: changes.FormHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 44, // 34: changes.InvestigateHypothesesTimelineEntry.hypotheses:type_name -> changes.HypothesisSummary + 2, // 35: changes.HypothesisSummary.status:type_name -> changes.HypothesisStatus + 103, // 36: changes.CalculatedRisksTimelineEntry.risks:type_name -> changes.Risk + 67, // 37: changes.CalculatedLabelsTimelineEntry.labels:type_name -> changes.Label + 48, // 38: changes.ChangeValidationTimelineEntry.validationChecklist:type_name -> changes.ChangeValidationCategory + 62, // 39: changes.GetDiffResponse.expectedItems:type_name -> changes.ItemDiff + 62, // 40: changes.GetDiffResponse.unexpectedItems:type_name -> changes.ItemDiff + 112, // 41: changes.GetDiffResponse.edges:type_name -> Edge + 62, // 42: changes.GetDiffResponse.missingItems:type_name -> changes.ItemDiff + 61, // 43: changes.ListChangingItemsSummaryResponse.items:type_name -> changes.ItemDiffSummary + 62, // 44: changes.MappedItemDiff.item:type_name -> changes.ItemDiff + 113, // 45: changes.MappedItemDiff.mappingQuery:type_name -> Query + 114, // 46: changes.MappedItemDiff.mappingError:type_name -> QueryError + 1, // 47: changes.MappedItemDiff.mapping_status:type_name -> changes.MappedItemMappingStatus + 53, // 48: changes.StartChangeAnalysisRequest.changingItems:type_name -> changes.MappedItemDiff + 115, // 49: changes.StartChangeAnalysisRequest.blastRadiusConfigOverride:type_name -> config.BlastRadiusConfig + 116, // 50: changes.StartChangeAnalysisRequest.routineChangesConfigOverride:type_name -> config.RoutineChangesConfig + 117, // 51: changes.StartChangeAnalysisRequest.githubOrganisationProfileOverride:type_name -> config.GithubOrganisationProfile + 30, // 52: changes.StartChangeAnalysisRequest.knowledge:type_name -> changes.Knowledge + 118, // 53: changes.ListHomeChangesRequest.pagination:type_name -> PaginationRequest + 57, // 54: changes.ListHomeChangesRequest.filters:type_name -> changes.ChangeFiltersRequest + 10, // 55: changes.ChangeFiltersRequest.risks:type_name -> changes.Risk.Severity + 7, // 56: changes.ChangeFiltersRequest.statuses:type_name -> changes.ChangeStatus + 119, // 57: changes.ChangeFiltersRequest.sortOrder:type_name -> SortOrder + 68, // 58: changes.ListHomeChangesResponse.changes:type_name -> changes.ChangeSummary + 120, // 59: changes.ListHomeChangesResponse.pagination:type_name -> PaginationResponse + 121, // 60: changes.ItemDiffSummary.item:type_name -> Reference + 4, // 61: changes.ItemDiffSummary.status:type_name -> changes.ItemDiffStatus + 122, // 62: changes.ItemDiffSummary.healthAfter:type_name -> Health + 121, // 63: changes.ItemDiff.item:type_name -> Reference + 4, // 64: changes.ItemDiff.status:type_name -> changes.ItemDiffStatus + 123, // 65: changes.ItemDiff.before:type_name -> Item + 123, // 66: changes.ItemDiff.after:type_name -> Item + 107, // 67: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry + 65, // 68: changes.TagValue.userTagValue:type_name -> changes.UserTagValue + 66, // 69: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue + 6, // 70: changes.Label.type:type_name -> changes.LabelType + 7, // 71: changes.ChangeSummary.status:type_name -> changes.ChangeStatus + 111, // 72: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp + 108, // 73: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry + 63, // 74: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags + 67, // 75: changes.ChangeSummary.labels:type_name -> changes.Label + 72, // 76: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo + 70, // 77: changes.Change.metadata:type_name -> changes.ChangeMetadata + 71, // 78: changes.Change.properties:type_name -> changes.ChangeProperties + 111, // 79: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp + 111, // 80: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 7, // 81: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus + 109, // 82: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 83: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 84: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 85: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 86: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 72, // 87: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo + 104, // 88: changes.ChangeMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 62, // 89: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff + 110, // 90: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry + 63, // 91: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags + 67, // 92: changes.ChangeProperties.labels:type_name -> changes.Label + 69, // 93: changes.ListChangesResponse.changes:type_name -> changes.Change + 7, // 94: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus + 69, // 95: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change + 71, // 96: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 97: changes.CreateChangeResponse.change:type_name -> changes.Change + 5, // 98: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 10, // 99: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity + 5, // 100: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 69, // 101: changes.GetChangeResponse.change:type_name -> changes.Change + 104, // 102: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 103, // 103: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk + 87, // 104: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 71, // 105: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 106: changes.UpdateChangeResponse.change:type_name -> changes.Change + 69, // 107: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change + 8, // 108: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State + 9, // 109: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State + 10, // 110: changes.Risk.severity:type_name -> changes.Risk.Severity + 121, // 111: changes.Risk.relatedItems:type_name -> Reference + 11, // 112: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status + 64, // 113: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue + 73, // 114: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest + 75, // 115: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest + 77, // 116: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest + 79, // 117: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest + 80, // 118: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest + 81, // 119: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest + 34, // 120: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request + 86, // 121: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest + 89, // 122: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest + 91, // 123: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest + 92, // 124: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest + 95, // 125: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest + 97, // 126: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest + 99, // 127: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest + 97, // 128: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest + 99, // 129: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest + 56, // 130: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest + 54, // 131: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest + 51, // 132: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest + 49, // 133: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest + 59, // 134: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest + 105, // 135: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest + 31, // 136: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest + 83, // 137: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest + 15, // 138: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest + 17, // 139: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest + 19, // 140: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest + 21, // 141: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest + 23, // 142: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest + 25, // 143: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest + 27, // 144: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest + 74, // 145: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse + 76, // 146: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse + 78, // 147: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse + 85, // 148: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse + 85, // 149: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse + 82, // 150: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse + 35, // 151: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response + 88, // 152: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse + 90, // 153: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse + 94, // 154: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse + 93, // 155: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse + 96, // 156: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse + 98, // 157: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse + 100, // 158: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse + 101, // 159: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse + 102, // 160: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse + 58, // 161: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse + 55, // 162: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse + 52, // 163: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse + 50, // 164: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse + 60, // 165: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse + 106, // 166: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse + 32, // 167: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse + 84, // 168: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse + 16, // 169: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse + 18, // 170: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse + 20, // 171: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse + 22, // 172: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse + 24, // 173: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse + 26, // 174: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse + 28, // 175: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse + 145, // [145:176] is the sub-list for method output_type + 114, // [114:145] is the sub-list for method input_type + 114, // [114:114] is the sub-list for extension type_name + 114, // [114:114] is the sub-list for extension extendee + 0, // [0:114] is the sub-list for field type_name } func init() { file_changes_proto_init() } @@ -7219,7 +7371,7 @@ func file_changes_proto_init() { file_config_proto_init() file_items_proto_init() file_util_proto_init() - file_changes_proto_msgTypes[22].OneofWrappers = []any{ + file_changes_proto_msgTypes[24].OneofWrappers = []any{ (*ChangeTimelineEntryV2_MappedItems)(nil), (*ChangeTimelineEntryV2_CalculatedBlastRadius)(nil), (*ChangeTimelineEntryV2_CalculatedRisks)(nil), @@ -7232,24 +7384,24 @@ func file_changes_proto_init() { (*ChangeTimelineEntryV2_InvestigateHypotheses)(nil), (*ChangeTimelineEntryV2_RecordObservations)(nil), } - file_changes_proto_msgTypes[24].OneofWrappers = []any{} - file_changes_proto_msgTypes[39].OneofWrappers = []any{} - file_changes_proto_msgTypes[40].OneofWrappers = []any{} + file_changes_proto_msgTypes[26].OneofWrappers = []any{} + file_changes_proto_msgTypes[41].OneofWrappers = []any{} file_changes_proto_msgTypes[42].OneofWrappers = []any{} - file_changes_proto_msgTypes[43].OneofWrappers = []any{} - file_changes_proto_msgTypes[48].OneofWrappers = []any{} - file_changes_proto_msgTypes[50].OneofWrappers = []any{ + file_changes_proto_msgTypes[44].OneofWrappers = []any{} + file_changes_proto_msgTypes[45].OneofWrappers = []any{} + file_changes_proto_msgTypes[50].OneofWrappers = []any{} + file_changes_proto_msgTypes[52].OneofWrappers = []any{ (*TagValue_UserTagValue)(nil), (*TagValue_AutoTagValue)(nil), } - file_changes_proto_msgTypes[56].OneofWrappers = []any{} + file_changes_proto_msgTypes[58].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_changes_proto_rawDesc), len(file_changes_proto_rawDesc)), NumEnums: 12, - NumMessages: 97, + NumMessages: 99, NumExtensions: 0, NumServices: 2, }, From d8d29e224ed89733d82dd2d159a728772423e873 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Wed, 18 Feb 2026 15:26:24 +0000 Subject: [PATCH 31/51] Cli knowledge files (#3928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement CLI knowledge file discovery, validation, and submission to enhance change analysis with contextual information. --- Linear Issue: [ENG-2612](https://linear.app/overmind/issue/ENG-2612/cli-knowledge-implementation)

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Adds new file parsing/validation and increases API request payloads for change analysis; issues could cause knowledge to be skipped or larger requests, but core change submission flow remains intact. > > **Overview** > The CLI now discovers markdown “knowledge” files under `.overmind/knowledge/`, validates required YAML frontmatter (`name`, `description`), enforces naming/size constraints, and deterministically loads/deduplicates them (logging warnings and skipping invalid files). > > Both `changes submit-plan` and `terraform plan` now include the discovered knowledge payload in `StartChangeAnalysisRequest`, allowing change analysis to be enriched with local contextual documentation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f6babb03506a8d4d36f539057b44b0e38f805f26. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: 630cafb27f0a29d5a4ce55f6573bf68ccec97c05 --- cmd/changes_submit_plan.go | 5 + cmd/terraform_plan.go | 5 + knowledge/discover.go | 343 +++++++++++++++++++ knowledge/discover_test.go | 662 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1015 insertions(+) create mode 100644 knowledge/discover.go create mode 100644 knowledge/discover_test.go diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index 66f9a8b0..e1eb5035 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -11,6 +11,7 @@ import ( "connectrpc.com/connect" "github.com/google/uuid" + "github.com/overmindtech/cli/knowledge" "github.com/overmindtech/cli/tfutils" "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" @@ -287,6 +288,9 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { routineChangesConfigOverride = signalConfigOverride.RoutineChangesConfig } + // Discover and convert knowledge files + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ ChangeUUID: changeUUID[:], @@ -294,6 +298,7 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { BlastRadiusConfigOverride: blastRadiusConfigOverride, RoutineChangesConfigOverride: routineChangesConfigOverride, GithubOrganisationProfileOverride: githubOrganisationProfileOverride, + Knowledge: sdpKnowledge, }, }) if err != nil { diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index ace728c4..b54f026e 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/muesli/reflow/wordwrap" "github.com/overmindtech/pterm" + "github.com/overmindtech/cli/knowledge" "github.com/overmindtech/cli/tfutils" "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" @@ -314,10 +315,14 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI uploadPlannedChange, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Uploading planned changes") log.WithField("change", changeUuid).Debug("Uploading planned changes") + // Discover and convert knowledge files + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ ChangeUUID: changeUuid[:], ChangingItems: mappingResponse.GetItemDiffs(), + Knowledge: sdpKnowledge, }, }) if err != nil { diff --git a/knowledge/discover.go b/knowledge/discover.go new file mode 100644 index 00000000..5e92dbf6 --- /dev/null +++ b/knowledge/discover.go @@ -0,0 +1,343 @@ +package knowledge + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +// KnowledgeFile represents a discovered and validated knowledge file +type KnowledgeFile struct { + Name string + Description string + Content string // markdown body only (excluding frontmatter) + FileName string // path relative to .overmind/knowledge/ +} + +// Warning represents a validation or parsing issue with a knowledge file +type Warning struct { + Path string // relative path within .overmind/knowledge/ + Reason string +} + +// frontmatter represents the YAML frontmatter structure +type frontmatter struct { + Name string `yaml:"name"` + Description string `yaml:"description"` +} + +// nameRegex validates knowledge file names (kebab-case: lowercase letters, digits, hyphens) +// Must start with a letter, end with letter or digit, 1-64 chars total +var nameRegex = regexp.MustCompile(`^[a-z]([a-z0-9-]*[a-z0-9])?$`) + +const ( + // maxFileSize is the maximum allowed size for a knowledge file (10MB) + // This prevents memory exhaustion and excessive API payload sizes + maxFileSize = 10 * 1024 * 1024 // 10MB +) + +// Discover walks the knowledge directory and discovers all valid knowledge files +// Returns valid files and any warnings encountered during discovery +func Discover(knowledgeDir string) ([]KnowledgeFile, []Warning) { + var files []KnowledgeFile + var warnings []Warning + + // Check if directory exists + if _, err := os.Stat(knowledgeDir); os.IsNotExist(err) { + return files, warnings + } + + // Collect all markdown files first for deterministic ordering + type fileInfo struct { + path string + relPath string + } + var mdFiles []fileInfo + + err := filepath.WalkDir(knowledgeDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + // Warn about directories/files we can't access + relPath, _ := filepath.Rel(knowledgeDir, path) + warnings = append(warnings, Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot access: %v", err), + }) + return nil // Continue walking + } + + // Skip directories + if d.IsDir() { + return nil + } + + // Only process .md files + if !strings.HasSuffix(d.Name(), ".md") { + return nil + } + + relPath, err := filepath.Rel(knowledgeDir, path) + if err != nil { + return err + } + + mdFiles = append(mdFiles, fileInfo{ + path: path, + relPath: relPath, + }) + + return nil + }) + + if err != nil { + warnings = append(warnings, Warning{ + Path: "", + Reason: fmt.Sprintf("error walking directory: %v", err), + }) + return files, warnings + } + + // Sort files lexicographically for deterministic processing + sort.Slice(mdFiles, func(i, j int) bool { + return mdFiles[i].relPath < mdFiles[j].relPath + }) + + // Track seen names for deduplication + seenNames := make(map[string]string) // name -> first file path + + // Process each file + for _, f := range mdFiles { + kf, warn := processFile(f.path, f.relPath) + if warn != nil { + warnings = append(warnings, *warn) + continue + } + + // Check for duplicate names + if firstPath, exists := seenNames[kf.Name]; exists { + warnings = append(warnings, Warning{ + Path: f.relPath, + Reason: fmt.Sprintf("duplicate name %q (already loaded from %q)", kf.Name, firstPath), + }) + continue + } + + seenNames[kf.Name] = f.relPath + files = append(files, *kf) + } + + return files, warnings +} + +// processFile reads and validates a single knowledge file +func processFile(path, relPath string) (*KnowledgeFile, *Warning) { + // Check file size before reading + fileInfo, err := os.Stat(path) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot stat file: %v", err), + } + } + + if fileInfo.Size() > maxFileSize { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("file size %d bytes exceeds maximum allowed size of %d bytes", fileInfo.Size(), maxFileSize), + } + } + + // Read file content + content, err := os.ReadFile(path) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: fmt.Sprintf("cannot read file: %v", err), + } + } + + // Parse frontmatter + name, description, body, err := parseFrontmatter(string(content)) + if err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + // Validate name + if err := validateName(name); err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + // Validate description + if err := validateDescription(description); err != nil { + return nil, &Warning{ + Path: relPath, + Reason: err.Error(), + } + } + + return &KnowledgeFile{ + Name: name, + Description: description, + Content: body, + FileName: relPath, + }, nil +} + +// parseFrontmatter extracts YAML frontmatter from markdown content +// Returns name, description, body (without frontmatter), and any error +func parseFrontmatter(content string) (string, string, string, error) { + // Frontmatter must start at the beginning of the file + if !strings.HasPrefix(content, "---\n") && !strings.HasPrefix(content, "---\r\n") { + return "", "", "", fmt.Errorf("frontmatter is required (must start with ---)") + } + + // Determine opening delimiter length + startIdx := 4 // "---\n" + if strings.HasPrefix(content, "---\r\n") { + startIdx = 5 // "---\r\n" + } + + // Find the closing delimiter + remaining := content[startIdx:] + + // Handle edge case: empty frontmatter where second --- is immediately after first + if strings.HasPrefix(remaining, "---\n") || strings.HasPrefix(remaining, "---\r\n") { + bodyStartIdx := startIdx + 4 // "---\n" + if strings.HasPrefix(remaining, "---\r\n") { + bodyStartIdx = startIdx + 5 // "---\r\n" + } + body := strings.TrimLeft(content[bodyStartIdx:], "\n\r") + + // Empty frontmatter will result in empty name/description which will fail validation + var fm frontmatter + return fm.Name, fm.Description, body, nil + } + + // Find closing delimiter and track which type we found + var endIdx int + var closingDelimLen int + + // Try CRLF first (more specific), then LF + endIdx = strings.Index(remaining, "\n---\r\n") + if endIdx != -1 { + closingDelimLen = 6 // "\n---\r\n" + } else { + endIdx = strings.Index(remaining, "\n---\n") + if endIdx != -1 { + closingDelimLen = 5 // "\n---\n" + } else { + // Check for closing delimiter at end of file (more specific first) + if strings.HasSuffix(remaining, "\r\n---") { + endIdx = len(remaining) - 5 + closingDelimLen = 5 // "\r\n---" (no trailing newline) + } else if strings.HasSuffix(remaining, "\n---") { + endIdx = len(remaining) - 4 + closingDelimLen = 4 // "\n---" (no trailing newline) + } else { + return "", "", "", fmt.Errorf("frontmatter closing delimiter (---) not found") + } + } + } + + // Extract YAML content + yamlContent := remaining[:endIdx] + + // Parse YAML with strict mode (unknown fields will cause error) + var fm frontmatter + decoder := yaml.NewDecoder(strings.NewReader(yamlContent)) + decoder.KnownFields(true) // Reject unknown fields + if err := decoder.Decode(&fm); err != nil { + if strings.Contains(err.Error(), "field") && strings.Contains(err.Error(), "not found") { + return "", "", "", fmt.Errorf("only 'name' and 'description' fields are allowed in frontmatter") + } + return "", "", "", fmt.Errorf("invalid YAML in frontmatter: %w", err) + } + + // Extract body using the correct offset for the delimiter type found + bodyStartIdx := startIdx + endIdx + closingDelimLen + if bodyStartIdx > len(content) { + bodyStartIdx = len(content) + } + body := strings.TrimLeft(content[bodyStartIdx:], "\n\r") + + // Trim whitespace from name and description as per validation + return strings.TrimSpace(fm.Name), strings.TrimSpace(fm.Description), body, nil +} + +// validateName checks if the name meets the specification requirements +func validateName(name string) error { + name = strings.TrimSpace(name) + + if name == "" { + return fmt.Errorf("name is required") + } + + if len(name) > 64 { + return fmt.Errorf("name must be 64 characters or less") + } + + if !nameRegex.MatchString(name) { + return fmt.Errorf("name must use kebab-case (lowercase letters, digits, hyphens; start with letter, end with letter or digit)") + } + + return nil +} + +// validateDescription checks if the description meets the specification requirements +func validateDescription(description string) error { + description = strings.TrimSpace(description) + + if description == "" { + return fmt.Errorf("description is required") + } + + if len(description) > 1024 { + return fmt.Errorf("description must be 1024 characters or less") + } + + return nil +} + +// DiscoverAndConvert discovers knowledge files and converts them to SDP Knowledge messages. +// This is a convenience function that combines discovery, warning logging, and conversion +// to reduce code duplication across commands. +func DiscoverAndConvert(ctx context.Context, knowledgeDir string) []*sdp.Knowledge { + knowledgeFiles, warnings := Discover(knowledgeDir) + + // Log warnings + for _, w := range warnings { + log.WithContext(ctx).Warnf("Warning: skipping knowledge file %q: %s", w.Path, w.Reason) + } + + // Convert to SDP Knowledge messages + sdpKnowledge := make([]*sdp.Knowledge, len(knowledgeFiles)) + for i, kf := range knowledgeFiles { + sdpKnowledge[i] = &sdp.Knowledge{ + Name: kf.Name, + Description: kf.Description, + Content: kf.Content, + FileName: kf.FileName, + } + } + + // Log when knowledge files are loaded + if len(knowledgeFiles) > 0 { + log.WithContext(ctx).WithField("knowledgeCount", len(knowledgeFiles)).Info("Loaded knowledge files") + } + + return sdpKnowledge +} diff --git a/knowledge/discover_test.go b/knowledge/discover_test.go new file mode 100644 index 00000000..2e8d21b0 --- /dev/null +++ b/knowledge/discover_test.go @@ -0,0 +1,662 @@ +package knowledge + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDiscover_EmptyDirectory(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + files, warnings := Discover(knowledgeDir) + + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } +} + +func TestDiscover_DirectoryDoesNotExist(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "nonexistent") + + files, warnings := Discover(knowledgeDir) + + if len(files) != 0 { + t.Errorf("expected 0 files, got %d", len(files)) + } + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } +} + +func TestDiscover_ValidFiles(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create valid files at root + writeFile(t, filepath.Join(knowledgeDir, "aws-s3.md"), `--- +name: aws-s3-security +description: Security best practices for S3 buckets +--- +# AWS S3 Security +Content here. +`) + + // Create valid file in subfolder + subdir := filepath.Join(knowledgeDir, "cloud") + err = os.Mkdir(subdir, 0755) + if err != nil { + t.Fatal(err) + } + writeFile(t, filepath.Join(subdir, "gcp.md"), `--- +name: gcp-compute +description: GCP Compute Engine guidelines +--- +# GCP Compute +Content here. +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 2 { + t.Fatalf("expected 2 files, got %d", len(files)) + } + + // Check first file (lexicographic order) + if files[0].Name != "aws-s3-security" { + t.Errorf("expected name 'aws-s3-security', got %q", files[0].Name) + } + if files[0].Description != "Security best practices for S3 buckets" { + t.Errorf("unexpected description: %q", files[0].Description) + } + if files[0].FileName != "aws-s3.md" { + t.Errorf("expected fileName 'aws-s3.md', got %q", files[0].FileName) + } + if files[0].Content != "# AWS S3 Security\nContent here.\n" { + t.Errorf("unexpected content: %q", files[0].Content) + } + + // Check second file + if files[1].Name != "gcp-compute" { + t.Errorf("expected name 'gcp-compute', got %q", files[1].Name) + } + if files[1].FileName != filepath.Join("cloud", "gcp.md") { + t.Errorf("expected fileName 'cloud/gcp.md', got %q", files[1].FileName) + } +} + +func TestDiscover_NonMarkdownFilesSkipped(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create non-markdown files + writeFile(t, filepath.Join(knowledgeDir, "readme.txt"), "This is a text file") + writeFile(t, filepath.Join(knowledgeDir, "config.yaml"), "key: value") + writeFile(t, filepath.Join(knowledgeDir, "script.sh"), "#!/bin/bash") + + // Create one valid markdown file + writeFile(t, filepath.Join(knowledgeDir, "valid.md"), `--- +name: valid-file +description: A valid knowledge file +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 1 { + t.Errorf("expected 1 file, got %d", len(files)) + } +} + +func TestDiscover_NestedSubfolders(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + + // Create nested directory structure + deepDir := filepath.Join(knowledgeDir, "cloud", "aws", "services") + err := os.MkdirAll(deepDir, 0755) + if err != nil { + t.Fatal(err) + } + + writeFile(t, filepath.Join(deepDir, "s3.md"), `--- +name: deep-s3 +description: Deeply nested file +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d: %v", len(warnings), warnings) + } + if len(files) != 1 { + t.Fatalf("expected 1 file, got %d", len(files)) + } + expectedPath := filepath.Join("cloud", "aws", "services", "s3.md") + if files[0].FileName != expectedPath { + t.Errorf("expected fileName %q, got %q", expectedPath, files[0].FileName) + } +} + +func TestParseFrontmatter_Valid(t *testing.T) { + content := `--- +name: test-file +description: Test description +--- +# Markdown content +Here is some content. +` + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "test-file" { + t.Errorf("expected name 'test-file', got %q", name) + } + if desc != "Test description" { + t.Errorf("expected description 'Test description', got %q", desc) + } + if body != "# Markdown content\nHere is some content.\n" { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_CRLF(t *testing.T) { + // Test with Windows-style CRLF line endings + content := "---\r\nname: windows-file\r\ndescription: File with CRLF endings\r\n---\r\n# Windows content\r\nWith CRLF.\r\n" + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "windows-file" { + t.Errorf("expected name 'windows-file', got %q", name) + } + if desc != "File with CRLF endings" { + t.Errorf("expected description 'File with CRLF endings', got %q", desc) + } + // Body should have CRLF stripped by TrimLeft + if !strings.Contains(body, "Windows content") { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_CRLFAtEOF(t *testing.T) { + // Test CRLF with frontmatter at end of file (no trailing content) + content := "---\r\nname: eof-test\r\ndescription: Frontmatter at EOF\r\n---" + + name, desc, _, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "eof-test" { + t.Errorf("expected name 'eof-test', got %q", name) + } + if desc != "Frontmatter at EOF" { + t.Errorf("expected description 'Frontmatter at EOF', got %q", desc) + } +} + +func TestParseFrontmatter_MixedLineEndings(t *testing.T) { + // Test with LF in frontmatter but CRLF in closing delimiter + content := "---\nname: mixed-file\ndescription: Mixed line endings\n---\r\n# Content\nHere.\n" + + name, desc, body, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "mixed-file" { + t.Errorf("expected name 'mixed-file', got %q", name) + } + if desc != "Mixed line endings" { + t.Errorf("expected description 'Mixed line endings', got %q", desc) + } + if !strings.Contains(body, "Content") { + t.Errorf("unexpected body: %q", body) + } +} + +func TestParseFrontmatter_Whitespace(t *testing.T) { + // Test that whitespace is trimmed from name and description + content := `--- +name: whitespace-name +description: Lots of whitespace +--- +Content +` + + name, desc, _, err := parseFrontmatter(content) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != "whitespace-name" { + t.Errorf("expected trimmed name 'whitespace-name', got %q", name) + } + if desc != "Lots of whitespace" { + t.Errorf("expected trimmed description 'Lots of whitespace', got %q", desc) + } +} + +func TestParseFrontmatter_MissingFrontmatter(t *testing.T) { + content := `# Just markdown content +No frontmatter here. +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for missing frontmatter") + } +} + +func TestParseFrontmatter_EmptyFrontmatter(t *testing.T) { + content := `--- +--- +Content +` + + name, desc, _, err := parseFrontmatter(content) + + // Empty frontmatter parses successfully but will fail validation + if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + if name != "" || desc != "" { + t.Error("expected empty name and description") + } +} + +func TestParseFrontmatter_UnknownFields(t *testing.T) { + content := `--- +name: test +description: Test +license: MIT +author: Someone +--- +Content +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for unknown fields") + } + if err != nil && err.Error() != "only 'name' and 'description' fields are allowed in frontmatter" { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestParseFrontmatter_InvalidYAML(t *testing.T) { + content := `--- +name: test +description: [unclosed bracket +--- +Content +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for invalid YAML") + } +} + +func TestParseFrontmatter_NoClosingDelimiter(t *testing.T) { + content := `--- +name: test +description: No closing delimiter +` + + _, _, _, err := parseFrontmatter(content) + + if err == nil { + t.Error("expected error for missing closing delimiter") + } +} + +func TestValidateName_Valid(t *testing.T) { + validNames := []string{ + "a", + "a1", + "aws-s3-security", + "kubernetes-resource-limits", + "test123", + "a-b-c-1-2-3", + } + + for _, name := range validNames { + err := validateName(name) + if err != nil { + t.Errorf("expected %q to be valid, got error: %v", name, err) + } + } +} + +func TestValidateName_Invalid(t *testing.T) { + tests := []struct { + name string + expectedErr string + }{ + {"", "name is required"}, + {" ", "name is required"}, + {"AWS-S3", "name must use kebab-case"}, + {"-leading-hyphen", "name must use kebab-case"}, + {"trailing-hyphen-", "name must use kebab-case"}, + {"123-starts-with-digit", "name must use kebab-case"}, + {"has_underscores", "name must use kebab-case"}, + {"has spaces", "name must use kebab-case"}, + {"Capital-Letter", "name must use kebab-case"}, + {string(make([]byte, 65)), "name must be 64 characters or less"}, // 65 chars + } + + for _, tt := range tests { + err := validateName(tt.name) + if err == nil { + t.Errorf("expected %q to be invalid", tt.name) + } else if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("for name %q, expected error containing %q, got %q", tt.name, tt.expectedErr, err.Error()) + } + } +} + +func TestValidateDescription_Valid(t *testing.T) { + validDescs := []string{ + "A", + "Short description", + string(make([]byte, 1024)), // exactly 1024 chars + } + + for _, desc := range validDescs { + err := validateDescription(desc) + if err != nil { + t.Errorf("expected %q to be valid, got error: %v", desc, err) + } + } +} + +func TestValidateDescription_Invalid(t *testing.T) { + tests := []struct { + desc string + expectedErr string + }{ + {"", "description is required"}, + {" ", "description is required"}, + {string(make([]byte, 1025)), "description must be 1024 characters or less"}, + } + + for _, tt := range tests { + err := validateDescription(tt.desc) + if err == nil { + t.Errorf("expected description to be invalid") + } else if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("expected error containing %q, got %q", tt.expectedErr, err.Error()) + } + } +} + +func TestDiscover_Deduplication(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create two files with same name + writeFile(t, filepath.Join(knowledgeDir, "aws-s3.md"), `--- +name: duplicate-name +description: First file +--- +First +`) + + writeFile(t, filepath.Join(knowledgeDir, "s3-aws.md"), `--- +name: duplicate-name +description: Second file +--- +Second +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 file (first wins), got %d", len(files)) + } + if len(warnings) != 1 { + t.Fatalf("expected 1 warning for duplicate, got %d", len(warnings)) + } + + // First file (lexicographic order) should win + if files[0].Description != "First file" { + t.Errorf("expected first file to win, got description: %q", files[0].Description) + } + + // Check warning message + if !strings.Contains(warnings[0].Reason, "duplicate name") { + t.Errorf("expected warning about duplicate name, got: %q", warnings[0].Reason) + } + if !strings.Contains(warnings[0].Reason, "aws-s3.md") { + t.Errorf("expected warning to mention first file, got: %q", warnings[0].Reason) + } +} + +func TestDiscover_DuplicateInSubfolder(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + + subdir := filepath.Join(knowledgeDir, "cloud") + err := os.MkdirAll(subdir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create files with same name in different folders + writeFile(t, filepath.Join(knowledgeDir, "aws.md"), `--- +name: aws-service +description: Root file +--- +Root +`) + + writeFile(t, filepath.Join(subdir, "aws.md"), `--- +name: aws-service +description: Subfolder file +--- +Subfolder +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 file, got %d", len(files)) + } + if len(warnings) != 1 { + t.Errorf("expected 1 warning for duplicate, got %d", len(warnings)) + } +} + +func TestDiscover_InvalidFilesProduceWarnings(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Invalid name + writeFile(t, filepath.Join(knowledgeDir, "invalid-name.md"), `--- +name: INVALID-NAME +description: Invalid name with uppercase +--- +Content +`) + + // Missing description + writeFile(t, filepath.Join(knowledgeDir, "no-desc.md"), `--- +name: no-description +--- +Content +`) + + // Invalid frontmatter + writeFile(t, filepath.Join(knowledgeDir, "bad-yaml.md"), `Not yaml frontmatter +`) + + // Valid file + writeFile(t, filepath.Join(knowledgeDir, "valid.md"), `--- +name: valid-file +description: This one is valid +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 valid file, got %d", len(files)) + } + if len(warnings) != 3 { + t.Fatalf("expected 3 warnings, got %d: %v", len(warnings), warnings) + } + + // Check that all warnings have paths and reasons + for _, w := range warnings { + if w.Path == "" { + t.Error("warning path should not be empty") + } + if w.Reason == "" { + t.Error("warning reason should not be empty") + } + } +} + +func TestDiscover_FileSizeLimit(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create a file that exceeds the size limit + // Generate content larger than 10MB + largeContent := "---\nname: large-file\ndescription: Too large\n---\n" + largeContent += strings.Repeat("x", 11*1024*1024) // 11MB of content + + writeFile(t, filepath.Join(knowledgeDir, "large.md"), largeContent) + + // Create a valid small file + writeFile(t, filepath.Join(knowledgeDir, "small.md"), `--- +name: small-file +description: Normal size +--- +Content +`) + + files, warnings := Discover(knowledgeDir) + + if len(files) != 1 { + t.Errorf("expected 1 valid file, got %d", len(files)) + } + if len(warnings) != 1 { + t.Fatalf("expected 1 warning for large file, got %d", len(warnings)) + } + + if !strings.Contains(warnings[0].Reason, "exceeds maximum") { + t.Errorf("expected warning about file size, got: %q", warnings[0].Reason) + } +} + +func TestDiscover_LexicographicOrdering(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, "knowledge") + err := os.Mkdir(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create files in non-alphabetical order + writeFile(t, filepath.Join(knowledgeDir, "zebra.md"), `--- +name: z-file +description: Last alphabetically +--- +Z +`) + + writeFile(t, filepath.Join(knowledgeDir, "apple.md"), `--- +name: a-file +description: First alphabetically +--- +A +`) + + writeFile(t, filepath.Join(knowledgeDir, "middle.md"), `--- +name: m-file +description: Middle alphabetically +--- +M +`) + + files, warnings := Discover(knowledgeDir) + + if len(warnings) != 0 { + t.Errorf("expected 0 warnings, got %d", len(warnings)) + } + if len(files) != 3 { + t.Fatalf("expected 3 files, got %d", len(files)) + } + + // Files should be processed in lexicographic order + if files[0].Name != "a-file" { + t.Errorf("expected first file to be 'a-file', got %q", files[0].Name) + } + if files[1].Name != "m-file" { + t.Errorf("expected second file to be 'm-file', got %q", files[1].Name) + } + if files[2].Name != "z-file" { + t.Errorf("expected third file to be 'z-file', got %q", files[2].Name) + } +} + +// Helper functions + +func writeFile(t *testing.T, path, content string) { + t.Helper() + err := os.WriteFile(path, []byte(content), 0644) + if err != nil { + t.Fatalf("failed to write file %s: %v", path, err) + } +} From cf502d70ccd04db554c4304f806ad1f04f877185 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 18 Feb 2026 16:36:36 +0100 Subject: [PATCH 32/51] =?UTF-8?q?feat(api-server):=20add=20GetOrCreateAWSE?= =?UTF-8?q?xternalId=20RPC=20for=20per-account=20AW=E2=80=A6=20(#3926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …S external ID Implements adr-external-id: a stable, server-generated UUID per Overmind account for AWS IAM trust policies (confused deputy protection). - Proto: new RPC + messages on ManagementService - Migration: aws_external_id column on accounts table - SQLC: atomic get-or-create with COALESCE, conditional updated_at - Handler: requires source:write/sources:write scope - CreateSource: auto-populates aws-external-id for AWS sources - Tests: idempotency, auth, auto-population, explicit ID preservation --- > [!NOTE] > **Medium Risk** > Touches API surface area and database schema, and changes source creation behavior for AWS sources; risk is moderate but bounded with idempotent/permission tests and an atomic get-or-create query. > > **Overview** > Adds a new `ManagementService.GetOrCreateAWSExternalId` RPC (and regenerated Go/TS clients) to return a stable, per-account UUID intended for AWS IAM trust policies. > > Persists the value by adding nullable `accounts.aws_external_id` plus an atomic SQLC `GetOrCreateAWSExternalId` query, exposes it via a scope-gated handler (`source:write`/`sources:write`), and updates `CreateSource` to auto-fill `aws-external-id` in AWS source configs when omitted (while preserving explicitly provided IDs). Includes DB migration updates and new tests covering idempotency, auth enforcement, and source config behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit af2b8b5a7d2fa1be09c2640508d083a64aa44bf6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 0f6fa7a78e4ad2bd868f59738bc524f62db85977 --- go/sdp-go/account.pb.go | 252 ++++++++++++++++-------- go/sdp-go/sdpconnect/account.connect.go | 33 ++++ 2 files changed, 203 insertions(+), 82 deletions(-) diff --git a/go/sdp-go/account.pb.go b/go/sdp-go/account.pb.go index ea051d50..8f4cd406 100644 --- a/go/sdp-go/account.pb.go +++ b/go/sdp-go/account.pb.go @@ -3804,6 +3804,86 @@ func (*UnsetGithubInstallationIDResponse) Descriptor() ([]byte, []int) { return file_account_proto_rawDescGZIP(), []int{65} } +type GetOrCreateAWSExternalIdRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOrCreateAWSExternalIdRequest) Reset() { + *x = GetOrCreateAWSExternalIdRequest{} + mi := &file_account_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOrCreateAWSExternalIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOrCreateAWSExternalIdRequest) ProtoMessage() {} + +func (x *GetOrCreateAWSExternalIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOrCreateAWSExternalIdRequest.ProtoReflect.Descriptor instead. +func (*GetOrCreateAWSExternalIdRequest) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{66} +} + +type GetOrCreateAWSExternalIdResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AwsExternalId string `protobuf:"bytes,1,opt,name=aws_external_id,json=awsExternalId,proto3" json:"aws_external_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetOrCreateAWSExternalIdResponse) Reset() { + *x = GetOrCreateAWSExternalIdResponse{} + mi := &file_account_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetOrCreateAWSExternalIdResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOrCreateAWSExternalIdResponse) ProtoMessage() {} + +func (x *GetOrCreateAWSExternalIdResponse) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOrCreateAWSExternalIdResponse.ProtoReflect.Descriptor instead. +func (*GetOrCreateAWSExternalIdResponse) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{67} +} + +func (x *GetOrCreateAWSExternalIdResponse) GetAwsExternalId() string { + if x != nil { + return x.AwsExternalId + } + return "" +} + // Team member related messages type ListTeamMembersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3813,7 +3893,7 @@ type ListTeamMembersRequest struct { func (x *ListTeamMembersRequest) Reset() { *x = ListTeamMembersRequest{} - mi := &file_account_proto_msgTypes[66] + mi := &file_account_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3825,7 +3905,7 @@ func (x *ListTeamMembersRequest) String() string { func (*ListTeamMembersRequest) ProtoMessage() {} func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[66] + mi := &file_account_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3838,7 +3918,7 @@ func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamMembersRequest.ProtoReflect.Descriptor instead. func (*ListTeamMembersRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{66} + return file_account_proto_rawDescGZIP(), []int{68} } type ListTeamMembersResponse struct { @@ -3850,7 +3930,7 @@ type ListTeamMembersResponse struct { func (x *ListTeamMembersResponse) Reset() { *x = ListTeamMembersResponse{} - mi := &file_account_proto_msgTypes[67] + mi := &file_account_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3862,7 +3942,7 @@ func (x *ListTeamMembersResponse) String() string { func (*ListTeamMembersResponse) ProtoMessage() {} func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[67] + mi := &file_account_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3875,7 +3955,7 @@ func (x *ListTeamMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTeamMembersResponse.ProtoReflect.Descriptor instead. func (*ListTeamMembersResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{67} + return file_account_proto_rawDescGZIP(), []int{69} } func (x *ListTeamMembersResponse) GetMembers() []*TeamMember { @@ -3899,7 +3979,7 @@ type TeamMember struct { func (x *TeamMember) Reset() { *x = TeamMember{} - mi := &file_account_proto_msgTypes[68] + mi := &file_account_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3911,7 +3991,7 @@ func (x *TeamMember) String() string { func (*TeamMember) ProtoMessage() {} func (x *TeamMember) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[68] + mi := &file_account_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3924,7 +4004,7 @@ func (x *TeamMember) ProtoReflect() protoreflect.Message { // Deprecated: Use TeamMember.ProtoReflect.Descriptor instead. func (*TeamMember) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{68} + return file_account_proto_rawDescGZIP(), []int{70} } func (x *TeamMember) GetUUID() []byte { @@ -3956,7 +4036,7 @@ type GetWelcomeScreenInformationRequest struct { func (x *GetWelcomeScreenInformationRequest) Reset() { *x = GetWelcomeScreenInformationRequest{} - mi := &file_account_proto_msgTypes[69] + mi := &file_account_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3968,7 +4048,7 @@ func (x *GetWelcomeScreenInformationRequest) String() string { func (*GetWelcomeScreenInformationRequest) ProtoMessage() {} func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[69] + mi := &file_account_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3981,7 +4061,7 @@ func (x *GetWelcomeScreenInformationRequest) ProtoReflect() protoreflect.Message // Deprecated: Use GetWelcomeScreenInformationRequest.ProtoReflect.Descriptor instead. func (*GetWelcomeScreenInformationRequest) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{69} + return file_account_proto_rawDescGZIP(), []int{71} } type InviterInformation struct { @@ -3995,7 +4075,7 @@ type InviterInformation struct { func (x *InviterInformation) Reset() { *x = InviterInformation{} - mi := &file_account_proto_msgTypes[70] + mi := &file_account_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4007,7 +4087,7 @@ func (x *InviterInformation) String() string { func (*InviterInformation) ProtoMessage() {} func (x *InviterInformation) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[70] + mi := &file_account_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4020,7 +4100,7 @@ func (x *InviterInformation) ProtoReflect() protoreflect.Message { // Deprecated: Use InviterInformation.ProtoReflect.Descriptor instead. func (*InviterInformation) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{70} + return file_account_proto_rawDescGZIP(), []int{72} } func (x *InviterInformation) GetEmail() string { @@ -4053,7 +4133,7 @@ type GetWelcomeScreenInformationResponse struct { func (x *GetWelcomeScreenInformationResponse) Reset() { *x = GetWelcomeScreenInformationResponse{} - mi := &file_account_proto_msgTypes[71] + mi := &file_account_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4065,7 +4145,7 @@ func (x *GetWelcomeScreenInformationResponse) String() string { func (*GetWelcomeScreenInformationResponse) ProtoMessage() {} func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Message { - mi := &file_account_proto_msgTypes[71] + mi := &file_account_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4078,7 +4158,7 @@ func (x *GetWelcomeScreenInformationResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use GetWelcomeScreenInformationResponse.ProtoReflect.Descriptor instead. func (*GetWelcomeScreenInformationResponse) Descriptor() ([]byte, []int) { - return file_account_proto_rawDescGZIP(), []int{71} + return file_account_proto_rawDescGZIP(), []int{73} } func (x *GetWelcomeScreenInformationResponse) GetInviterInformation() *InviterInformation { @@ -4310,7 +4390,10 @@ const file_account_proto_rawDesc = "" + "\x16github_installation_id\x18\x01 \x01(\x03R\x14githubInstallationId\"!\n" + "\x1fSetGithubInstallationIDResponse\"\"\n" + " UnsetGithubInstallationIDRequest\"#\n" + - "!UnsetGithubInstallationIDResponse\"\x18\n" + + "!UnsetGithubInstallationIDResponse\"!\n" + + "\x1fGetOrCreateAWSExternalIdRequest\"J\n" + + " GetOrCreateAWSExternalIdResponse\x12&\n" + + "\x0faws_external_id\x18\x01 \x01(\tR\rawsExternalId\"\x18\n" + "\x16ListTeamMembersRequest\"H\n" + "\x17ListTeamMembersResponse\x12-\n" + "\amembers\x18\x01 \x03(\v2\x13.account.TeamMemberR\amembers\"U\n" + @@ -4369,7 +4452,7 @@ const file_account_proto_rawDesc = "" + "\fUpdateSource\x12!.account.AdminUpdateSourceRequest\x1a\x1d.account.UpdateSourceResponse\x12P\n" + "\fDeleteSource\x12!.account.AdminDeleteSourceRequest\x1a\x1d.account.DeleteSourceResponse\x12\\\n" + "\x10KeepaliveSources\x12%.account.AdminKeepaliveSourcesRequest\x1a!.account.KeepaliveSourcesResponse\x12M\n" + - "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x98\x0f\n" + + "\vCreateToken\x12 .account.AdminCreateTokenRequest\x1a\x1c.account.CreateTokenResponse2\x89\x10\n" + "\x11ManagementService\x12E\n" + "\n" + "GetAccount\x12\x1a.account.GetAccountRequest\x1a\x1b.account.GetAccountResponse\x12N\n" + @@ -4392,7 +4475,8 @@ const file_account_proto_rawDesc = "" + "\x0fListTeamMembers\x12\x1f.account.ListTeamMembersRequest\x1a .account.ListTeamMembersResponse\x12x\n" + "\x1bGetWelcomeScreenInformation\x12+.account.GetWelcomeScreenInformationRequest\x1a,.account.GetWelcomeScreenInformationResponse\x12l\n" + "\x17SetGithubInstallationID\x12'.account.SetGithubInstallationIDRequest\x1a(.account.SetGithubInstallationIDResponse\x12r\n" + - "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" + "\x19UnsetGithubInstallationID\x12).account.UnsetGithubInstallationIDRequest\x1a*.account.UnsetGithubInstallationIDResponse\x12o\n" + + "\x18GetOrCreateAWSExternalId\x12(.account.GetOrCreateAWSExternalIdRequest\x1a).account.GetOrCreateAWSExternalIdResponseB1Z/github.com/overmindtech/workspace/go/sdp-go;sdpb\x06proto3" var ( file_account_proto_rawDescOnce sync.Once @@ -4407,7 +4491,7 @@ func file_account_proto_rawDescGZIP() []byte { } var file_account_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 72) +var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 74) var file_account_proto_goTypes = []any{ (SourceStatus)(0), // 0: account.SourceStatus (RepositoryStatus)(0), // 1: account.RepositoryStatus @@ -4480,16 +4564,18 @@ var file_account_proto_goTypes = []any{ (*SetGithubInstallationIDResponse)(nil), // 68: account.SetGithubInstallationIDResponse (*UnsetGithubInstallationIDRequest)(nil), // 69: account.UnsetGithubInstallationIDRequest (*UnsetGithubInstallationIDResponse)(nil), // 70: account.UnsetGithubInstallationIDResponse - (*ListTeamMembersRequest)(nil), // 71: account.ListTeamMembersRequest - (*ListTeamMembersResponse)(nil), // 72: account.ListTeamMembersResponse - (*TeamMember)(nil), // 73: account.TeamMember - (*GetWelcomeScreenInformationRequest)(nil), // 74: account.GetWelcomeScreenInformationRequest - (*InviterInformation)(nil), // 75: account.InviterInformation - (*GetWelcomeScreenInformationResponse)(nil), // 76: account.GetWelcomeScreenInformationResponse - (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 78: google.protobuf.Struct - (*durationpb.Duration)(nil), // 79: google.protobuf.Duration - (QueryMethod)(0), // 80: QueryMethod + (*GetOrCreateAWSExternalIdRequest)(nil), // 71: account.GetOrCreateAWSExternalIdRequest + (*GetOrCreateAWSExternalIdResponse)(nil), // 72: account.GetOrCreateAWSExternalIdResponse + (*ListTeamMembersRequest)(nil), // 73: account.ListTeamMembersRequest + (*ListTeamMembersResponse)(nil), // 74: account.ListTeamMembersResponse + (*TeamMember)(nil), // 75: account.TeamMember + (*GetWelcomeScreenInformationRequest)(nil), // 76: account.GetWelcomeScreenInformationRequest + (*InviterInformation)(nil), // 77: account.InviterInformation + (*GetWelcomeScreenInformationResponse)(nil), // 78: account.GetWelcomeScreenInformationResponse + (*timestamppb.Timestamp)(nil), // 79: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 80: google.protobuf.Struct + (*durationpb.Duration)(nil), // 81: google.protobuf.Duration + (QueryMethod)(0), // 82: QueryMethod } var file_account_proto_depIdxs = []int32{ 25, // 0: account.ListAccountsResponse.accounts:type_name -> account.Account @@ -4507,15 +4593,15 @@ var file_account_proto_depIdxs = []int32{ 54, // 12: account.AdminCreateTokenRequest.request:type_name -> account.CreateTokenRequest 23, // 13: account.Source.metadata:type_name -> account.SourceMetadata 24, // 14: account.Source.properties:type_name -> account.SourceProperties - 77, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp + 79, // 15: account.SourceMetadata.TokenExpiry:type_name -> google.protobuf.Timestamp 0, // 16: account.SourceMetadata.Status:type_name -> account.SourceStatus - 78, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct - 78, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct + 80, // 17: account.SourceProperties.Config:type_name -> google.protobuf.Struct + 80, // 18: account.SourceProperties.AdditionalConfig:type_name -> google.protobuf.Struct 26, // 19: account.Account.metadata:type_name -> account.AccountMetadata 28, // 20: account.Account.properties:type_name -> account.AccountProperties 27, // 21: account.AccountMetadata.repositories:type_name -> account.Repository 2, // 22: account.AccountMetadata.Plan:type_name -> account.AccountPlan - 77, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp + 79, // 23: account.Repository.lastChangeAt:type_name -> google.protobuf.Timestamp 1, // 24: account.Repository.status:type_name -> account.RepositoryStatus 25, // 25: account.GetAccountResponse.account:type_name -> account.Account 22, // 26: account.ListSourcesResponse.Sources:type_name -> account.Source @@ -4526,27 +4612,27 @@ var file_account_proto_depIdxs = []int32{ 22, // 31: account.UpdateSourceResponse.source:type_name -> account.Source 0, // 32: account.SourceKeepaliveResult.Status:type_name -> account.SourceStatus 0, // 33: account.SourceHealth.status:type_name -> account.SourceStatus - 77, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp - 77, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp - 77, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp + 79, // 34: account.SourceHealth.createdAt:type_name -> google.protobuf.Timestamp + 79, // 35: account.SourceHealth.lastHeartbeat:type_name -> google.protobuf.Timestamp + 79, // 36: account.SourceHealth.nextHeartbeat:type_name -> google.protobuf.Timestamp 3, // 37: account.SourceHealth.managed:type_name -> account.SourceManaged 48, // 38: account.SourceHealth.adapterMetadata:type_name -> account.AdapterMetadata 45, // 39: account.ListAllSourcesStatusResponse.sources:type_name -> account.SourceHealth - 79, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration + 81, // 40: account.SubmitSourceHeartbeatRequest.nextHeartbeatMax:type_name -> google.protobuf.Duration 3, // 41: account.SubmitSourceHeartbeatRequest.managed:type_name -> account.SourceManaged 48, // 42: account.SubmitSourceHeartbeatRequest.adapterMetadata:type_name -> account.AdapterMetadata 4, // 43: account.AdapterMetadata.category:type_name -> account.AdapterCategory 49, // 44: account.AdapterMetadata.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods 50, // 45: account.AdapterMetadata.terraformMappings:type_name -> account.TerraformMapping - 80, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod - 79, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration + 82, // 46: account.TerraformMapping.terraformMethod:type_name -> QueryMethod + 81, // 47: account.KeepaliveSourcesRequest.timeout:type_name -> google.protobuf.Duration 43, // 48: account.KeepaliveSourcesResponse.sources:type_name -> account.SourceKeepaliveResult 4, // 49: account.AvailableItemType.category:type_name -> account.AdapterCategory 49, // 50: account.AvailableItemType.supportedQueryMethods:type_name -> account.AdapterSupportedQueryMethods 58, // 51: account.ListAvailableItemTypesResponse.types:type_name -> account.AvailableItemType 45, // 52: account.GetSourceStatusResponse.source:type_name -> account.SourceHealth - 73, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember - 75, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation + 75, // 53: account.ListTeamMembersResponse.members:type_name -> account.TeamMember + 77, // 54: account.GetWelcomeScreenInformationResponse.inviter_information:type_name -> account.InviterInformation 5, // 55: account.AdminService.ListAccounts:input_type -> account.ListAccountsRequest 7, // 56: account.AdminService.CreateAccount:input_type -> account.CreateAccountRequest 11, // 57: account.AdminService.UpdateAccount:input_type -> account.AdminUpdateAccountRequest @@ -4576,45 +4662,47 @@ var file_account_proto_depIdxs = []int32{ 61, // 81: account.ManagementService.GetSourceStatus:input_type -> account.GetSourceStatusRequest 63, // 82: account.ManagementService.GetUserOnboardingStatus:input_type -> account.GetUserOnboardingStatusRequest 65, // 83: account.ManagementService.SetUserOnboardingStatus:input_type -> account.SetUserOnboardingStatusRequest - 71, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest - 74, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest + 73, // 84: account.ManagementService.ListTeamMembers:input_type -> account.ListTeamMembersRequest + 76, // 85: account.ManagementService.GetWelcomeScreenInformation:input_type -> account.GetWelcomeScreenInformationRequest 67, // 86: account.ManagementService.SetGithubInstallationID:input_type -> account.SetGithubInstallationIDRequest 69, // 87: account.ManagementService.UnsetGithubInstallationID:input_type -> account.UnsetGithubInstallationIDRequest - 6, // 88: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse - 8, // 89: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse - 10, // 90: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse - 30, // 91: account.AdminService.GetAccount:output_type -> account.GetAccountResponse - 14, // 92: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse - 34, // 93: account.AdminService.ListSources:output_type -> account.ListSourcesResponse - 36, // 94: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 95: account.AdminService.GetSource:output_type -> account.GetSourceResponse - 40, // 96: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 97: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse - 53, // 98: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 99: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse - 30, // 100: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse - 32, // 101: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse - 34, // 102: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse - 36, // 103: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse - 38, // 104: account.ManagementService.GetSource:output_type -> account.GetSourceResponse - 40, // 105: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse - 42, // 106: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse - 46, // 107: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 46, // 108: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse - 51, // 109: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse - 53, // 110: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse - 55, // 111: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse - 57, // 112: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse - 60, // 113: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse - 62, // 114: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse - 64, // 115: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse - 66, // 116: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse - 72, // 117: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse - 76, // 118: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse - 68, // 119: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse - 70, // 120: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse - 88, // [88:121] is the sub-list for method output_type - 55, // [55:88] is the sub-list for method input_type + 71, // 88: account.ManagementService.GetOrCreateAWSExternalId:input_type -> account.GetOrCreateAWSExternalIdRequest + 6, // 89: account.AdminService.ListAccounts:output_type -> account.ListAccountsResponse + 8, // 90: account.AdminService.CreateAccount:output_type -> account.CreateAccountResponse + 10, // 91: account.AdminService.UpdateAccount:output_type -> account.UpdateAccountResponse + 30, // 92: account.AdminService.GetAccount:output_type -> account.GetAccountResponse + 14, // 93: account.AdminService.DeleteAccount:output_type -> account.AdminDeleteAccountResponse + 34, // 94: account.AdminService.ListSources:output_type -> account.ListSourcesResponse + 36, // 95: account.AdminService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 96: account.AdminService.GetSource:output_type -> account.GetSourceResponse + 40, // 97: account.AdminService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 98: account.AdminService.DeleteSource:output_type -> account.DeleteSourceResponse + 53, // 99: account.AdminService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 100: account.AdminService.CreateToken:output_type -> account.CreateTokenResponse + 30, // 101: account.ManagementService.GetAccount:output_type -> account.GetAccountResponse + 32, // 102: account.ManagementService.DeleteAccount:output_type -> account.DeleteAccountResponse + 34, // 103: account.ManagementService.ListSources:output_type -> account.ListSourcesResponse + 36, // 104: account.ManagementService.CreateSource:output_type -> account.CreateSourceResponse + 38, // 105: account.ManagementService.GetSource:output_type -> account.GetSourceResponse + 40, // 106: account.ManagementService.UpdateSource:output_type -> account.UpdateSourceResponse + 42, // 107: account.ManagementService.DeleteSource:output_type -> account.DeleteSourceResponse + 46, // 108: account.ManagementService.ListAllSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 46, // 109: account.ManagementService.ListActiveSourcesStatus:output_type -> account.ListAllSourcesStatusResponse + 51, // 110: account.ManagementService.SubmitSourceHeartbeat:output_type -> account.SubmitSourceHeartbeatResponse + 53, // 111: account.ManagementService.KeepaliveSources:output_type -> account.KeepaliveSourcesResponse + 55, // 112: account.ManagementService.CreateToken:output_type -> account.CreateTokenResponse + 57, // 113: account.ManagementService.RevlinkWarmup:output_type -> account.RevlinkWarmupResponse + 60, // 114: account.ManagementService.ListAvailableItemTypes:output_type -> account.ListAvailableItemTypesResponse + 62, // 115: account.ManagementService.GetSourceStatus:output_type -> account.GetSourceStatusResponse + 64, // 116: account.ManagementService.GetUserOnboardingStatus:output_type -> account.GetUserOnboardingStatusResponse + 66, // 117: account.ManagementService.SetUserOnboardingStatus:output_type -> account.SetUserOnboardingStatusResponse + 74, // 118: account.ManagementService.ListTeamMembers:output_type -> account.ListTeamMembersResponse + 78, // 119: account.ManagementService.GetWelcomeScreenInformation:output_type -> account.GetWelcomeScreenInformationResponse + 68, // 120: account.ManagementService.SetGithubInstallationID:output_type -> account.SetGithubInstallationIDResponse + 70, // 121: account.ManagementService.UnsetGithubInstallationID:output_type -> account.UnsetGithubInstallationIDResponse + 72, // 122: account.ManagementService.GetOrCreateAWSExternalId:output_type -> account.GetOrCreateAWSExternalIdResponse + 89, // [89:123] is the sub-list for method output_type + 55, // [55:89] is the sub-list for method input_type 55, // [55:55] is the sub-list for extension type_name 55, // [55:55] is the sub-list for extension extendee 0, // [0:55] is the sub-list for field type_name @@ -4634,7 +4722,7 @@ func file_account_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_account_proto_rawDesc), len(file_account_proto_rawDesc)), NumEnums: 5, - NumMessages: 72, + NumMessages: 74, NumExtensions: 0, NumServices: 2, }, diff --git a/go/sdp-go/sdpconnect/account.connect.go b/go/sdp-go/sdpconnect/account.connect.go index 092d05d3..1c9c8162 100644 --- a/go/sdp-go/sdpconnect/account.connect.go +++ b/go/sdp-go/sdpconnect/account.connect.go @@ -132,6 +132,9 @@ const ( // ManagementServiceUnsetGithubInstallationIDProcedure is the fully-qualified name of the // ManagementService's UnsetGithubInstallationID RPC. ManagementServiceUnsetGithubInstallationIDProcedure = "/account.ManagementService/UnsetGithubInstallationID" + // ManagementServiceGetOrCreateAWSExternalIdProcedure is the fully-qualified name of the + // ManagementService's GetOrCreateAWSExternalId RPC. + ManagementServiceGetOrCreateAWSExternalIdProcedure = "/account.ManagementService/GetOrCreateAWSExternalId" ) // AdminServiceClient is a client for the account.AdminService service. @@ -583,6 +586,9 @@ type ManagementServiceClient interface { // this will unset the github installation ID for the account, allowing the user to install the github app again // it will also remove the organisation profile, so we no longer generate signals for that org UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) + // Returns a stable, per-account external ID for AWS IAM trust policies. + // Generates a UUID on first call; returns the same UUID on subsequent calls. + GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) } // NewManagementServiceClient constructs a client for the account.ManagementService service. By @@ -722,6 +728,12 @@ func NewManagementServiceClient(httpClient connect.HTTPClient, baseURL string, o connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), connect.WithClientOptions(opts...), ), + getOrCreateAWSExternalId: connect.NewClient[sdp_go.GetOrCreateAWSExternalIdRequest, sdp_go.GetOrCreateAWSExternalIdResponse]( + httpClient, + baseURL+ManagementServiceGetOrCreateAWSExternalIdProcedure, + connect.WithSchema(managementServiceMethods.ByName("GetOrCreateAWSExternalId")), + connect.WithClientOptions(opts...), + ), } } @@ -748,6 +760,7 @@ type managementServiceClient struct { getWelcomeScreenInformation *connect.Client[sdp_go.GetWelcomeScreenInformationRequest, sdp_go.GetWelcomeScreenInformationResponse] setGithubInstallationID *connect.Client[sdp_go.SetGithubInstallationIDRequest, sdp_go.SetGithubInstallationIDResponse] unsetGithubInstallationID *connect.Client[sdp_go.UnsetGithubInstallationIDRequest, sdp_go.UnsetGithubInstallationIDResponse] + getOrCreateAWSExternalId *connect.Client[sdp_go.GetOrCreateAWSExternalIdRequest, sdp_go.GetOrCreateAWSExternalIdResponse] } // GetAccount calls account.ManagementService.GetAccount. @@ -855,6 +868,11 @@ func (c *managementServiceClient) UnsetGithubInstallationID(ctx context.Context, return c.unsetGithubInstallationID.CallUnary(ctx, req) } +// GetOrCreateAWSExternalId calls account.ManagementService.GetOrCreateAWSExternalId. +func (c *managementServiceClient) GetOrCreateAWSExternalId(ctx context.Context, req *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) { + return c.getOrCreateAWSExternalId.CallUnary(ctx, req) +} + // ManagementServiceHandler is an implementation of the account.ManagementService service. type ManagementServiceHandler interface { // Get the details of the account that this user belongs to @@ -914,6 +932,9 @@ type ManagementServiceHandler interface { // this will unset the github installation ID for the account, allowing the user to install the github app again // it will also remove the organisation profile, so we no longer generate signals for that org UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) + // Returns a stable, per-account external ID for AWS IAM trust policies. + // Generates a UUID on first call; returns the same UUID on subsequent calls. + GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) } // NewManagementServiceHandler builds an HTTP handler from the service implementation. It returns @@ -1049,6 +1070,12 @@ func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.H connect.WithSchema(managementServiceMethods.ByName("UnsetGithubInstallationID")), connect.WithHandlerOptions(opts...), ) + managementServiceGetOrCreateAWSExternalIdHandler := connect.NewUnaryHandler( + ManagementServiceGetOrCreateAWSExternalIdProcedure, + svc.GetOrCreateAWSExternalId, + connect.WithSchema(managementServiceMethods.ByName("GetOrCreateAWSExternalId")), + connect.WithHandlerOptions(opts...), + ) return "/account.ManagementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ManagementServiceGetAccountProcedure: @@ -1093,6 +1120,8 @@ func NewManagementServiceHandler(svc ManagementServiceHandler, opts ...connect.H managementServiceSetGithubInstallationIDHandler.ServeHTTP(w, r) case ManagementServiceUnsetGithubInstallationIDProcedure: managementServiceUnsetGithubInstallationIDHandler.ServeHTTP(w, r) + case ManagementServiceGetOrCreateAWSExternalIdProcedure: + managementServiceGetOrCreateAWSExternalIdHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -1185,3 +1214,7 @@ func (UnimplementedManagementServiceHandler) SetGithubInstallationID(context.Con func (UnimplementedManagementServiceHandler) UnsetGithubInstallationID(context.Context, *connect.Request[sdp_go.UnsetGithubInstallationIDRequest]) (*connect.Response[sdp_go.UnsetGithubInstallationIDResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.UnsetGithubInstallationID is not implemented")) } + +func (UnimplementedManagementServiceHandler) GetOrCreateAWSExternalId(context.Context, *connect.Request[sdp_go.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp_go.GetOrCreateAWSExternalIdResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("account.ManagementService.GetOrCreateAWSExternalId is not implemented")) +} From f186902ba2e270febf105e94c57ab76624c36702 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:53:13 +0100 Subject: [PATCH 33/51] Eng 2433 create computesharedgalleryimage adapter (#3931) > [!NOTE] > **Medium Risk** > Introduces new Azure discovery adapters and wiring that will affect adapter registration and API calls during inventory; failures could impact discovery completeness for compute gallery resources. > > **Overview** > Adds new Azure compute adapters for **gallery image definitions** and **shared gallery images**, including `Get`/`Search` support, unique key composition, IAM permissions, Terraform mapping (gallery images only), and linked relationships (parent gallery/shared gallery plus URI-derived network links). > > Updates adapter initialization to create the new Azure SDK clients and register these adapters in both normal and metadata-only modes, and extends Azure item type/resource constants and resource-ID parsing keys for the new gallery image type. > > Refactors gallery application version link extraction by introducing `AppendURILinks` (HTTP + deduped DNS/IP links with configurable blast propagation) and reusing it for blob-source URIs; includes generated mocks and comprehensive tests for the new adapters and link behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8fed06542c12a92e3fa1a5acb28c29bad9416bdc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent Co-authored-by: Lionel Wilson GitOrigin-RevId: 4b68038f61ecbefb12eff3cd16b6fd3373b9d8ec --- .../azure/clients/gallery-images-client.go | 36 ++ .../clients/shared-gallery-images-client.go | 32 ++ sources/azure/manual/adapters.go | 44 +- .../compute-gallery-application-version.go | 43 +- sources/azure/manual/compute-gallery-image.go | 203 ++++++++ .../manual/compute-gallery-image_test.go | 466 ++++++++++++++++++ .../manual/compute-shared-gallery-image.go | 198 ++++++++ .../compute-shared-gallery-image_test.go | 464 +++++++++++++++++ sources/azure/manual/links_helpers.go | 62 +++ .../manual/mock_gallery_images_client_test.go | 72 +++ sources/azure/shared/item-types.go | 2 + .../mocks/mock_gallery_images_client.go | 72 +++ .../mock_shared_gallery_images_client.go | 72 +++ sources/azure/shared/models.go | 2 + sources/azure/shared/utils.go | 1 + 15 files changed, 1717 insertions(+), 52 deletions(-) create mode 100644 sources/azure/clients/gallery-images-client.go create mode 100644 sources/azure/clients/shared-gallery-images-client.go create mode 100644 sources/azure/manual/compute-gallery-image.go create mode 100644 sources/azure/manual/compute-gallery-image_test.go create mode 100644 sources/azure/manual/compute-shared-gallery-image.go create mode 100644 sources/azure/manual/compute-shared-gallery-image_test.go create mode 100644 sources/azure/manual/mock_gallery_images_client_test.go create mode 100644 sources/azure/shared/mocks/mock_gallery_images_client.go create mode 100644 sources/azure/shared/mocks/mock_shared_gallery_images_client.go diff --git a/sources/azure/clients/gallery-images-client.go b/sources/azure/clients/gallery-images-client.go new file mode 100644 index 00000000..2ae750ab --- /dev/null +++ b/sources/azure/clients/gallery-images-client.go @@ -0,0 +1,36 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_gallery_images_client.go -package=mocks -source=gallery-images-client.go + +// GalleryImagesPager is a type alias for the generic Pager interface with gallery image response type. +// This uses the generic Pager[T] interface to avoid code duplication. +type GalleryImagesPager = Pager[armcompute.GalleryImagesClientListByGalleryResponse] + +// GalleryImagesClient is an interface for interacting with Azure gallery image definitions +type GalleryImagesClient interface { + NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) GalleryImagesPager + Get(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) +} + +type galleryImagesClient struct { + client *armcompute.GalleryImagesClient +} + +func (c *galleryImagesClient) NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) GalleryImagesPager { + return c.client.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func (c *galleryImagesClient) Get(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewGalleryImagesClient creates a new GalleryImagesClient from the Azure SDK client +func NewGalleryImagesClient(client *armcompute.GalleryImagesClient) GalleryImagesClient { + return &galleryImagesClient{client: client} +} diff --git a/sources/azure/clients/shared-gallery-images-client.go b/sources/azure/clients/shared-gallery-images-client.go new file mode 100644 index 00000000..5625f30b --- /dev/null +++ b/sources/azure/clients/shared-gallery-images-client.go @@ -0,0 +1,32 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_shared_gallery_images_client.go -package=mocks -source=shared-gallery-images-client.go + +type SharedGalleryImagesPager = Pager[armcompute.SharedGalleryImagesClientListResponse] + +type SharedGalleryImagesClient interface { + NewListPager(location string, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) SharedGalleryImagesPager + Get(ctx context.Context, location string, galleryUniqueName string, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) +} + +type sharedGalleryImagesClient struct { + client *armcompute.SharedGalleryImagesClient +} + +func (c *sharedGalleryImagesClient) NewListPager(location string, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) SharedGalleryImagesPager { + return c.client.NewListPager(location, galleryUniqueName, options) +} + +func (c *sharedGalleryImagesClient) Get(ctx context.Context, location string, galleryUniqueName string, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) { + return c.client.Get(ctx, location, galleryUniqueName, galleryImageName, options) +} + +func NewSharedGalleryImagesClient(client *armcompute.SharedGalleryImagesClient) SharedGalleryImagesClient { + return &sharedGalleryImagesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 242ec381..df82a979 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -258,11 +258,21 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create gallery application versions client: %w", err) } + galleryImagesClient, err := armcompute.NewGalleryImagesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create gallery images client: %w", err) + } + snapshotsClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create snapshots client: %w", err) } + sharedGalleryImagesClient, err := armcompute.NewSharedGalleryImagesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create shared gallery images client: %w", err) + } + // Multi-scope resource group adapters (one adapter per type handling all resource groups) if len(resourceGroupScopes) > 0 { adapters = append(adapters, @@ -410,17 +420,29 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), resourceGroupScopes, ), cache), - sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( - clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), - resourceGroupScopes, - ), cache), - sources.WrapperToAdapter(NewComputeSnapshot( - clients.NewSnapshotsClient(snapshotsClient), - resourceGroupScopes, - ), cache), - ) + sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( + clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeGalleryImage( + clients.NewGalleryImagesClient(galleryImagesClient), + resourceGroupScopes, + ), cache), + sources.WrapperToAdapter(NewComputeSnapshot( + clients.NewSnapshotsClient(snapshotsClient), + resourceGroupScopes, + ), cache), + ) } + // Subscription-scoped adapters (not resource-group-scoped) + adapters = append(adapters, + sources.WrapperToAdapter(NewComputeSharedGalleryImage( + clients.NewSharedGalleryImagesClient(sharedGalleryImagesClient), + subscriptionID, + ), cache), + ) + log.WithFields(log.Fields{ "ovm.source.subscription_id": subscriptionID, "ovm.source.adapter_count": len(adapters), @@ -470,7 +492,9 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), - sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGalleryImage(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeSharedGalleryImage(nil, subscriptionID), noOpCache), ) _ = regions diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index 711c4285..c16a62fb 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -3,7 +3,6 @@ package manual import ( "context" "errors" - "net" "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" @@ -169,47 +168,7 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT if link == "" || (!strings.HasPrefix(link, "http://") && !strings.HasPrefix(link, "https://")) { return } - linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: stdlib.NetworkHTTP.String(), - Method: sdp.QueryMethod_SEARCH, - Query: link, - Scope: "global", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If endpoint is unavailable → version artifact cannot be accessed (In: true) - Out: true, // Endpoint may be used by other resources (Out: true) - }, - }) - hostFromURL := azureshared.ExtractDNSFromURL(link) - if hostFromURL != "" { - hostOnly := hostFromURL - if h, _, err := net.SplitHostPort(hostFromURL); err == nil { - hostOnly = h - } - if net.ParseIP(hostOnly) != nil { - if _, seen := seenIPs[hostOnly]; !seen { - seenIPs[hostOnly] = struct{}{} - linkedItemQueries = append(linkedItemQueries, networkIPQuery(hostOnly)) - } - } else { - if _, seen := linkedDNSHostnames[hostOnly]; !seen { - linkedDNSHostnames[hostOnly] = struct{}{} - linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: stdlib.NetworkDNS.String(), - Method: sdp.QueryMethod_SEARCH, - Query: hostOnly, - Scope: "global", - }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, - }) - } - } - } + AppendURILinks(&linkedItemQueries, link, linkedDNSHostnames, seenIPs, true, true) if accountName := azureshared.ExtractStorageAccountNameFromBlobURI(link); accountName != "" { if _, seen := seenStorageAccounts[accountName]; !seen { seenStorageAccounts[accountName] = struct{}{} diff --git a/sources/azure/manual/compute-gallery-image.go b/sources/azure/manual/compute-gallery-image.go new file mode 100644 index 00000000..dade2662 --- /dev/null +++ b/sources/azure/manual/compute-gallery-image.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/go/sdp-go" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + ComputeGalleryImageLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryImage) +) + +type computeGalleryImageWrapper struct { + client clients.GalleryImagesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGalleryImage(client clients.GalleryImagesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeGalleryImageWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGalleryImage, + ), + } +} + +func (c computeGalleryImageWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery image name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + galleryImageName := queryParts[1] + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, galleryImageName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryImageToSDPItem(&resp.GalleryImage, galleryName, scope) +} + +func (c computeGalleryImageWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByGalleryPager(rgScope.ResourceGroup, galleryName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, galleryImage := range page.Value { + if galleryImage == nil || galleryImage.Name == nil { + continue + } + item, sdpErr := c.azureGalleryImageToSDPItem(galleryImage, galleryName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryImageWrapper) azureGalleryImageToSDPItem( + galleryImage *armcompute.GalleryImage, + galleryName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(galleryImage, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if galleryImage.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery image name is nil"), scope, c.Type()) + } + galleryImageName := *galleryImage.Name + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(galleryName, galleryImageName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Gallery: image definition depends on gallery + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGallery.String(), + Method: sdp.QueryMethod_GET, + Query: galleryName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If gallery is deleted → image definition is deleted + Out: false, // If image definition is deleted → gallery remains + }, + }) + + // URI-based links: Eula, PrivacyStatementURI, ReleaseNoteURI + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if galleryImage.Properties != nil { + if galleryImage.Properties.Eula != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + } + if galleryImage.Properties.PrivacyStatementURI != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + } + if galleryImage.Properties.ReleaseNoteURI != nil { + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.ReleaseNoteURI, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGalleryImage.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(galleryImage.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeGalleryImageWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + ComputeGalleryImageLookupByName, + } +} + +func (c computeGalleryImageWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeGalleryLookupByName, + }, + } +} + +func (c computeGalleryImageWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGallery, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/shared_image +func (c computeGalleryImageWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + // example id: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Compute/galleries/gallery1/images/image1 + TerraformQueryMap: "azurerm_shared_image.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryImageWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/images/read", + } +} + +func (c computeGalleryImageWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery-image_test.go b/sources/azure/manual/compute-gallery-image_test.go new file mode 100644 index 00000000..6f15e0c2 --- /dev/null +++ b/sources/azure/manual/compute-gallery-image_test.go @@ -0,0 +1,466 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +// mockGalleryImagesPager is a mock pager for ListByGallery. +type mockGalleryImagesPager struct { + pages []armcompute.GalleryImagesClientListByGalleryResponse + index int +} + +func (m *mockGalleryImagesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockGalleryImagesPager) NextPage(ctx context.Context) (armcompute.GalleryImagesClientListByGalleryResponse, error) { + if m.index >= len(m.pages) { + return armcompute.GalleryImagesClientListByGalleryResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorGalleryImagesPager is a mock pager that always returns an error. +type errorGalleryImagesPager struct{} + +func (e *errorGalleryImagesPager) More() bool { + return true +} + +func (e *errorGalleryImagesPager) NextPage(ctx context.Context) (armcompute.GalleryImagesClientListByGalleryResponse, error) { + return armcompute.GalleryImagesClientListByGalleryResponse{}, errors.New("pager error") +} + +// testGalleryImagesClient wraps the mock and returns a pager from NewListByGalleryPager. +type testGalleryImagesClient struct { + *MockGalleryImagesClient + pager clients.GalleryImagesPager +} + +// NewListByGalleryPager returns the test pager so we don't need to mock this call. +func (t *testGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + if t.pager != nil { + return t.pager + } + return t.MockGalleryImagesClient.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func createAzureGalleryImage(imageName string) *armcompute.GalleryImage { + return &armcompute.GalleryImage{ + Name: to.Ptr(imageName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.GalleryImageProperties{ + Identifier: &armcompute.GalleryImageIdentifier{ + Publisher: to.Ptr("test-publisher"), + Offer: to.Ptr("test-offer"), + SKU: to.Ptr("test-sku"), + }, + OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), + OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + }, + } +} + +func createAzureGalleryImageWithURIs(imageName string) *armcompute.GalleryImage { + img := createAzureGalleryImage(imageName) + img.Properties.Eula = to.Ptr("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + img.Properties.ReleaseNoteURI = to.Ptr("https://releases.example.com/notes") + return img +} + +func TestComputeGalleryImage(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + galleryName := "test-gallery" + galleryImageName := "test-image" + + t.Run("Get", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryImage.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGalleryImage.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(galleryName, galleryImageName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag env=test, got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithURIs", func(t *testing.T) { + image := createAzureGalleryImageWithURIs(galleryImageName) + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://releases.example.com/notes", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "releases.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_PlainTextEula_NoLinks", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.Eula = to.Ptr("This software is provided as-is. No warranty.") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Plain-text Eula should not generate HTTP/DNS/IP links + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq == nil { + continue + } + typ := lq.GetType() + if typ == stdlib.NetworkHTTP.String() || typ == stdlib.NetworkDNS.String() || typ == stdlib.NetworkIP.String() { + t.Errorf("Plain-text Eula must not create network links; found linked query type %s with query %s", typ, lq.GetQuery()) + } + } + }) + + t.Run("Get_SameHostDeduplication", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + image.Properties.ReleaseNoteURI = to.Ptr("https://example.com/release-notes") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Should have 2 HTTP links (one per URI) but only 1 DNS link (same hostname) + httpCount := 0 + dnsCount := 0 + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil { + if query.GetType() == stdlib.NetworkHTTP.String() { + httpCount++ + } + if query.GetType() == stdlib.NetworkDNS.String() { + dnsCount++ + } + } + } + if httpCount != 2 { + t.Errorf("Expected 2 HTTP links, got %d", httpCount) + } + if dnsCount != 1 { + t.Errorf("Expected 1 DNS link (deduped), got %d", dnsCount) + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + image := createAzureGalleryImage(galleryImageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( + armcompute.GalleryImagesClientGetResponse{ + GalleryImage: *image, + }, nil) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryImageName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + query := q.GetQuery() + if query != nil && query.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if query.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", query.GetMethod()) + } + if query.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", query.GetScope()) + } + if query.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", query.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when PrivacyStatementURI host is an IP address") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Adapter expects query to split into 2 parts (gallery, image); single part is invalid + _, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryImageName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Get_EmptyImageName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, "") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when image name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("image not found") + mockClient := NewMockGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, "nonexistent", nil).Return( + armcompute.GalleryImagesClientGetResponse{}, expectedErr) + + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, "nonexistent") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + img1 := createAzureGalleryImage("image-1") + img2 := createAzureGalleryImage("image-2") + + mockClient := NewMockGalleryImagesClient(ctrl) + pages := []armcompute.GalleryImagesClientListByGalleryResponse{ + { + GalleryImageList: armcompute.GalleryImageList{ + Value: []*armcompute.GalleryImage{img1, img2}, + }, + }, + } + mockPager := &mockGalleryImagesPager{pages: pages} + testClient := &testGalleryImagesClient{ + MockGalleryImagesClient: mockClient, + pager: mockPager, + } + + wrapper := NewComputeGalleryImage(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + sdpItems, err := searchable.Search(ctx, scope, galleryName, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + // Search expects exactly 1 query part; giving 2 is invalid + searchQuery := shared.CompositeLookupKey(galleryName, galleryImageName) + _, err := searchable.Search(ctx, scope, searchQuery, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyGalleryName", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error when gallery name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + errorPager := &errorGalleryImagesPager{} + testClient := &testGalleryImagesClient{ + MockGalleryImagesClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeGalleryImage(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + _, err := searchable.Search(ctx, scope, galleryName, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeGallery: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := NewMockGalleryImagesClient(ctrl) + wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} diff --git a/sources/azure/manual/compute-shared-gallery-image.go b/sources/azure/manual/compute-shared-gallery-image.go new file mode 100644 index 00000000..9b1a6445 --- /dev/null +++ b/sources/azure/manual/compute-shared-gallery-image.go @@ -0,0 +1,198 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ( + ComputeSharedGalleryImageLookupByLocation = shared.NewItemTypeLookup("location", azureshared.ComputeSharedGalleryImage) + ComputeSharedGalleryImageLookupByGalleryUniqueName = shared.NewItemTypeLookup("galleryUniqueName", azureshared.ComputeSharedGalleryImage) + ComputeSharedGalleryImageLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeSharedGalleryImage) +) + +type computeSharedGalleryImageWrapper struct { + client clients.SharedGalleryImagesClient + *azureshared.SubscriptionBase +} + +func NewComputeSharedGalleryImage(client clients.SharedGalleryImagesClient, subscriptionID string) sources.SearchableWrapper { + return &computeSharedGalleryImageWrapper{ + client: client, + SubscriptionBase: azureshared.NewSubscriptionBase( + subscriptionID, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeSharedGalleryImage, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/shared-gallery-images/get +func (c computeSharedGalleryImageWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 3 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 3: location, gallery unique name, and image name"), scope, c.Type()) + } + location := queryParts[0] + if location == "" { + return nil, azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type()) + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + return nil, azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type()) + } + galleryImageName := queryParts[2] + if galleryImageName == "" { + return nil, azureshared.QueryError(errors.New("gallery image name cannot be empty"), scope, c.Type()) + } + + resp, err := c.client.Get(ctx, location, galleryUniqueName, galleryImageName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureSharedGalleryImageToSDPItem(&resp.SharedGalleryImage, location, galleryUniqueName, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/shared-gallery-images/list +func (c computeSharedGalleryImageWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 2 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 2: location and gallery unique name"), scope, c.Type()) + } + location := queryParts[0] + if location == "" { + return nil, azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type()) + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + return nil, azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type()) + } + + pager := c.client.NewListPager(location, galleryUniqueName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, image := range page.Value { + if image == nil || image.Name == nil { + continue + } + item, sdpErr := c.azureSharedGalleryImageToSDPItem(image, location, galleryUniqueName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeSharedGalleryImageWrapper) azureSharedGalleryImageToSDPItem( + image *armcompute.SharedGalleryImage, + location, + galleryUniqueName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + if image.Name == nil { + return nil, azureshared.QueryError(errors.New("shared gallery image name is nil"), scope, c.Type()) + } + + attributes, err := shared.ToAttributesWithExclude(image) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + imageName := *image.Name + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(location, galleryUniqueName, imageName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Shared Gallery: image definition depends on shared gallery (Microsoft.Compute/locations/sharedGalleries) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGallery.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(location, galleryUniqueName), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + In: true, // If shared gallery is removed → image is no longer visible + Out: false, // If image definition is deleted → shared gallery remains + }, + }) + + // URI-based links. Note: armcompute.SharedGalleryImageProperties has no ReleaseNoteURI field (unlike GalleryImage). + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if image.Properties != nil { + if image.Properties.Eula != nil { + AppendURILinks(&linkedItemQueries, *image.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + } + if image.Properties.PrivacyStatementURI != nil { + AppendURILinks(&linkedItemQueries, *image.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeSharedGalleryImage.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeSharedGalleryImageWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeSharedGalleryImageLookupByLocation, + ComputeSharedGalleryImageLookupByGalleryUniqueName, + ComputeSharedGalleryImageLookupByName, + } +} + +func (c computeSharedGalleryImageWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeSharedGalleryImageLookupByLocation, + ComputeSharedGalleryImageLookupByGalleryUniqueName, + }, + } +} + +func (c computeSharedGalleryImageWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeSharedGallery, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// Shared gallery images are read-only views with no direct Terraform resource mapping. +func (c computeSharedGalleryImageWrapper) TerraformMappings() []*sdp.TerraformMapping { + return nil +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeSharedGalleryImageWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/locations/sharedGalleries/images/read", + } +} + +func (c computeSharedGalleryImageWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-shared-gallery-image_test.go b/sources/azure/manual/compute-shared-gallery-image_test.go new file mode 100644 index 00000000..9f5647d1 --- /dev/null +++ b/sources/azure/manual/compute-shared-gallery-image_test.go @@ -0,0 +1,464 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +func TestComputeSharedGalleryImage(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + location := "eastus" + galleryUniqueName := "test-gallery-unique-name" + imageName := "test-image" + + t.Run("Get", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeSharedGalleryImage.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeSharedGalleryImage.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithURIs", func(t *testing.T) { + image := createSharedGalleryImageWithURIs(imageName) + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_PlainTextEula_NoLinks", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.Eula = to.Ptr("This software is provided as-is. No warranty.") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq == nil { + continue + } + typ := lq.GetType() + if typ == stdlib.NetworkHTTP.String() || typ == stdlib.NetworkDNS.String() || typ == stdlib.NetworkIP.String() { + t.Errorf("Plain-text Eula must not create network links; found linked query type %s with query %s", typ, lq.GetQuery()) + } + } + }) + + t.Run("Get_SameHostDeduplication", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.Eula = to.Ptr("https://example.com/eula") + image.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + httpCount := 0 + dnsCount := 0 + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq != nil { + if lq.GetType() == stdlib.NetworkHTTP.String() { + httpCount++ + } + if lq.GetType() == stdlib.NetworkDNS.String() { + dnsCount++ + } + } + } + if httpCount != 2 { + t.Errorf("Expected 2 HTTP links, got %d", httpCount) + } + if dnsCount != 1 { + t.Errorf("Expected 1 DNS link (deduped), got %d", dnsCount) + } + }) + + t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { + image := createSharedGalleryImage(imageName) + image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{ + SharedGalleryImage: *image, + }, nil) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + hasIP := false + for _, q := range sdpItem.GetLinkedItemQueries() { + lq := q.GetQuery() + if lq != nil && lq.GetType() == stdlib.NetworkIP.String() { + hasIP = true + if lq.GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected NetworkIP link to use GET, got %v", lq.GetMethod()) + } + if lq.GetScope() != "global" { + t.Errorf("Expected NetworkIP link scope global, got %s", lq.GetScope()) + } + if lq.GetQuery() != "192.168.1.10" { + t.Errorf("Expected NetworkIP link query 192.168.1.10, got %s", lq.GetQuery()) + } + break + } + } + if !hasIP { + t.Error("Expected NetworkIP linked query when PrivacyStatementURI host is an IP address") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], location, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyLocation", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryUniqueName, imageName) + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when location is empty, but got nil") + } + }) + + t.Run("Get_EmptyGalleryUniqueName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, "", imageName) + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when gallery unique name is empty, but got nil") + } + }) + + t.Run("Get_EmptyImageName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when image name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("image not found") + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockClient.EXPECT().Get(ctx, location, galleryUniqueName, "nonexistent", nil).Return( + armcompute.SharedGalleryImagesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(location, galleryUniqueName, "nonexistent") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when client returns error, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + img1 := createSharedGalleryImage("image-1") + img2 := createSharedGalleryImage("image-2") + + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + mockPager := newMockSharedGalleryImagesPager([]*armcompute.SharedGalleryImage{img1, img2}) + mockClient.EXPECT().NewListPager(location, galleryUniqueName, nil).Return(mockPager) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if err := item.Validate(); err != nil { + t.Errorf("Expected valid item, got: %v", err) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName, imageName) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyLocation", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], "", galleryUniqueName) + if qErr == nil { + t.Error("Expected error when location is empty, but got nil") + } + }) + + t.Run("Search_EmptyGalleryUniqueName", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], location, "") + if qErr == nil { + t.Error("Expected error when gallery unique name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + errorPager := &errorSharedGalleryImagesPager{} + mockClient.EXPECT().NewListPager(location, galleryUniqueName, nil).Return(errorPager) + + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter does not support Search operation") + } + + searchQuery := shared.CompositeLookupKey(location, galleryUniqueName) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], searchQuery, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeSharedGallery: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + stdlib.NetworkIP: true, + } + for itemType, want := range expected { + if got := links[itemType]; got != want { + t.Errorf("PotentialLinks()[%v] = %v, want %v", itemType, got, want) + } + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) + wrapper := manual.NewComputeSharedGalleryImage(mockClient, subscriptionID) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} + +func createSharedGalleryImage(name string) *armcompute.SharedGalleryImage { + return &armcompute.SharedGalleryImage{ + Name: to.Ptr(name), + Location: to.Ptr("eastus"), + Identifier: &armcompute.SharedGalleryIdentifier{ + UniqueID: to.Ptr("/SharedGalleries/test-gallery-unique-name"), + }, + Properties: &armcompute.SharedGalleryImageProperties{ + Identifier: &armcompute.GalleryImageIdentifier{ + Publisher: to.Ptr("test-publisher"), + Offer: to.Ptr("test-offer"), + SKU: to.Ptr("test-sku"), + }, + OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), + OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + }, + } +} + +func createSharedGalleryImageWithURIs(name string) *armcompute.SharedGalleryImage { + img := createSharedGalleryImage(name) + img.Properties.Eula = to.Ptr("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = to.Ptr("https://example.com/privacy") + return img +} + +type mockSharedGalleryImagesPager struct { + pages []armcompute.SharedGalleryImagesClientListResponse + index int +} + +func newMockSharedGalleryImagesPager(items []*armcompute.SharedGalleryImage) clients.SharedGalleryImagesPager { + return &mockSharedGalleryImagesPager{ + pages: []armcompute.SharedGalleryImagesClientListResponse{ + { + SharedGalleryImageList: armcompute.SharedGalleryImageList{ + Value: items, + }, + }, + }, + index: 0, + } +} + +func (m *mockSharedGalleryImagesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSharedGalleryImagesPager) NextPage(ctx context.Context) (armcompute.SharedGalleryImagesClientListResponse, error) { + if m.index >= len(m.pages) { + return armcompute.SharedGalleryImagesClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSharedGalleryImagesPager struct{} + +func (e *errorSharedGalleryImagesPager) More() bool { + return true +} + +func (e *errorSharedGalleryImagesPager) NextPage(ctx context.Context) (armcompute.SharedGalleryImagesClientListResponse, error) { + return armcompute.SharedGalleryImagesClientListResponse{}, errors.New("pager error") +} diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index 5e6eb2ae..98fa99f8 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -1,7 +1,11 @@ package manual import ( + "net" + "strings" + "github.com/overmindtech/cli/go/sdp-go" + azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -27,6 +31,64 @@ func appendLinkIfValid( } } +// AppendURILinks appends linked item queries for a URI: HTTP link plus DNS or IP link from the host (with deduplication). +// It mutates linkedItemQueries and the dedupe maps. Skips empty or non-http(s) URIs. +// blastIn and blastOut set BlastPropagation for the added HTTP/DNS/IP links. +func AppendURILinks( + linkedItemQueries *[]*sdp.LinkedItemQuery, + uri string, + linkedDNSHostnames map[string]struct{}, + seenIPs map[string]struct{}, + blastIn, blastOut bool, +) { + if uri == "" || (!strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://")) { + return + } + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: uri, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, + }) + hostFromURL := azureshared.ExtractDNSFromURL(uri) + if hostFromURL != "" { + hostOnly := hostFromURL + if h, _, err := net.SplitHostPort(hostFromURL); err == nil { + hostOnly = h + } + if net.ParseIP(hostOnly) != nil { + if _, seen := seenIPs[hostOnly]; !seen { + seenIPs[hostOnly] = struct{}{} + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: hostOnly, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, + }) + } + } else { + if _, seen := linkedDNSHostnames[hostOnly]; !seen { + linkedDNSHostnames[hostOnly] = struct{}{} + *linkedItemQueries = append(*linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: hostOnly, + Scope: "global", + }, + BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, + }) + } + } + } +} + // networkIPQuery returns a linked item query for stdlib.NetworkIP. func networkIPQuery(query string) *sdp.LinkedItemQuery { return &sdp.LinkedItemQuery{ diff --git a/sources/azure/manual/mock_gallery_images_client_test.go b/sources/azure/manual/mock_gallery_images_client_test.go new file mode 100644 index 00000000..58709319 --- /dev/null +++ b/sources/azure/manual/mock_gallery_images_client_test.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sources/azure/clients/gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=sources/azure/manual/mock_gallery_images_client_test.go -package=manual -source=sources/azure/clients/gallery-images-client.go +// + +// Package manual is a generated GoMock package. +package manual + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryImagesClient is a mock of GalleryImagesClient interface. +type MockGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockGalleryImagesClientMockRecorder is the mock recorder for MockGalleryImagesClient. +type MockGalleryImagesClientMockRecorder struct { + mock *MockGalleryImagesClient +} + +// NewMockGalleryImagesClient creates a new mock instance. +func NewMockGalleryImagesClient(ctrl *gomock.Controller) *MockGalleryImagesClient { + mock := &MockGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryImagesClient) EXPECT() *MockGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryImagesClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.GalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryImagesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryImagesClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryImagesPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryImagesClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryImagesClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 09953d9a..41b2f74d 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -24,9 +24,11 @@ var ( ComputeDiskAccess = shared.NewItemType(Azure, Compute, DiskAccess) ComputeDiskAccessPrivateEndpointConnection = shared.NewItemType(Azure, Compute, DiskAccessPrivateEndpointConnection) ComputeSharedGalleryImage = shared.NewItemType(Azure, Compute, SharedGalleryImage) + ComputeSharedGallery = shared.NewItemType(Azure, Compute, SharedGallery) ComputeCommunityGalleryImage = shared.NewItemType(Azure, Compute, CommunityGalleryImage) ComputeGalleryApplication = shared.NewItemType(Azure, Compute, GalleryApplication) ComputeGalleryApplicationVersion = shared.NewItemType(Azure, Compute, GalleryApplicationVersion) + ComputeGalleryImage = shared.NewItemType(Azure, Compute, GalleryImage) ComputeGallery = shared.NewItemType(Azure, Compute, Gallery) // Network item types diff --git a/sources/azure/shared/mocks/mock_gallery_images_client.go b/sources/azure/shared/mocks/mock_gallery_images_client.go new file mode 100644 index 00000000..585f8825 --- /dev/null +++ b/sources/azure/shared/mocks/mock_gallery_images_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_gallery_images_client.go -package=mocks -source=gallery-images-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleryImagesClient is a mock of GalleryImagesClient interface. +type MockGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockGalleryImagesClientMockRecorder is the mock recorder for MockGalleryImagesClient. +type MockGalleryImagesClientMockRecorder struct { + mock *MockGalleryImagesClient +} + +// NewMockGalleryImagesClient creates a new mock instance. +func NewMockGalleryImagesClient(ctrl *gomock.Controller) *MockGalleryImagesClient { + mock := &MockGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryImagesClient) EXPECT() *MockGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryImagesClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryImageName string, options *armcompute.GalleryImagesClientGetOptions) (armcompute.GalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.GalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryImagesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryImagesClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryImageName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryImagesClientListByGalleryOptions) clients.GalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryImagesPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryImagesClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryImagesClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/shared/mocks/mock_shared_gallery_images_client.go b/sources/azure/shared/mocks/mock_shared_gallery_images_client.go new file mode 100644 index 00000000..5cff0b31 --- /dev/null +++ b/sources/azure/shared/mocks/mock_shared_gallery_images_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: shared-gallery-images-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_shared_gallery_images_client.go -package=mocks -source=shared-gallery-images-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSharedGalleryImagesClient is a mock of SharedGalleryImagesClient interface. +type MockSharedGalleryImagesClient struct { + ctrl *gomock.Controller + recorder *MockSharedGalleryImagesClientMockRecorder + isgomock struct{} +} + +// MockSharedGalleryImagesClientMockRecorder is the mock recorder for MockSharedGalleryImagesClient. +type MockSharedGalleryImagesClientMockRecorder struct { + mock *MockSharedGalleryImagesClient +} + +// NewMockSharedGalleryImagesClient creates a new mock instance. +func NewMockSharedGalleryImagesClient(ctrl *gomock.Controller) *MockSharedGalleryImagesClient { + mock := &MockSharedGalleryImagesClient{ctrl: ctrl} + mock.recorder = &MockSharedGalleryImagesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSharedGalleryImagesClient) EXPECT() *MockSharedGalleryImagesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSharedGalleryImagesClient) Get(ctx context.Context, location, galleryUniqueName, galleryImageName string, options *armcompute.SharedGalleryImagesClientGetOptions) (armcompute.SharedGalleryImagesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, location, galleryUniqueName, galleryImageName, options) + ret0, _ := ret[0].(armcompute.SharedGalleryImagesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSharedGalleryImagesClientMockRecorder) Get(ctx, location, galleryUniqueName, galleryImageName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSharedGalleryImagesClient)(nil).Get), ctx, location, galleryUniqueName, galleryImageName, options) +} + +// NewListPager mocks base method. +func (m *MockSharedGalleryImagesClient) NewListPager(location, galleryUniqueName string, options *armcompute.SharedGalleryImagesClientListOptions) clients.SharedGalleryImagesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", location, galleryUniqueName, options) + ret0, _ := ret[0].(clients.SharedGalleryImagesPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockSharedGalleryImagesClientMockRecorder) NewListPager(location, galleryUniqueName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockSharedGalleryImagesClient)(nil).NewListPager), location, galleryUniqueName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index a081da54..0025c3c7 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -72,9 +72,11 @@ const ( DiskAccess shared.Resource = "disk-access" DiskAccessPrivateEndpointConnection shared.Resource = "disk-access-private-endpoint-connection" SharedGalleryImage shared.Resource = "shared-gallery-image" + SharedGallery shared.Resource = "shared-gallery" CommunityGalleryImage shared.Resource = "community-gallery-image" GalleryApplicationVersion shared.Resource = "gallery-application-version" GalleryApplication shared.Resource = "gallery-application" + GalleryImage shared.Resource = "gallery-image" Gallery shared.Resource = "gallery" // Network resources diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 06c9a0dd..941ea07f 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -29,6 +29,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-compute-virtual-machine-run-command": {"virtualMachines", "runCommands"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/runCommands/{runCommandName}", "azure-compute-virtual-machine-extension": {"virtualMachines", "extensions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{virtualMachineName}/extensions/{extensionName}", "azure-compute-gallery-application-version": {"galleries", "applications", "versions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}/versions/{versionName}", + "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", } if keys, ok := pathKeysMap[resourceType]; ok { From e2be846f8d203b5c0bb86e0b29319a7eded96b2a Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 18 Feb 2026 18:31:32 +0100 Subject: [PATCH 34/51] Fix merge error (#3933) > [!NOTE] > **Low Risk** > Test-only change that relaxes assertions around link metadata; low risk aside from potentially reducing coverage for blast propagation behavior. > > **Overview** > Updates Azure compute gallery image unit tests to **stop asserting `BlastPropagation`** on linked `QueryTests` entries. > > This resolves mismatched expectations in `compute-gallery-image_test.go` and `compute-shared-gallery-image_test.go` by only validating type/method/query/scope for the generated links (gallery parent and derived HTTP/DNS searches). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e8935a9f8e1f46ba083e65ad010f9933831d1bfd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f0ad140cf2223e9c188b16a396c3a6d998a7d3d9 --- .../azure/manual/compute-gallery-image_test.go | 16 ++++++++-------- .../manual/compute-shared-gallery-image_test.go | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sources/azure/manual/compute-gallery-image_test.go b/sources/azure/manual/compute-gallery-image_test.go index 6f15e0c2..5a2cd299 100644 --- a/sources/azure/manual/compute-gallery-image_test.go +++ b/sources/azure/manual/compute-gallery-image_test.go @@ -138,7 +138,7 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -164,13 +164,13 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://releases.example.com/notes", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "releases.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://releases.example.com/notes", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "releases.example.com", ExpectedScope: "global"}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/compute-shared-gallery-image_test.go b/sources/azure/manual/compute-shared-gallery-image_test.go index 9f5647d1..25efb0a7 100644 --- a/sources/azure/manual/compute-shared-gallery-image_test.go +++ b/sources/azure/manual/compute-shared-gallery-image_test.go @@ -64,7 +64,7 @@ func TestComputeSharedGalleryImage(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) @@ -90,11 +90,11 @@ func TestComputeSharedGalleryImage(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ - {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID, ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, - {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global", ExpectedBlastPropagation: &sdp.BlastPropagation{In: true, Out: false}}, + {ExpectedType: azureshared.ComputeSharedGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey(location, galleryUniqueName), ExpectedScope: subscriptionID}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://eula.example.com/terms", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "eula.example.com", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkHTTP.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "https://example.com/privacy", ExpectedScope: "global"}, + {ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "example.com", ExpectedScope: "global"}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) From 1e3ae173664e06bb8fee33468c080a172a5800c0 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 18 Feb 2026 19:15:35 +0100 Subject: [PATCH 35/51] chore(deps): lock file maintenance (#3887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Update | Change | |---|---| | lockFileMaintenance | All locks refreshed | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. 🔧 This Pull Request updates lock files to use the latest dependency versions. --- ### Configuration 📅 **Schedule**: Branch creation - "before 4am on monday" in timezone Europe/London, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/overmindtech/workspace). GitOrigin-RevId: e527ca609e089cdbdeb0e53b8ebb59b0fc910f24 --- .terraform.lock.hcl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 539dd127..589f730a 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "6.32.0" + version = "6.32.1" constraints = ">= 4.56.0" hashes = [ - "h1:06E17LKFuocdc5P7YHtAmig8Zu+S+gy4CKhYhdxh434=", - "h1:GmBSZ299apGpQl3y7W8AlBQKXtpUQvis75x5tb8t5hs=", - "h1:KlmHUHezk7AkOTTJLhl8JpBOsV+qK6wV9Py3vIsN7Mo=", - "h1:NoPDPkJFBBCZiJIvPhyQG198UmDBXoX2yHnkeJFW0gU=", - "h1:Orm3zdQYphZwxTzDxMQznPAgssFi9OOME2MzJUBg1P8=", - "h1:SQSVbmxbj9QHkla4qaAiJOgjjZD2bUOLrniCT8DEpfQ=", - "h1:VI0iNxbVgblVEil2nyJgtHqijaLQE6VAgv55EWi56Ow=", - "h1:Yq/qR5VAO2WSH4gextYqb7zTcCNgO7Q7G8cJR2ps23E=", - "h1:bm1kmfgtxYybVyTG/zlQVOpaDxMZ9aOQ7kmX0BjAbxs=", - "h1:hFdNMX6bUH1hyxVGW8MxdUhPSUpQRNFR4cscSPfJDzk=", - "h1:kB9++004H9FgpxdtZS11L2ESIw1tJqNW4xsyt1WM0+c=", - "h1:mi90ohakIqjaalUb185KRxb/F+etJN7LB2/44k8+u9g=", - "h1:qA6Rfb/qlHhjnAmyLtv/jfu/tO2XOmp0tNNP0Fdmzfs=", - "h1:rriTF/YcLBH13/IijWHwuWAG84KRPw3x++afslASeL4=", - "zh:100d4cffe4be3d0ae41799ab1bb649f0dd1d40aebe8956d0a30ab923e417e085", - "zh:132ed90c0572a6ff5d760b5b2f2e18e67eabed44286619475777de647cd4fe7b", - "zh:4bc7efffa57ed187a4ae1b88a81fd0fd7663610aad8a1d4e3f2c6d19621a5424", - "zh:5682f9486a77d41245867f20e4858fac7e62b2e32cdf8553fd3eeeb520349294", - "zh:65524aa437659dc111f1c9bb304f18f63b368c32a2375651752ce01400321f6a", - "zh:9086e392b5759e35610b45f0e19348916a6782707ab24a0acafa89e020976393", + "h1:/kSj+4KeiYIJR4GZKUIp+NjaOSGPbEpoJFo+n8r21iQ=", + "h1:2vgwE6+ZCd7tLwQOb41OO0nLYAgV7ssIb8Xr9CdUupo=", + "h1:5tbI29RszzXinjHPzy5Qqp1ooS3/T+zLrjodyr0osJs=", + "h1:CAu73BoUtKnbgWP6oBs5pCTXL+Hfy8xc3sWZuZf1vEk=", + "h1:HYVnQr6ZXWVB/U3j/VuDZmn5fdzrCD6StyC3t7LM150=", + "h1:ONBRGZUR973/u9y3Yf2yxYGNHH4acyVm0r03iHxS4L8=", + "h1:Qhwre3rhX8AN+kAOiNjyS9uNtjlpK8yhwtoQPAJ2HyM=", + "h1:S9FhFACHbATLcz5I7dsM7KwUL94hdyfxj2AhHZ4rtKA=", + "h1:e8ls5XHmaRt5w4XVpWhon8BnmdaFqYtm+kIhIutyR4g=", + "h1:j691GxEePvwjhYV08mwgTLD/CiCG4YHdZOXL+gV6qt0=", + "h1:mdjVlnAux6Ddq2c+14yDmwT1PBK0t9D/v/y2sv3Pyu4=", + "h1:pIWlu/D0yFPM5dk37TWNaRUZZSNOs9MW7to8IP/F5TA=", + "h1:uHqBzBSaSuJrwsqcbF4kY5tW8hc2qAlVLAl7QdRrEmE=", + "h1:yqfHWALCdPdktH7iDa9gWKmxHeTl8r7Wu97JDeRO3tI=", + "zh:024d2cc116c8c83bb63b71623e3654109948791b250929449f4533b06678d574", + "zh:0ee944eb1c0b28957ad04541546ebac66f81b74ae811d20bcd7043d0313722e1", + "zh:43f1b6bcc2d6ba34dd4f02aab2ef3923281cf82455e608ac1ea493374dbb132d", + "zh:52e91c66c3d946d9d24ecf6684e23337abbe7e93a7e8d927f8b7cc69d096215e", + "zh:5d8030a02b61256fb6ee51efe70c1ddfc0d57b4dc0f25c621afddab81575a9c2", + "zh:67b25c8732af678af5772cf57bfb68937bdb535ef06f7f353202e272d843f52c", + "zh:6e846e85e55d7c49820410fb3db338e2d2adf19e3481558e3bec0d63b953c521", + "zh:8d4922a86a39cb2788c14f430008fcaf236b0023260439bc95cc7758d5b76f4a", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:b76afced8e29db21f6d49ef3fbb2ed504f4da11cad1585c53a1c0a141c707ebe", - "zh:c3611868f7504e3e1d35d4b834d59c2de30718c4581f5f06afc448525884eeec", - "zh:c62f1f262975ccb9014815484ff09b3854668384213797cf6e6bea9b237a469e", - "zh:c6cdcb1d859b83b2507f163aeeea0a9019eacdeb90baca65fe014713485aae6b", - "zh:d157ccba5e47ac9c919c4b7c00cbb34ffd0e67dcf90e5b67e24f5f639cd1e496", - "zh:d21b00b8b4cfc6234794ad83ee37756b08eded9fcf262455d4443c38ac020917", - "zh:db55a5057cda8ca84445a362366b2bc15ffef55297ef1c883eb9ac52439c5933", - "zh:f2497a4cece92bc16ffdb9f0445f28bea1b99ef06923871ab1ef16c69b7dce36", + "zh:9e3d4d1848fc6675c6bd88087188f229c4ec98b1a35de97c2697a0160fb76678", + "zh:b21c1b932c896c21988baac3b1cbc8b51843581b8fabf5e396952a329c9e6a12", + "zh:df8e5b1a2713880e2b3c489cc22ad3b14490e1702a1637273f91747bf091c071", + "zh:ec66785d40f7c04f138bb94fec55b8ddaae6fcc9cb25cc388989150bfaf2de4c", + "zh:f1ecb00fcfdb0c2aec3622549c023f469db401f395bc25bbddfe5cf8b51cd046", + "zh:fca78bf28897c8077130ce8d0f4d67900dbd77619adb1326bcd017ef421e5f1f", ] } From e95ef38b3f6c5823134d98c78c2ef7ae60bcde31 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 18 Feb 2026 19:21:00 +0100 Subject: [PATCH 36/51] fix, cli uses get change's changeAnalysisStatus (#3842) https://github.com/user-attachments/assets/a56fe17e-cb1a-4520-9368-76b965a998e7 > [!NOTE] > **Medium Risk** > Touches end-to-end polling/control-flow for change analysis completion and risk retrieval in both CLI UX and server-side run-task execution; mistakes could cause hangs, premature exits, or incorrect failure/retry behavior. > > **Overview** > **Stops using `GetChangeTimelineV2` to detect change-analysis completion** in multiple CLI commands and the API server run-task worker, and instead polls `GetChange` and inspects `change.metadata.change_analysis_status` (handling DONE/SKIPPED/ERROR/in-progress states). > > In `terraform plan` and the run-task flow, **risk extraction is decoupled from timeline entries** by calling `GetChangeRisks` after analysis completes, with added nil checks and updated error handling/messages (including retry vs. fail semantics in the worker). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cbdf9ebb49ce6eeb6b981499960b665d2c525329. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fa48bdae414cbba13288d9f798d506abe0017728 --- cmd/changes_get_change.go | 48 +++++++++--------- cmd/changes_get_signals.go | 48 +++++++++--------- cmd/changes_start_change.go | 59 +++++++++------------- cmd/terraform_plan.go | 98 +++++++++++++++++-------------------- 4 files changed, 112 insertions(+), 141 deletions(-) diff --git a/cmd/changes_get_change.go b/cmd/changes_get_change.go index 93f50d10..7bf1a03d 100644 --- a/cmd/changes_get_change.go +++ b/cmd/changes_get_change.go @@ -72,47 +72,43 @@ func GetChange(cmd *cobra.Command, args []string) error { } client := AuthenticatedChangesClient(ctx, oi) - var timeLine *sdp.GetChangeTimelineV2Response fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine = rawTimeLine.Msg - for _, entry := range timeLine.GetEntries() { - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // find the running timeline entry - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for change analysis to complete") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), diff --git a/cmd/changes_get_signals.go b/cmd/changes_get_signals.go index 8edb9dd6..03bf9ff3 100644 --- a/cmd/changes_get_signals.go +++ b/cmd/changes_get_signals.go @@ -55,47 +55,43 @@ func GetSignals(cmd *cobra.Command, args []string) error { } client := AuthenticatedChangesClient(ctx, oi) - var timeLine *sdp.GetChangeTimelineV2Response fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine = rawTimeLine.Msg - for _, entry := range timeLine.GetEntries() { - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // find the running timeline entry - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for change analysis to complete") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), diff --git a/cmd/changes_start_change.go b/cmd/changes_start_change.go index 56e93fae..5d757aef 100644 --- a/cmd/changes_start_change.go +++ b/cmd/changes_start_change.go @@ -1,7 +1,7 @@ package cmd import ( - "regexp" + "fmt" "time" "connectrpc.com/connect" @@ -43,56 +43,45 @@ func StartChange(cmd *cobra.Command, args []string) error { "ticket-link": viper.GetString("ticket-link"), } - // poll the timeline for the Calculated Blast Radius to be complete + // wait for change analysis to complete (poll GetChange by change_analysis_status) client := AuthenticatedChangesClient(ctx, oi) fetch: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { + if err != nil || changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { return loggedError{ - err: timelineErr, + err: err, fields: lf, - message: "failed to get timeline", + message: "failed to get change", } } - timeLine := rawTimeLine.Msg - // Use a case-insensitive regex to match any entry containing "blast radius" - blastRadiusRegex := regexp.MustCompile(`(?i)blast\s+radius`) - for _, entry := range timeLine.GetEntries() { - if blastRadiusRegex.MatchString(entry.GetName()) { - if entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - break fetch - } - if entry.GetStatus() == sdp.ChangeTimelineEntryStatus_ERROR { - // the api server will retry the blast radius calculation, so lets wait for the retry - log.WithContext(ctx).WithFields(lf).Warn("Blast radius calculation failed, waiting for retry") - break - } + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + return loggedError{ + err: fmt.Errorf("change metadata or change analysis status is nil"), + fields: lf, + message: "failed to get change", } } - // display the running entry - runningEntry, contentDescription, status, err := sdp.TimelineFindInProgressEntry(timeLine.GetEntries()) - if err != nil { + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + break fetch + case sdp.ChangeAnalysisStatus_STATUS_ERROR: return loggedError{ - err: err, + err: fmt.Errorf("change analysis completed with error status"), fields: lf, - message: "failed to find running entry", + message: "change analysis failed", } + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + log.WithContext(ctx).WithFields(lf).WithField("status", status.String()).Info("Waiting for change analysis to complete") } - // log progress while waiting for blast radius calculation - log.WithContext(ctx).WithFields(log.Fields{ - "status": status.String(), - "running": runningEntry, - "content": contentDescription, - }).Info("Waiting for blast radius to be calculated") - // retry time.Sleep(3 * time.Second) - - // check if the context is cancelled if ctx.Err() != nil { return loggedError{ err: ctx.Err(), diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index b54f026e..8a3d343e 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -336,73 +336,63 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI log.WithField("change-url", changeUrl.String()).Info("Change ready") /////////////////////////////////////////////////////////////////// - // wait for change analysis to complete + // wait for change analysis to complete (poll GetChange by change_analysis_status) /////////////////////////////////////////////////////////////////// changeAnalysisSpinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Change Analysis") - var timeLine *sdp.GetChangeTimelineV2Response - milestoneSpinners := []*pterm.SpinnerPrinter{} retryLoop: for { - rawTimeLine, timelineErr := client.GetChangeTimelineV2(ctx, &connect.Request[sdp.GetChangeTimelineV2Request]{ - Msg: &sdp.GetChangeTimelineV2Request{ - ChangeUUID: changeUuid[:], + changeRes, err := client.GetChange(ctx, &connect.Request[sdp.GetChangeRequest]{ + Msg: &sdp.GetChangeRequest{ + UUID: changeUuid[:], }, }) - if timelineErr != nil || rawTimeLine.Msg == nil { - changeAnalysisSpinner.Fail(fmt.Sprintf("Change Analysis failed to get timeline: %v", timelineErr)) - return nil + if err != nil { + changeAnalysisSpinner.Fail(fmt.Sprintf("Change Analysis failed to get change: %v", err)) + return fmt.Errorf("failed to get change during change analysis: %w", err) } - timeLine = rawTimeLine.Msg - - // display the status for the timeline entries - for i, entry := range timeLine.GetEntries() { - // populate the spinner list on the first run - if i <= len(milestoneSpinners) { - milestoneSpinners = append(milestoneSpinners, pterm.DefaultSpinner. - WithWriter(multi.NewWriter()). - WithIndentation(IndentSymbol()). - WithText(entry.GetName())) - } - // render the spinner for this entry - switch entry.GetStatus() { - case sdp.ChangeTimelineEntryStatus_PENDING: - continue - case sdp.ChangeTimelineEntryStatus_IN_PROGRESS: - if !milestoneSpinners[i].IsActive { - milestoneSpinners[i], _ = milestoneSpinners[i].Start() - } - case sdp.ChangeTimelineEntryStatus_ERROR: - milestoneSpinners[i].Fail() - case sdp.ChangeTimelineEntryStatus_DONE: - milestoneSpinners[i].Success() - case sdp.ChangeTimelineEntryStatus_UNSPECIFIED: - // do nothing - default: - milestoneSpinners[i].Fail(fmt.Sprintf("Unknown status: %v", entry.GetStatus())) - } - - // ENG-1993: This is temporary to still track the auto tagging entry in the timeline. this is to prevent the cli from hanging - // check if change analysis is done - if entry.GetName() == sdp.ChangeTimelineEntryV2IDAutoTagging.Name && entry.GetStatus() == sdp.ChangeTimelineEntryStatus_DONE { - changeAnalysisSpinner.Success() - break retryLoop - } + if changeRes.Msg == nil || changeRes.Msg.GetChange() == nil { + changeAnalysisSpinner.Fail("Change Analysis failed: received empty change response") + return fmt.Errorf("change analysis failed: received empty change response") + } + ch := changeRes.Msg.GetChange() + md := ch.GetMetadata() + if md == nil || md.GetChangeAnalysisStatus() == nil { + changeAnalysisSpinner.Fail("Change Analysis failed: change metadata or analysis status missing") + return fmt.Errorf("change analysis failed: change metadata or change analysis status is nil") + } + status := md.GetChangeAnalysisStatus().GetStatus() + switch status { + case sdp.ChangeAnalysisStatus_STATUS_DONE, sdp.ChangeAnalysisStatus_STATUS_SKIPPED: + changeAnalysisSpinner.Success() + break retryLoop + case sdp.ChangeAnalysisStatus_STATUS_ERROR: + changeAnalysisSpinner.Fail("Change analysis failed") + return fmt.Errorf("change analysis completed with error status") + case sdp.ChangeAnalysisStatus_STATUS_UNSPECIFIED, sdp.ChangeAnalysisStatus_STATUS_INPROGRESS: + // keep polling } - // retry time.Sleep(3 * time.Second) - } - var calculateRiskStep *sdp.ChangeTimelineEntryV2 - for _, entry := range timeLine.GetEntries() { - if entry.GetName() == sdp.ChangeTimelineEntryV2IDCalculatedRisks.Name { - calculateRiskStep = entry - break + if ctx.Err() != nil { + changeAnalysisSpinner.Fail("Cancelled") + return ctx.Err() } } - if calculateRiskStep == nil || calculateRiskStep.GetCalculatedRisks() == nil { - return fmt.Errorf("Failed to get calculated risks") + risksRes, err := client.GetChangeRisks(ctx, &connect.Request[sdp.GetChangeRisksRequest]{ + Msg: &sdp.GetChangeRisksRequest{ + UUID: changeUuid[:], + }, + }) + if err != nil { + return fmt.Errorf("failed to get calculated risks: %w", err) + } + if risksRes.Msg == nil { + return fmt.Errorf("failed to get calculated risks: response message was nil") + } + if risksRes.Msg.GetChangeRiskMetadata() == nil { + return fmt.Errorf("failed to get calculated risks: change risk metadata was nil") } - calculatedRisks := calculateRiskStep.GetCalculatedRisks().GetRisks() + calculatedRisks := risksRes.Msg.GetChangeRiskMetadata().GetRisks() // Submit milestone for tracing if cmdSpan != nil { cmdSpan.AddEvent("Change Analysis finished", trace.WithAttributes( From bac4f779765230c8930c07af4cce81efc28f515e Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 19 Feb 2026 16:17:07 +0100 Subject: [PATCH 37/51] Snapshot discovery source (#3904) Sample LIST all from explore local run with a test snapshot: image Implements a new discovery source that serves data from a snapshot file or URL to enable consistent local testing and deterministic v6 investigation reruns. --- Linear Issue: [ENG-2577](https://linear.app/overmind/issue/ENG-2577/implement-snapshot-source-for-local-testing)

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Introduces a new discovery source and changes `cli explore` startup behavior when `SNAPSHOT_SOURCE` is set, which could affect local workflows and query results. Runtime risk is mostly around snapshot parsing/indexing correctness and engine startup/shutdown handling rather than security-sensitive logic. > > **Overview** > Adds a new `sources/snapshot` discovery source that loads a protobuf snapshot from a file or HTTP(S) URL, builds an in-memory index (including hydrating `LinkedItems` from snapshot edges), and registers per-type adapters that implement `GET`/`LIST`/`SEARCH` with adapter metadata pulled from an embedded JSON catalog. > > Updates `cli explore` so setting `SNAPSHOT_SOURCE` bypasses all live cloud sources and starts only the snapshot engine, and adds supporting VS Code launch/docs wiring (embedded adapter catalog FS and snapshot source README). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9a8beba18cb5c843a28bd1c1b9b17710175c670d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: b5bfb065399f74053e579380bb69413c42f04566 --- cmd/explore.go | 46 +++- docs.overmind.tech/docs/sources/embed.go | 11 + sources/snapshot/README.md | 169 ++++++++++++ sources/snapshot/adapters/adapter.go | 149 +++++++++++ sources/snapshot/adapters/adapter_test.go | 297 ++++++++++++++++++++++ sources/snapshot/adapters/catalog.go | 96 +++++++ sources/snapshot/adapters/index.go | 242 ++++++++++++++++++ sources/snapshot/adapters/index_test.go | 288 +++++++++++++++++++++ sources/snapshot/adapters/loader.go | 89 +++++++ sources/snapshot/adapters/loader_test.go | 161 ++++++++++++ sources/snapshot/adapters/main.go | 45 ++++ sources/snapshot/cmd/root.go | 233 +++++++++++++++++ sources/snapshot/main.go | 11 + 13 files changed, 1836 insertions(+), 1 deletion(-) create mode 100644 docs.overmind.tech/docs/sources/embed.go create mode 100644 sources/snapshot/README.md create mode 100644 sources/snapshot/adapters/adapter.go create mode 100644 sources/snapshot/adapters/adapter_test.go create mode 100644 sources/snapshot/adapters/catalog.go create mode 100644 sources/snapshot/adapters/index.go create mode 100644 sources/snapshot/adapters/index_test.go create mode 100644 sources/snapshot/adapters/loader.go create mode 100644 sources/snapshot/adapters/loader_test.go create mode 100644 sources/snapshot/adapters/main.go create mode 100644 sources/snapshot/cmd/root.go create mode 100644 sources/snapshot/main.go diff --git a/cmd/explore.go b/cmd/explore.go index 743240ee..20df5072 100644 --- a/cmd/explore.go +++ b/cmd/explore.go @@ -20,10 +20,11 @@ import ( "github.com/overmindtech/cli/tfutils" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/tracing" azureproc "github.com/overmindtech/cli/sources/azure/proc" gcpproc "github.com/overmindtech/cli/sources/gcp/proc" + snapshotadapters "github.com/overmindtech/cli/sources/snapshot/adapters" stdlibSource "github.com/overmindtech/cli/stdlib-source/adapters" - "github.com/overmindtech/cli/go/tracing" "github.com/pkg/browser" log "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" @@ -43,6 +44,8 @@ The CLI automatically discovers and uses: - GCP providers from your Terraform configuration (google and google-beta) - Falls back to default cloud provider credentials if no Terraform providers are found +Set SNAPSHOT_SOURCE to a snapshot file path or URL to run only the snapshot source (no cloud sources will be started). Useful for local testing with fixed data. + For GCP, ensure you have appropriate permissions (roles/browser or equivalent) to access project metadata.`, PreRun: PreRunSetup, @@ -75,6 +78,47 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut return func() {}, fmt.Errorf("failed to get hostname: %w", err) } + // If SNAPSHOT_SOURCE is set, run ONLY the snapshot source -- skip all live sources. + // Snapshot mode replays pre-recorded data, so cloud providers are unnecessary. + if snapshotSourcePath := os.Getenv("SNAPSHOT_SOURCE"); snapshotSourcePath != "" { + snapshotSpinner, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Starting snapshot source engine (snapshot-only mode)") + + ec := discovery.EngineConfig{ + EngineType: "cli-snapshot", + Version: fmt.Sprintf("cli-%v", tracing.Version()), + SourceName: fmt.Sprintf("snapshot-source-%v", hostname), + SourceUUID: uuid.New(), + App: oi.ApiUrl.Host, + ApiKey: token.AccessToken, + NATSOptions: &natsOpts, + MaxParallelExecutions: 2_000, + HeartbeatOptions: heartbeatOptions(oi, token), + } + snapshotEngine, err := discovery.NewEngine(&ec) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to create snapshot source engine: %v", err)) + return func() {}, fmt.Errorf("failed to create snapshot source engine: %w", err) + } + err = snapshotadapters.InitializeAdapters(ctx, snapshotEngine, snapshotSourcePath) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to initialize snapshot source adapters: %v", err)) + return func() {}, fmt.Errorf("failed to initialize snapshot source adapters: %w", err) + } + err = snapshotEngine.Start(ctx) + if err != nil { + snapshotSpinner.Fail(fmt.Sprintf("Failed to start snapshot source engine: %v", err)) + return func() {}, fmt.Errorf("failed to start snapshot source engine: %w", err) + } + snapshotEngine.StartSendingHeartbeats(ctx) + snapshotSpinner.Success("Snapshot source engine started (snapshot-only mode)") + + return func() { + if err := snapshotEngine.Stop(); err != nil { + log.WithError(err).Error("failed to stop snapshot engine") + } + }, nil + } + p := pool.NewWithResults[[]*discovery.Engine]().WithErrors() // find all the terraform files diff --git a/docs.overmind.tech/docs/sources/embed.go b/docs.overmind.tech/docs/sources/embed.go new file mode 100644 index 00000000..e501df8a --- /dev/null +++ b/docs.overmind.tech/docs/sources/embed.go @@ -0,0 +1,11 @@ +// Package adapterdata embeds the per-type adapter metadata JSON files so +// other packages can look up category, descriptive name, supported query +// methods, and potential links without duplicating the data. +package adapterdata + +import "embed" + +// Files contains every adapter JSON file under {provider}/data/*.json. +// +//go:embed */data/*.json +var Files embed.FS diff --git a/sources/snapshot/README.md b/sources/snapshot/README.md new file mode 100644 index 00000000..4981e1d7 --- /dev/null +++ b/sources/snapshot/README.md @@ -0,0 +1,169 @@ +# Snapshot Source + +A discovery source that serves items from a snapshot file or URL, enabling local testing with fixed data and deterministic re-runs of v6 investigation jobs. + +## Overview + +The snapshot source loads a protobuf snapshot (`.pb` file) at startup and responds to NATS discovery queries (GET, LIST, SEARCH) with items from that snapshot. This enables: + +- **Local testing**: Run backend services (gateway, api-server, NATS) locally with consistent snapshot data +- **Deterministic v6 re-runs**: Re-run change analysis and blast radius calculations with the same snapshot data +- **Consistent exploration**: Query the same fixed data set repeatedly for debugging and testing + +## Features + +- **Snapshot loading**: Loads snapshots from local files or HTTP(S) URLs +- **Wildcard scope support**: Single adapter handles all types and scopes in the snapshot +- **Full query support**: Implements GET, LIST, and SEARCH query methods +- **In-memory indexing**: Fast lookups by type, scope, GUN, or query string +- **Comprehensive tests**: Unit tests for loader, index, and adapter components + +## Usage + +### Configuration + +The snapshot source requires a snapshot file or URL to be specified: + +**Environment variables:** +- `SNAPSHOT_SOURCE` or `SNAPSHOT_PATH` or `SNAPSHOT_URL` - Path to snapshot file or HTTP(S) URL +- Standard discovery engine config (NATS connection, auth, etc.) + +**Command-line flags:** +```bash +--snapshot-source # Path to snapshot file or URL (required) +--log # Log level (default: info) +--json-log # JSON logging (default: true) +--health-check-port # Health check port (default: 8089) +``` + +### Running Locally + +#### Option 1: With backend services (recommended) + +1. Start backend services (gateway, api-server, NATS) in devcontainer or via docker-compose +2. Run the snapshot source: + +```bash +ALLOW_UNAUTHENTICATED=true \ +SNAPSHOT_SOURCE=/workspace/services/api-server/service/changeanalysis/testdata/snapshot.pb \ +NATS_SERVICE_HOST=nats \ +NATS_SERVICE_PORT=4222 \ +go run ./sources/snapshot/main.go --log=debug --json-log=false +``` + +#### Option 2: Using VS Code launch configuration + +Use the provided launch configurations in `.vscode/launch.json`: + +- **"snapshot-source (with backend)"**: For use when backend services are running +- **"snapshot-source (standalone)"**: For standalone debugging with local NATS + +Update the `SNAPSHOT_SOURCE` environment variable in the launch config to point to your snapshot file. + +#### Option 3: Load snapshot from URL + +```bash +ALLOW_UNAUTHENTICATED=true \ +SNAPSHOT_SOURCE=https://gateway-host/area51/snapshots/{uuid}/protobuf \ +NATS_SERVICE_HOST=nats \ +NATS_SERVICE_PORT=4222 \ +go run ./sources/snapshot/main.go +``` + +### Query Behavior + +The snapshot source implements a **wildcard scope adapter** that handles all types and scopes: + +- **LIST**: Returns all items in the snapshot (or filtered by scope if scope != "*") +- **GET**: Finds an item by its globally unique name (GUN) or unique attribute value +- **SEARCH**: Searches items by regex pattern on globally unique name + +Example queries via the gateway: +``` +LIST *.* # Returns all 179 items in test snapshot +GET *.* # Gets specific item by GUN +SEARCH *.* # Finds items matching pattern +``` + +## Implementation Details + +### Architecture + +``` +sources/snapshot/ +├── main.go # Entrypoint +├── cmd/ +│ └── root.go # Cobra CLI setup, viper config +└── adapters/ + ├── loader.go # Snapshot loading (file/URL) + ├── index.go # In-memory indexing + ├── adapter.go # Discovery adapter implementation + └── main.go # Adapter initialization +``` + +### Snapshot Index + +The source builds in-memory indices for efficient querying: + +- **By GUN**: Map of `GloballyUniqueName` → `*Item` for fast GET lookups +- **By type/scope**: Nested map for filtering by type and scope +- **All items**: Full list for wildcard LIST queries + +### Adapter Strategy + +The snapshot source uses **Option B from the design doc**: a single adapter with wildcard type (`*`) and wildcard scope (`*`). This adapter: + +- Reports `Type() = "*"` and `Scopes() = ["*"]` +- Implements `WildcardScopeAdapter` interface +- Handles all query types (GET, LIST, SEARCH) across all types and scopes in the snapshot + +This differs from "one adapter per (type, scope)" because the gateway's query expansion expects adapters to report specific types. The wildcard approach lets us serve any item from the snapshot regardless of type or scope. + +## Testing + +Run unit tests: +```bash +cd sources/snapshot/adapters +go test -v +``` + +Test snapshot loading: +```bash +cd sources/snapshot +go run main.go --snapshot-source=/path/to/snapshot.pb --help +``` + +Verify with real snapshot: +```bash +cd sources/snapshot +go test -run TestLoadSnapshotFromFile -v ./adapters +``` + +## Example: Using with v6 Investigations + +1. Download a snapshot from Area 51 or use an existing test snapshot +2. Start backend services locally (gateway, api-server, NATS) +3. Start the snapshot source pointing at your snapshot file +4. Run a v6 investigation - it will query from the snapshot instead of live sources +5. Re-run with the same snapshot for consistent, deterministic results + +## Troubleshooting + +**Error: "snapshot has no items"** +- Verify the snapshot file is valid protobuf and contains items +- Check file path or URL is correct + +**Error: "api-key must be set"** +- Set `ALLOW_UNAUTHENTICATED=true` for local testing +- Or provide a valid API key via `API_KEY` env var + +**Error: "could not connect to NATS"** +- Verify NATS is running at the configured host/port +- Check `NATS_SERVICE_HOST` and `NATS_SERVICE_PORT` are correct + +## Related Documentation + +- **Linear issue**: [ENG-2577](https://linear.app/overmind/issue/ENG-2577) +- **Snapshot protobuf**: `sdp/snapshots.proto` +- **Discovery engine**: `go/discovery/` +- **Test snapshot**: `services/api-server/service/changeanalysis/testdata/snapshot.pb` diff --git a/sources/snapshot/adapters/adapter.go b/sources/snapshot/adapters/adapter.go new file mode 100644 index 00000000..06353a98 --- /dev/null +++ b/sources/snapshot/adapters/adapter.go @@ -0,0 +1,149 @@ +package adapters + +import ( + "context" + "fmt" + "regexp" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// SnapshotAdapter is a discovery adapter that serves items of a single type +// from a snapshot. One adapter is created per type found in the snapshot so +// that the discovery engine can route specific-type GET/SEARCH queries +// correctly. +type SnapshotAdapter struct { + index *SnapshotIndex + itemType string + scopes []string + metadata *sdp.AdapterMetadata +} + +// NewSnapshotAdapter creates a new per-type adapter backed by the shared index. +func NewSnapshotAdapter(index *SnapshotIndex, itemType string, scopes []string) *SnapshotAdapter { + return &SnapshotAdapter{ + index: index, + itemType: itemType, + scopes: scopes, + metadata: lookupAdapterMetadata(itemType, scopes), + } +} + +func cloneItems(items []*sdp.Item) []*sdp.Item { + out := make([]*sdp.Item, len(items)) + for i, item := range items { + out[i] = proto.Clone(item).(*sdp.Item) + } + return out +} + +func (a *SnapshotAdapter) Type() string { + return a.itemType +} + +func (a *SnapshotAdapter) Name() string { + return fmt.Sprintf("snapshot-%s", a.itemType) +} + +func (a *SnapshotAdapter) Scopes() []string { + return a.scopes +} + +func (a *SnapshotAdapter) Get(ctx context.Context, scope string, query string, ignoreCache bool) (*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + "query": query, + }).Debug("SnapshotAdapter.Get called") + + // Try GUN lookup first (includes type in the GUN so it's already scoped) + item := a.index.GetByGUN(query) + if item != nil && item.GetType() == a.itemType { + if scope == "*" || item.GetScope() == scope { + return cloneItems([]*sdp.Item{item})[0], nil + } + } + + // Fall back to unique attribute value match within this type + for _, candidateItem := range a.index.GetItemsByTypeAndScope(a.itemType, scope) { + if candidateItem.UniqueAttributeValue() == query { + return cloneItems([]*sdp.Item{candidateItem})[0], nil + } + } + + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("item not found: scope=%s, type=%s, query=%s", scope, a.itemType, query), + Scope: scope, + } +} + +func (a *SnapshotAdapter) List(ctx context.Context, scope string, ignoreCache bool) ([]*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + }).Debug("SnapshotAdapter.List called") + + return cloneItems(a.index.GetItemsByTypeAndScope(a.itemType, scope)), nil +} + +// Search searches for items of this type by regex on GUN and includes 1-hop +// neighbors that also match this type and scope. +func (a *SnapshotAdapter) Search(ctx context.Context, scope string, query string, ignoreCache bool) ([]*sdp.Item, error) { + log.WithFields(log.Fields{ + "scope": scope, + "type": a.itemType, + "query": query, + }).Debug("SnapshotAdapter.Search called") + + regex, err := regexp.Compile(query) + if err != nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf("invalid regex pattern: %v", err), + Scope: scope, + } + } + + candidates := a.index.GetItemsByTypeAndScope(a.itemType, scope) + + var primaryMatches []*sdp.Item + for _, item := range candidates { + if regex.MatchString(item.GloballyUniqueName()) { + primaryMatches = append(primaryMatches, item) + } + } + + seen := make(map[string]bool, len(primaryMatches)) + for _, item := range primaryMatches { + seen[item.GloballyUniqueName()] = true + } + + var neighborMatches []*sdp.Item + for _, item := range primaryMatches { + for _, neighbor := range a.index.NeighborItems(item) { + if neighbor.GetType() != a.itemType { + continue + } + if scope != "*" && neighbor.GetScope() != scope { + continue + } + gun := neighbor.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + neighborMatches = append(neighborMatches, neighbor) + } + } + } + + result := make([]*sdp.Item, 0, len(primaryMatches)+len(neighborMatches)) + result = append(result, primaryMatches...) + result = append(result, neighborMatches...) + return cloneItems(result), nil +} + +func (a *SnapshotAdapter) Metadata() *sdp.AdapterMetadata { + return a.metadata +} diff --git a/sources/snapshot/adapters/adapter_test.go b/sources/snapshot/adapters/adapter_test.go new file mode 100644 index 00000000..7952ca2b --- /dev/null +++ b/sources/snapshot/adapters/adapter_test.go @@ -0,0 +1,297 @@ +package adapters + +import ( + "context" + "errors" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func createTestAdapters(t *testing.T) map[string]*SnapshotAdapter { + t.Helper() + snapshot := createTestSnapshot() + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("Failed to create test index: %v", err) + } + + adapters := make(map[string]*SnapshotAdapter) + for _, typ := range index.GetAllTypes() { + scopes := index.GetScopesForType(typ) + adapters[typ] = NewSnapshotAdapter(index, typ, scopes) + } + return adapters +} + +func TestAdapterType(t *testing.T) { + adapters := createTestAdapters(t) + + ec2 := adapters["ec2-instance"] + if ec2.Type() != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", ec2.Type()) + } + + s3 := adapters["s3-bucket"] + if s3.Type() != "s3-bucket" { + t.Errorf("Expected type 's3-bucket', got '%s'", s3.Type()) + } +} + +func TestAdapterName(t *testing.T) { + adapters := createTestAdapters(t) + + if adapters["ec2-instance"].Name() != "snapshot-ec2-instance" { + t.Errorf("Expected name 'snapshot-ec2-instance', got '%s'", adapters["ec2-instance"].Name()) + } +} + +func TestAdapterScopes(t *testing.T) { + adapters := createTestAdapters(t) + + ec2Scopes := adapters["ec2-instance"].Scopes() + if len(ec2Scopes) != 2 { + t.Fatalf("Expected 2 scopes for ec2-instance, got %d: %v", len(ec2Scopes), ec2Scopes) + } + scopeSet := map[string]bool{} + for _, s := range ec2Scopes { + scopeSet[s] = true + } + if !scopeSet["us-east-1"] || !scopeSet["us-west-2"] { + t.Errorf("Expected scopes [us-east-1, us-west-2], got %v", ec2Scopes) + } + + s3Scopes := adapters["s3-bucket"].Scopes() + if len(s3Scopes) != 1 || s3Scopes[0] != "global" { + t.Errorf("Expected scopes [global], got %v", s3Scopes) + } +} + +func TestAdapterGet(t *testing.T) { + adapters := createTestAdapters(t) + ec2 := adapters["ec2-instance"] + ctx := context.Background() + + // Get by unique attribute value with wildcard scope + item, err := ec2.Get(ctx, "*", "i-12345", false) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if item == nil || item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected 'i-12345', got '%v'", item) + } + + // Get by GUN + item, err = ec2.Get(ctx, "*", "us-east-1.ec2-instance.i-12345", false) + if err != nil { + t.Fatalf("Get by GUN failed: %v", err) + } + if item == nil { + t.Fatal("Expected item by GUN, got nil") + } + + // Get with specific scope + item, err = ec2.Get(ctx, "us-east-1", "i-12345", false) + if err != nil { + t.Fatalf("Get with specific scope failed: %v", err) + } + if item == nil { + t.Fatal("Expected item, got nil") + } + + // Not found + _, err = ec2.Get(ctx, "*", "nonexistent", false) + if err == nil { + t.Error("Expected error for non-existent item") + } + var queryErr *sdp.QueryError + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("Expected NOTFOUND, got %v", err) + } + + // Scope mismatch: requesting us-west-2 for an item in us-east-1 + _, err = ec2.Get(ctx, "us-west-2", "us-east-1.ec2-instance.i-12345", false) + if err == nil { + t.Fatal("Expected error when scope doesn't match GUN scope") + } + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("Expected NOTFOUND, got %v", err) + } + + // Same GUN with matching scope works + item, err = ec2.Get(ctx, "us-east-1", "us-east-1.ec2-instance.i-12345", false) + if err != nil || item == nil || item.GetScope() != "us-east-1" { + t.Errorf("Get with matching scope should work: err=%v item=%v", err, item) + } + + // Cross-type: ec2 adapter should not return s3-bucket items + _, err = ec2.Get(ctx, "*", "my-test-bucket", false) + if err == nil { + t.Error("ec2 adapter should not find s3-bucket items") + } +} + +func TestAdapterList(t *testing.T) { + adapters := createTestAdapters(t) + ctx := context.Background() + + // ec2 adapter lists its 2 items + items, err := adapters["ec2-instance"].List(ctx, "*", false) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if len(items) != 2 { + t.Errorf("Expected 2 ec2-instance items, got %d", len(items)) + } + + // Verify linked items are preserved + var ec2East *sdp.Item + for _, item := range items { + if item.GloballyUniqueName() == "us-east-1.ec2-instance.i-12345" { + ec2East = item + break + } + } + if ec2East == nil { + t.Fatal("Expected to find ec2 instance i-12345") + } + linked := ec2East.GetLinkedItems() + if len(linked) != 1 { + t.Fatalf("Expected 1 linked item, got %d", len(linked)) + } + ref := linked[0].GetItem() + if ref.GetType() != "s3-bucket" || ref.GetUniqueAttributeValue() != "my-test-bucket" { + t.Errorf("Unexpected linked item reference: %v", ref) + } + + // List with specific scope + items, err = adapters["ec2-instance"].List(ctx, "us-east-1", false) + if err != nil { + t.Fatalf("List with specific scope failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item for us-east-1, got %d", len(items)) + } + + // s3 adapter lists its 1 item + items, err = adapters["s3-bucket"].List(ctx, "*", false) + if err != nil { + t.Fatalf("s3 List failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 s3-bucket item, got %d", len(items)) + } + + // List with nonexistent scope + items, err = adapters["ec2-instance"].List(ctx, "nonexistent", false) + if err != nil { + t.Fatalf("List with nonexistent scope failed: %v", err) + } + if len(items) != 0 { + t.Errorf("Expected 0 items, got %d", len(items)) + } +} + +func TestAdapterSearch(t *testing.T) { + adapters := createTestAdapters(t) + ec2 := adapters["ec2-instance"] + ctx := context.Background() + + // Search matching both ec2 instances (neighbor s3-bucket is different type, not included) + items, err := ec2.Search(ctx, "*", ".*ec2-instance.*", false) + if err != nil { + t.Fatalf("Search failed: %v", err) + } + if len(items) != 2 { + t.Errorf("Expected 2 ec2-instance items, got %d", len(items)) + } + + // Search with specific scope + items, err = ec2.Search(ctx, "us-east-1", ".*ec2-instance.*", false) + if err != nil { + t.Fatalf("Search with specific scope failed: %v", err) + } + if len(items) != 1 { + t.Errorf("Expected 1 item in us-east-1, got %d", len(items)) + } + + // Search that matches nothing + items, err = ec2.Search(ctx, "*", "nonexistent-xyz", false) + if err != nil { + t.Fatalf("Search no match failed: %v", err) + } + if len(items) != 0 { + t.Errorf("Expected 0 items, got %d", len(items)) + } + + // Invalid regex + _, err = ec2.Search(ctx, "*", "[invalid(regex", false) + if err == nil { + t.Error("Expected error for invalid regex") + } + var queryErr *sdp.QueryError + if !errors.As(err, &queryErr) || queryErr.GetErrorType() != sdp.QueryError_OTHER { + t.Errorf("Expected OTHER error, got %v", err) + } +} + +func TestAdapterMetadata(t *testing.T) { + adapters := createTestAdapters(t) + + // ec2-instance should get metadata from the catalog + ec2Meta := adapters["ec2-instance"].Metadata() + if ec2Meta == nil { + t.Fatal("Expected metadata, got nil") + } + if ec2Meta.GetType() != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", ec2Meta.GetType()) + } + if ec2Meta.GetCategory() != sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION { + t.Errorf("Expected COMPUTE_APPLICATION category, got %v", ec2Meta.GetCategory()) + } + if ec2Meta.GetDescriptiveName() != "EC2 Instance" { + t.Errorf("Expected descriptive name 'EC2 Instance', got '%s'", ec2Meta.GetDescriptiveName()) + } + + methods := ec2Meta.GetSupportedQueryMethods() + if !methods.GetGet() || !methods.GetList() || !methods.GetSearch() { + t.Error("Expected all query methods to be supported for ec2-instance") + } + + // s3-bucket should also get catalog metadata + s3Meta := adapters["s3-bucket"].Metadata() + if s3Meta.GetType() != "s3-bucket" { + t.Errorf("Expected type 's3-bucket', got '%s'", s3Meta.GetType()) + } +} + +func TestNewSnapshotAdapter(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + adapter := NewSnapshotAdapter(index, "ec2-instance", []string{"us-east-1", "us-west-2"}) + if adapter == nil { + t.Fatal("Expected adapter, got nil") + } + if adapter.index != index { + t.Error("Expected adapter to store index reference") + } + if adapter.itemType != "ec2-instance" { + t.Errorf("Expected type 'ec2-instance', got '%s'", adapter.itemType) + } +} + +func TestAdapterMetadataFallback(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Use a type not in the catalog to test fallback + adapter := NewSnapshotAdapter(index, "unknown-type-xyz", []string{"test"}) + meta := adapter.Metadata() + if meta.GetType() != "unknown-type-xyz" { + t.Errorf("Expected type 'unknown-type-xyz', got '%s'", meta.GetType()) + } + if meta.GetCategory() != sdp.AdapterCategory_ADAPTER_CATEGORY_OTHER { + t.Errorf("Expected OTHER category for unknown type, got %v", meta.GetCategory()) + } +} diff --git a/sources/snapshot/adapters/catalog.go b/sources/snapshot/adapters/catalog.go new file mode 100644 index 00000000..e4e79f1d --- /dev/null +++ b/sources/snapshot/adapters/catalog.go @@ -0,0 +1,96 @@ +package adapters + +import ( + "encoding/json" + "io/fs" + + adapterdata "github.com/overmindtech/cli/docs.overmind.tech/docs/sources" + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" +) + +type catalogQueryMethods struct { + Get bool `json:"get"` + GetDescription string `json:"getDescription"` + List bool `json:"list"` + ListDescription string `json:"listDescription"` + Search bool `json:"search"` + SearchDescription string `json:"searchDescription"` +} + +type catalogEntry struct { + Type string `json:"type"` + Category int32 `json:"category"` + DescriptiveName string `json:"descriptiveName"` + PotentialLinks []string `json:"potentialLinks"` + SupportedQueryMethods catalogQueryMethods `json:"supportedQueryMethods"` +} + +var adapterCatalog map[string]*catalogEntry + +func init() { + adapterCatalog = make(map[string]*catalogEntry) + + err := fs.WalkDir(adapterdata.Files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + + data, readErr := adapterdata.Files.ReadFile(path) + if readErr != nil { + log.WithError(readErr).WithField("path", path).Warn("Failed to read adapter data file") + return nil + } + + var entry catalogEntry + if jsonErr := json.Unmarshal(data, &entry); jsonErr != nil { + log.WithError(jsonErr).WithField("path", path).Warn("Failed to parse adapter data file") + return nil + } + + if entry.Type != "" { + adapterCatalog[entry.Type] = &entry + } + return nil + }) + if err != nil { + log.WithError(err).Error("Failed to walk embedded adapter data") + } +} + +// lookupAdapterMetadata returns AdapterMetadata for the given type by looking +// up the embedded catalog. Falls back to sensible defaults when the type is not +// in the catalog. +func lookupAdapterMetadata(itemType string, scopes []string) *sdp.AdapterMetadata { + entry, ok := adapterCatalog[itemType] + if !ok { + return &sdp.AdapterMetadata{ + Type: itemType, + DescriptiveName: itemType, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + }, + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_OTHER, + } + } + + potentialLinks := make([]string, len(entry.PotentialLinks)) + copy(potentialLinks, entry.PotentialLinks) + + return &sdp.AdapterMetadata{ + Type: itemType, + DescriptiveName: entry.DescriptiveName, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: entry.SupportedQueryMethods.Get, + GetDescription: entry.SupportedQueryMethods.GetDescription, + List: entry.SupportedQueryMethods.List, + ListDescription: entry.SupportedQueryMethods.ListDescription, + Search: entry.SupportedQueryMethods.Search, + SearchDescription: entry.SupportedQueryMethods.SearchDescription, + }, + PotentialLinks: potentialLinks, + Category: sdp.AdapterCategory(entry.Category), + } +} diff --git a/sources/snapshot/adapters/index.go b/sources/snapshot/adapters/index.go new file mode 100644 index 00000000..e05ec840 --- /dev/null +++ b/sources/snapshot/adapters/index.go @@ -0,0 +1,242 @@ +package adapters + +import ( + "fmt" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" +) + +// SnapshotIndex maintains in-memory indices for efficient snapshot querying +type SnapshotIndex struct { + // All items in the snapshot + allItems []*sdp.Item + + // Index by GloballyUniqueName for fast GET lookups + byGUN map[string]*sdp.Item + + // Index by type and scope for filtering + byTypeScope map[string]map[string][]*sdp.Item + + // Edges from the snapshot (for future use) + edges []*sdp.Edge +} + +// NewSnapshotIndex builds indices from a snapshot +func NewSnapshotIndex(snapshot *sdp.Snapshot) (*SnapshotIndex, error) { + if snapshot == nil || snapshot.GetProperties() == nil { + return nil, fmt.Errorf("snapshot or properties is nil") + } + + items := snapshot.GetProperties().GetItems() + edges := snapshot.GetProperties().GetEdges() + + index := &SnapshotIndex{ + allItems: items, + byGUN: make(map[string]*sdp.Item), + byTypeScope: make(map[string]map[string][]*sdp.Item), + edges: edges, + } + + // Build indices + for _, item := range items { + gun := item.GloballyUniqueName() + index.byGUN[gun] = item + + itemType := item.GetType() + scope := item.GetScope() + + if index.byTypeScope[itemType] == nil { + index.byTypeScope[itemType] = make(map[string][]*sdp.Item) + } + index.byTypeScope[itemType][scope] = append(index.byTypeScope[itemType][scope], item) + } + + // Hydrate each item's LinkedItems from the snapshot edges so that + // callers (explore view, etc.) see the graph relationships directly on + // the returned items instead of having to cross-reference the separate + // edge list. + index.hydrateLinkedItems() + + log.WithFields(log.Fields{ + "total_items": len(items), + "total_edges": len(edges), + "types": len(index.byTypeScope), + }).Info("Snapshot index built") + + return index, nil +} + +// hydrateLinkedItems populates each item's LinkedItems field from the snapshot +// edges. For each edge, the item matching edge.From gets a LinkedItem pointing +// to edge.To (with blast propagation). Edges whose From item is not in the +// snapshot are skipped. +func (idx *SnapshotIndex) hydrateLinkedItems() { + // Build a map from item reference key → existing LinkedItem targets so + // we don't add duplicates when the item already carries some LinkedItems. + type refKey struct { + scope, typ, uav string + } + existingLinks := make(map[refKey]map[refKey]bool) + + for _, item := range idx.allItems { + key := refKey{item.GetScope(), item.GetType(), item.UniqueAttributeValue()} + set := make(map[refKey]bool) + for _, li := range item.GetLinkedItems() { + r := li.GetItem() + if r != nil { + set[refKey{r.GetScope(), r.GetType(), r.GetUniqueAttributeValue()}] = true + } + } + existingLinks[key] = set + } + + for _, edge := range idx.edges { + from := edge.GetFrom() + to := edge.GetTo() + if from == nil || to == nil { + continue + } + + item := idx.GetByReference(from) + if item == nil { + continue + } + + fromKey := refKey{item.GetScope(), item.GetType(), item.UniqueAttributeValue()} + toKey := refKey{to.GetScope(), to.GetType(), to.GetUniqueAttributeValue()} + + if existingLinks[fromKey][toKey] { + continue + } + + item.LinkedItems = append(item.LinkedItems, &sdp.LinkedItem{ + Item: to, + BlastPropagation: edge.GetBlastPropagation(), + }) + existingLinks[fromKey][toKey] = true + } +} + +// GetAllItems returns all items in the snapshot +func (idx *SnapshotIndex) GetAllItems() []*sdp.Item { + return idx.allItems +} + +// GetByGUN retrieves an item by its GloballyUniqueName +func (idx *SnapshotIndex) GetByGUN(gun string) *sdp.Item { + return idx.byGUN[gun] +} + +// GetByReference retrieves an item by its Reference using the GUN index. +func (idx *SnapshotIndex) GetByReference(ref *sdp.Reference) *sdp.Item { + if ref == nil { + return nil + } + return idx.byGUN[ref.GloballyUniqueName()] +} + +// GetAllTypes returns all unique types in the snapshot +func (idx *SnapshotIndex) GetAllTypes() []string { + types := make([]string, 0, len(idx.byTypeScope)) + for itemType := range idx.byTypeScope { + types = append(types, itemType) + } + return types +} + +// GetScopesForType returns all unique scopes that contain items of the given type. +func (idx *SnapshotIndex) GetScopesForType(itemType string) []string { + scopeMap, ok := idx.byTypeScope[itemType] + if !ok { + return nil + } + scopes := make([]string, 0, len(scopeMap)) + for s := range scopeMap { + scopes = append(scopes, s) + } + return scopes +} + +// GetItemsByTypeAndScope returns items matching the given type and scope. +// A wildcard ("*") scope returns all items of that type. +func (idx *SnapshotIndex) GetItemsByTypeAndScope(itemType, scope string) []*sdp.Item { + scopeMap, ok := idx.byTypeScope[itemType] + if !ok { + return nil + } + if scope == "*" { + var all []*sdp.Item + for _, items := range scopeMap { + all = append(all, items...) + } + return all + } + return scopeMap[scope] +} + +// EdgesFrom returns all edges whose From reference equals ref. +func (idx *SnapshotIndex) EdgesFrom(ref *sdp.Reference) []*sdp.Edge { + if ref == nil { + return nil + } + var out []*sdp.Edge + for _, e := range idx.edges { + if e.GetFrom() != nil && e.GetFrom().IsEqual(ref) { + out = append(out, e) + } + } + return out +} + +// EdgesTo returns all edges whose To reference equals ref. +func (idx *SnapshotIndex) EdgesTo(ref *sdp.Reference) []*sdp.Edge { + if ref == nil { + return nil + } + var out []*sdp.Edge + for _, e := range idx.edges { + if e.GetTo() != nil && e.GetTo().IsEqual(ref) { + out = append(out, e) + } + } + return out +} + +// NeighborItems returns items that are connected to the given item by any edge +// (as From or To). Each item is returned at most once. Items not present in +// the snapshot are skipped. +func (idx *SnapshotIndex) NeighborItems(item *sdp.Item) []*sdp.Item { + if item == nil { + return nil + } + ref := item.Reference() + seen := make(map[string]bool) + var out []*sdp.Item + for _, e := range idx.EdgesFrom(ref) { + if e.GetTo() != nil { + other := idx.GetByReference(e.GetTo()) + if other != nil { + gun := other.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + out = append(out, other) + } + } + } + } + for _, e := range idx.EdgesTo(ref) { + if e.GetFrom() != nil { + other := idx.GetByReference(e.GetFrom()) + if other != nil { + gun := other.GloballyUniqueName() + if !seen[gun] { + seen[gun] = true + out = append(out, other) + } + } + } + } + return out +} + diff --git a/sources/snapshot/adapters/index_test.go b/sources/snapshot/adapters/index_test.go new file mode 100644 index 00000000..0cb4a6ce --- /dev/null +++ b/sources/snapshot/adapters/index_test.go @@ -0,0 +1,288 @@ +package adapters + +import ( + "testing" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func createTestSnapshot() *sdp.Snapshot { + attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "instanceId": "i-12345", + "name": "test-instance", + }) + attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "instanceId": "i-67890", + "name": "test-instance-2", + }) + attrs3, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "bucketName": "my-test-bucket", + }) + + return &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot", + Items: []*sdp.Item{ + { + Type: "ec2-instance", + UniqueAttribute: "instanceId", + Attributes: attrs1, + Scope: "us-east-1", + }, + { + Type: "ec2-instance", + UniqueAttribute: "instanceId", + Attributes: attrs2, + Scope: "us-west-2", + }, + { + Type: "s3-bucket", + UniqueAttribute: "bucketName", + Attributes: attrs3, + Scope: "global", + }, + }, + Edges: []*sdp.Edge{ + { + From: &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + }, + To: &sdp.Reference{ + Type: "s3-bucket", + UniqueAttributeValue: "my-test-bucket", + Scope: "global", + }, + }, + }, + }, + } +} + +func TestNewSnapshotIndex(t *testing.T) { + snapshot := createTestSnapshot() + + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("NewSnapshotIndex failed: %v", err) + } + + if index == nil { + t.Fatal("Expected index to be non-nil") + } + + // Verify all items are indexed + allItems := index.GetAllItems() + if len(allItems) != 3 { + t.Errorf("Expected 3 items, got %d", len(allItems)) + } + + // Verify edges are stored + if len(index.edges) != 1 { + t.Errorf("Expected 1 edge, got %d", len(index.edges)) + } +} + +func TestLinkedItemsHydrated(t *testing.T) { + snapshot := createTestSnapshot() + index, err := NewSnapshotIndex(snapshot) + if err != nil { + t.Fatalf("NewSnapshotIndex failed: %v", err) + } + + // The ec2-instance i-12345 is the From side of the edge to s3-bucket + ec2 := index.GetByGUN("us-east-1.ec2-instance.i-12345") + if ec2 == nil { + t.Fatal("expected to find ec2 instance") + } + linked := ec2.GetLinkedItems() + if len(linked) != 1 { + t.Fatalf("Expected 1 linked item on ec2 instance, got %d", len(linked)) + } + ref := linked[0].GetItem() + if ref.GetType() != "s3-bucket" || ref.GetUniqueAttributeValue() != "my-test-bucket" || ref.GetScope() != "global" { + t.Errorf("Unexpected linked item reference: %v", ref) + } + + // The s3-bucket is only on the To side of the edge, so it should have no LinkedItems + bucket := index.GetByGUN("global.s3-bucket.my-test-bucket") + if bucket == nil { + t.Fatal("expected to find s3 bucket") + } + if len(bucket.GetLinkedItems()) != 0 { + t.Errorf("Expected 0 linked items on bucket (it is only a To target), got %d", len(bucket.GetLinkedItems())) + } + + // The us-west-2 instance has no edges at all + ec2West := index.GetByGUN("us-west-2.ec2-instance.i-67890") + if ec2West == nil { + t.Fatal("expected to find us-west-2 ec2 instance") + } + if len(ec2West.GetLinkedItems()) != 0 { + t.Errorf("Expected 0 linked items on us-west-2 instance, got %d", len(ec2West.GetLinkedItems())) + } +} + +func TestGetByGUN(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Test getting item by GUN + gun := "us-east-1.ec2-instance.i-12345" + item := index.GetByGUN(gun) + if item == nil { + t.Fatalf("Expected to find item with GUN %s", gun) + } + + if item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected unique attribute 'i-12345', got '%s'", item.UniqueAttributeValue()) + } + + // Test non-existent GUN + item = index.GetByGUN("nonexistent.type.query") + if item != nil { + t.Error("Expected nil for non-existent GUN") + } +} + +func TestGetByReference(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + // Test getting item by reference + ref := &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + } + + item := index.GetByReference(ref) + if item == nil { + t.Fatal("Expected to find item by reference") + } + + if item.UniqueAttributeValue() != "i-12345" { + t.Errorf("Expected unique attribute 'i-12345', got '%s'", item.UniqueAttributeValue()) + } +} + +func TestGetAllTypes(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + types := index.GetAllTypes() + if len(types) != 2 { + t.Errorf("Expected 2 unique types, got %d", len(types)) + } + + // Verify expected types exist + typeMap := make(map[string]bool) + for _, itemType := range types { + typeMap[itemType] = true + } + + expectedTypes := []string{"ec2-instance", "s3-bucket"} + for _, expected := range expectedTypes { + if !typeMap[expected] { + t.Errorf("Expected type '%s' not found", expected) + } + } +} + +func TestEdgesFromAndEdgesTo(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + refFrom := &sdp.Reference{ + Type: "ec2-instance", + UniqueAttributeValue: "i-12345", + Scope: "us-east-1", + } + refTo := &sdp.Reference{ + Type: "s3-bucket", + UniqueAttributeValue: "my-test-bucket", + Scope: "global", + } + + fromEdges := index.EdgesFrom(refFrom) + if len(fromEdges) != 1 { + t.Errorf("Expected 1 edge from ec2-instance i-12345, got %d", len(fromEdges)) + } + if len(fromEdges) > 0 && !fromEdges[0].GetTo().IsEqual(refTo) { + t.Error("EdgesFrom: expected To reference to be s3-bucket my-test-bucket") + } + + toEdges := index.EdgesTo(refTo) + if len(toEdges) != 1 { + t.Errorf("Expected 1 edge to s3-bucket my-test-bucket, got %d", len(toEdges)) + } + if len(toEdges) > 0 && !toEdges[0].GetFrom().IsEqual(refFrom) { + t.Error("EdgesTo: expected From reference to be ec2-instance i-12345") + } + + // No edges from the bucket (it only appears as To) + fromBucket := index.EdgesFrom(refTo) + if len(fromBucket) != 0 { + t.Errorf("Expected 0 edges from bucket, got %d", len(fromBucket)) + } + // No edges to the us-east-1 instance (it only appears as From in this snapshot) + toInstance := index.EdgesTo(refFrom) + if len(toInstance) != 0 { + t.Errorf("Expected 0 edges to us-east-1 instance, got %d", len(toInstance)) + } +} + +func TestNeighborItems(t *testing.T) { + snapshot := createTestSnapshot() + index, _ := NewSnapshotIndex(snapshot) + + ec2East := index.GetByGUN("us-east-1.ec2-instance.i-12345") + if ec2East == nil { + t.Fatal("expected to find us-east-1 ec2 instance") + } + neighbors := index.NeighborItems(ec2East) + if len(neighbors) != 1 { + t.Fatalf("Expected 1 neighbor of us-east-1 ec2 instance, got %d", len(neighbors)) + } + if neighbors[0].GloballyUniqueName() != "global.s3-bucket.my-test-bucket" { + t.Errorf("Expected neighbor to be s3-bucket, got %s", neighbors[0].GloballyUniqueName()) + } + + bucket := index.GetByGUN("global.s3-bucket.my-test-bucket") + if bucket == nil { + t.Fatal("expected to find s3 bucket") + } + neighbors = index.NeighborItems(bucket) + if len(neighbors) != 1 { + t.Fatalf("Expected 1 neighbor of s3 bucket, got %d", len(neighbors)) + } + if neighbors[0].GloballyUniqueName() != "us-east-1.ec2-instance.i-12345" { + t.Errorf("Expected neighbor to be ec2-instance i-12345, got %s", neighbors[0].GloballyUniqueName()) + } + + // us-west-2 instance has no edges + ec2West := index.GetByGUN("us-west-2.ec2-instance.i-67890") + if ec2West == nil { + t.Fatal("expected to find us-west-2 ec2 instance") + } + neighbors = index.NeighborItems(ec2West) + if len(neighbors) != 0 { + t.Errorf("Expected 0 neighbors for us-west-2 instance, got %d", len(neighbors)) + } +} + +func TestNewSnapshotIndexNilSnapshot(t *testing.T) { + _, err := NewSnapshotIndex(nil) + if err == nil { + t.Error("Expected error for nil snapshot, got nil") + } +} + +func TestNewSnapshotIndexNilProperties(t *testing.T) { + snapshot := &sdp.Snapshot{} + _, err := NewSnapshotIndex(snapshot) + if err == nil { + t.Error("Expected error for nil properties, got nil") + } +} diff --git a/sources/snapshot/adapters/loader.go b/sources/snapshot/adapters/loader.go new file mode 100644 index 00000000..d0dcb96a --- /dev/null +++ b/sources/snapshot/adapters/loader.go @@ -0,0 +1,89 @@ +package adapters + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/overmindtech/cli/go/sdp-go" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" +) + +// LoadSnapshot loads a snapshot from a URL or local file path +func LoadSnapshot(ctx context.Context, source string) (*sdp.Snapshot, error) { + var data []byte + var err error + + // Determine if source is a URL or file path + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + log.WithField("url", source).Info("Loading snapshot from URL") + data, err = loadSnapshotFromURL(ctx, source) + if err != nil { + return nil, fmt.Errorf("failed to load snapshot from URL: %w", err) + } + } else { + log.WithField("path", source).Info("Loading snapshot from file") + data, err = loadSnapshotFromFile(source) + if err != nil { + return nil, fmt.Errorf("failed to load snapshot from file: %w", err) + } + } + + // Unmarshal the protobuf data + snapshot := &sdp.Snapshot{} + if err := proto.Unmarshal(data, snapshot); err != nil { + return nil, fmt.Errorf("failed to unmarshal snapshot protobuf: %w", err) + } + + // Validate snapshot has items + if snapshot.GetProperties() == nil || len(snapshot.GetProperties().GetItems()) == 0 { + return nil, fmt.Errorf("snapshot has no items") + } + + log.WithFields(log.Fields{ + "items": len(snapshot.GetProperties().GetItems()), + "edges": len(snapshot.GetProperties().GetEdges()), + }).Info("Snapshot loaded successfully") + + return snapshot, nil +} + +// loadSnapshotFromURL loads snapshot data from an HTTP(S) URL +func loadSnapshotFromURL(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request returned status %d", resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + return data, nil +} + +// loadSnapshotFromFile loads snapshot data from a local file +func loadSnapshotFromFile(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + return data, nil +} diff --git a/sources/snapshot/adapters/loader_test.go b/sources/snapshot/adapters/loader_test.go new file mode 100644 index 00000000..7ae5d63b --- /dev/null +++ b/sources/snapshot/adapters/loader_test.go @@ -0,0 +1,161 @@ +package adapters + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/overmindtech/cli/go/sdp-go" + "google.golang.org/protobuf/proto" +) + +func TestLoadSnapshotFromFile(t *testing.T) { + // Create a test snapshot + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "test-item", + }) + + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot", + Items: []*sdp.Item{ + { + Type: "test-type", + UniqueAttribute: "name", + Attributes: attrs, + Scope: "test-scope", + }, + }, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Write to temp file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test-snapshot.pb") + if err := os.WriteFile(tmpFile, data, 0o644); err != nil { + t.Fatalf("Failed to write test snapshot file: %v", err) + } + + // Test loading + ctx := context.Background() + loaded, err := LoadSnapshot(ctx, tmpFile) + if err != nil { + t.Fatalf("LoadSnapshot failed: %v", err) + } + + if loaded.GetProperties().GetName() != "test-snapshot" { + t.Errorf("Expected snapshot name 'test-snapshot', got '%s'", loaded.GetProperties().GetName()) + } + + if len(loaded.GetProperties().GetItems()) != 1 { + t.Errorf("Expected 1 item, got %d", len(loaded.GetProperties().GetItems())) + } +} + +func TestLoadSnapshotFromURL(t *testing.T) { + // Create a test snapshot + attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + "name": "test-item", + }) + + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "test-snapshot-url", + Items: []*sdp.Item{ + { + Type: "test-type", + UniqueAttribute: "name", + Attributes: attrs, + Scope: "test-scope", + }, + }, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Create test HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) + })) + defer server.Close() + + // Test loading from URL + ctx := context.Background() + loaded, err := LoadSnapshot(ctx, server.URL) + if err != nil { + t.Fatalf("LoadSnapshot from URL failed: %v", err) + } + + if loaded.GetProperties().GetName() != "test-snapshot-url" { + t.Errorf("Expected snapshot name 'test-snapshot-url', got '%s'", loaded.GetProperties().GetName()) + } +} + +func TestLoadSnapshotEmptyItems(t *testing.T) { + // Create a snapshot with no items + snapshot := &sdp.Snapshot{ + Properties: &sdp.SnapshotProperties{ + Name: "empty-snapshot", + Items: []*sdp.Item{}, + }, + } + + // Marshal to bytes + data, err := proto.Marshal(snapshot) + if err != nil { + t.Fatalf("Failed to marshal test snapshot: %v", err) + } + + // Write to temp file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "empty-snapshot.pb") + if err := os.WriteFile(tmpFile, data, 0o644); err != nil { + t.Fatalf("Failed to write test snapshot file: %v", err) + } + + // Test loading - should fail validation + ctx := context.Background() + _, err = LoadSnapshot(ctx, tmpFile) + if err == nil { + t.Error("Expected error for snapshot with no items, got nil") + } +} + +func TestLoadSnapshotFileNotFound(t *testing.T) { + ctx := context.Background() + _, err := LoadSnapshot(ctx, "/nonexistent/file.pb") + if err == nil { + t.Error("Expected error for nonexistent file, got nil") + } +} + +func TestLoadSnapshotInvalidProtobuf(t *testing.T) { + // Write invalid protobuf data + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "invalid.pb") + if err := os.WriteFile(tmpFile, []byte("invalid protobuf data"), 0o644); err != nil { + t.Fatalf("Failed to write invalid data: %v", err) + } + + // Test loading - should fail + ctx := context.Background() + _, err := LoadSnapshot(ctx, tmpFile) + if err == nil { + t.Error("Expected error for invalid protobuf, got nil") + } +} diff --git a/sources/snapshot/adapters/main.go b/sources/snapshot/adapters/main.go new file mode 100644 index 00000000..91f570fc --- /dev/null +++ b/sources/snapshot/adapters/main.go @@ -0,0 +1,45 @@ +package adapters + +import ( + "context" + "fmt" + + "github.com/overmindtech/cli/go/discovery" + log "github.com/sirupsen/logrus" +) + +// InitializeAdapters loads a snapshot and registers one adapter per type found +// in the snapshot data. Each adapter carries the correct category and metadata +// from the embedded adapter catalog so that the discovery engine can route +// specific-type GET/SEARCH queries to it. +func InitializeAdapters(ctx context.Context, e *discovery.Engine, snapshotSource string) error { + snapshot, err := LoadSnapshot(ctx, snapshotSource) + if err != nil { + return fmt.Errorf("failed to load snapshot: %w", err) + } + + index, err := NewSnapshotIndex(snapshot) + if err != nil { + return fmt.Errorf("failed to build snapshot index: %w", err) + } + + types := index.GetAllTypes() + adapters := make([]discovery.Adapter, 0, len(types)) + for _, typ := range types { + scopes := index.GetScopesForType(typ) + adapters = append(adapters, NewSnapshotAdapter(index, typ, scopes)) + } + + if err := e.AddAdapters(adapters...); err != nil { + return fmt.Errorf("failed to add snapshot adapters: %w", err) + } + + log.WithFields(log.Fields{ + "items": len(snapshot.GetProperties().GetItems()), + "edges": len(snapshot.GetProperties().GetEdges()), + "types": len(types), + "adapters": len(adapters), + }).Info("Snapshot adapters initialized successfully") + + return nil +} diff --git a/sources/snapshot/cmd/root.go b/sources/snapshot/cmd/root.go new file mode 100644 index 00000000..0882ba22 --- /dev/null +++ b/sources/snapshot/cmd/root.go @@ -0,0 +1,233 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/getsentry/sentry-go" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/logging" + "github.com/overmindtech/cli/go/tracing" + "github.com/overmindtech/cli/sources/snapshot/adapters" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "snapshot-source", + Short: "Discovery source that serves data from a snapshot file", + SilenceUsage: true, + Long: `Snapshot source loads a snapshot from a file or URL and responds to +discovery queries with items from that snapshot. This enables local testing +with fixed data and deterministic re-runs of v6 investigations.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + defer tracing.LogRecoverToReturn(ctx, "snapshot-source.root") + + // Get snapshot source (required) + snapshotSource := viper.GetString("snapshot-source") + if snapshotSource == "" { + return fmt.Errorf("snapshot-source is required (use --snapshot-source or SNAPSHOT_SOURCE env var)") + } + + log.WithField("snapshot-source", snapshotSource).Info("Starting snapshot source") + + // Get engine config + engineConfig, err := discovery.EngineConfigFromViper("snapshot", tracing.Version()) + if err != nil { + log.WithError(err).Error("Could not get engine config from viper") + return fmt.Errorf("could not get engine config from viper: %w", err) + } + + // Create a basic engine first + e, err := discovery.NewEngine(engineConfig) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Could not create engine") + return fmt.Errorf("could not create engine: %w", err) + } + + // Start HTTP server for health checks before initialization + healthCheckPort := viper.GetInt("health-check-port") + e.ServeHealthProbes(healthCheckPort) + + // Start the engine (NATS connection) before adapter init so heartbeats work + err = e.Start(ctx) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Could not start engine") + return fmt.Errorf("could not start engine: %w", err) + } + + // Snapshot adapters load from files/URLs which may fail, so we use + // the initialization pattern with error handling + err = adapters.InitializeAdapters(ctx, e, snapshotSource) + if err != nil { + initErr := fmt.Errorf("could not initialize snapshot adapters: %w", err) + log.WithError(initErr).Error("Snapshot source initialization failed - pod will stay running with error status") + e.SetInitError(initErr) + sentry.CaptureException(initErr) + } else { + e.StartSendingHeartbeats(ctx) + } + + <-ctx.Done() + + log.Info("Stopping engine") + + err = e.Stop() + if err != nil { + log.WithError(err).Error("Could not stop engine") + return fmt.Errorf("could not stop engine: %w", err) + } + + log.Info("Stopped") + + return nil + }, +} + +// Execute adds all child commands to the root command and sets flags +// appropriately. This is called by main.main(). It only needs to happen once to +// the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + var logLevel string + + // General config options + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "/etc/srcman/config/source.yaml", "config file path") + rootCmd.PersistentFlags().StringVar(&logLevel, "log", "info", "Set the log level. Valid values: panic, fatal, error, warn, info, debug, trace") + cobra.CheckErr(viper.BindEnv("log", "SNAPSHOT_LOG", "LOG")) // fallback to global config + + // Snapshot-specific config + rootCmd.PersistentFlags().String("snapshot-source", "", "Path to snapshot file or URL to load (required). Can be a local file path or http(s) URL.") + cobra.CheckErr(viper.BindEnv("snapshot-source", "SNAPSHOT_SOURCE", "SNAPSHOT_PATH", "SNAPSHOT_URL")) + + // engine config options + discovery.AddEngineFlags(rootCmd) + + rootCmd.PersistentFlags().IntP("health-check-port", "", 8089, "The port that the health check should run on") + cobra.CheckErr(viper.BindEnv("health-check-port", "SNAPSHOT_HEALTH_CHECK_PORT", "HEALTH_CHECK_PORT", "SNAPSHOT_SERVICE_PORT", "SERVICE_PORT")) // new names + backwards compat + + // tracing + rootCmd.PersistentFlags().String("honeycomb-api-key", "", "If specified, configures opentelemetry libraries to submit traces to honeycomb") + cobra.CheckErr(viper.BindEnv("honeycomb-api-key", "SNAPSHOT_HONEYCOMB_API_KEY", "HONEYCOMB_API_KEY")) // fallback to global config + rootCmd.PersistentFlags().String("sentry-dsn", "", "If specified, configures sentry libraries to capture errors") + cobra.CheckErr(viper.BindEnv("sentry-dsn", "SNAPSHOT_SENTRY_DSN", "SENTRY_DSN")) // fallback to global config + rootCmd.PersistentFlags().String("run-mode", "release", "Set the run mode for this service, 'release', 'debug' or 'test'. Defaults to 'release'.") + rootCmd.PersistentFlags().Bool("json-log", true, "Set to false to emit logs as text for easier reading in development.") + cobra.CheckErr(viper.BindEnv("json-log", "SNAPSHOT_SOURCE_JSON_LOG", "JSON_LOG")) // fallback to global config + + // Bind these to viper + cobra.CheckErr(viper.BindPFlags(rootCmd.PersistentFlags())) + + // Run this before we do anything to set up the loglevel + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if lvl, err := log.ParseLevel(logLevel); err == nil { + log.SetLevel(lvl) + } else { + log.SetLevel(log.InfoLevel) + log.WithFields(log.Fields{ + "error": err, + }).Error("Could not parse log level") + } + + log.AddHook(TerminationLogHook{}) + + // Bind flags that haven't been set to the values from viper of we have them + var bindErr error + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + // Bind the flag to viper only if it has a non-empty default + if f.DefValue != "" || f.Changed { + if err := viper.BindPFlag(f.Name, f); err != nil { + bindErr = err + } + } + }) + if bindErr != nil { + log.WithError(bindErr).Error("Could not bind flag to viper") + return fmt.Errorf("could not bind flag to viper: %w", bindErr) + } + + if viper.GetBool("json-log") { + logging.ConfigureLogrusJSON(log.StandardLogger()) + } + + if err := tracing.InitTracerWithUpstreams("snapshot-source", viper.GetString("honeycomb-api-key"), viper.GetString("sentry-dsn")); err != nil { + log.WithError(err).Error("could not init tracer") + return fmt.Errorf("could not init tracer: %w", err) + } + return nil + } + + // shut down tracing at the end of the process + rootCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) { + tracing.ShutdownTracer(context.Background()) + } +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + viper.SetConfigFile(cfgFile) + + replacer := strings.NewReplacer("-", "_") + + viper.SetEnvKeyReplacer(replacer) + // Do not set env prefix so APP, API_KEY, NATS_* etc. are read the same as other sources (aws, gcp). + // Snapshot-specific options use explicit BindEnv (e.g. SNAPSHOT_SOURCE) in flag init. + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + log.Infof("Using config file: %v", viper.ConfigFileUsed()) + } +} + +// TerminationLogHook A hook that logs fatal errors to the termination log +type TerminationLogHook struct{} + +func (t TerminationLogHook) Levels() []log.Level { + return []log.Level{log.FatalLevel} +} + +func (t TerminationLogHook) Fire(e *log.Entry) error { + // shutdown tracing first to ensure all spans are flushed + tracing.ShutdownTracer(context.Background()) + tLog, err := os.OpenFile("/dev/termination-log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + + var message string + + message = e.Message + + for k, v := range e.Data { + message = fmt.Sprintf("%v %v=%v", message, k, v) + } + + _, err = tLog.WriteString(message) + + return err +} diff --git a/sources/snapshot/main.go b/sources/snapshot/main.go new file mode 100644 index 00000000..c5754cd1 --- /dev/null +++ b/sources/snapshot/main.go @@ -0,0 +1,11 @@ +package main + +import ( + _ "go.uber.org/automaxprocs" + + "github.com/overmindtech/cli/sources/snapshot/cmd" +) + +func main() { + cmd.Execute() +} From e2cdf0877023c125fc2d1e66b9253e6c5d2fea74 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 18:13:32 +0100 Subject: [PATCH 38/51] [ENG-2660] Add Terraform provider for managing AWS sources (#3955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Implements Phase 3 of ENG-2660: a Terraform/OpenTofu provider (`overmind_aws_source` resource) that lets customers manage Overmind AWS sources as infrastructure-as-code. - Provider authenticates via `OVERMIND_API_KEY` / `OVERMIND_APP_URL`, exchanges the API key for an OAuth token using the shared `auth` package, and calls `ManagementService` over ConnectRPC. - Includes unit tests backed by a mock ConnectRPC server (runs unconditionally via `resource.UnitTest`, no `TF_ACC` or external credentials required). ## Linear Ticket - **Ticket**: [ENG-2660](https://linear.app/overmind/issue/ENG-2660) — Phase 3: Terraform Provider implementation ## Changes New files in `aws-source/module/provider/`: | File | Purpose | | --- | --- | | `main.go` | Provider entry point (`providerserver.Serve`) | | `provider.go` | Provider schema, env-var resolution, auth setup, ManagementService client creation | | `resource_aws_source.go` | `overmind_aws_source` resource — full CRUD + ImportState against ManagementService | | `provider_test.go` | Unit tests (mock ConnectRPC server) and missing-API-key validation test | Other changes: - `aws-source/README.md` — added Terraform Provider section (build, test, config) - `go.mod` / `go.sum` — added `terraform-plugin-framework`, `terraform-plugin-go`, `terraform-plugin-testing` dependencies ## Deviations from plan 1. **`OVERMIND_APP_URL` instead of `OVERMIND_API_URL`**: The plan's task table references `OVERMIND_API_URL`, but the implementation uses `OVERMIND_APP_URL`. This is intentional — the provider resolves the API URL dynamically from the app URL via `sdp.NewOvermindInstance()` (calls `/api/public/instance-data`), following the existing Overmind convention. The plan itself acknowledges this in Decision 9. 2. **Auth uses `auth.NewAPIKeyTokenSource` directly**: The plan describes a "ConnectRPC client wrapper with API key -> OAuth token exchange" as a separate `client.go` concern. In practice, the shared `auth.NewAPIKeyTokenSource` already encapsulates the full token lifecycle (exchange, caching, refresh), so no custom exchange logic was needed and `client.go` was removed — the 3-line HTTP client setup is inlined in `provider.go`'s `Configure` method. ## New commit call-outs - `62fc7c8ad` — Adds baseline OpenTelemetry instrumentation to the provider: startup tracing wiring in `main.go`, provider configure span/context propagation in `provider.go`, and CRUD/import spans in `resource_aws_source.go`; also updates the observability ADR to document internal-vs-customer-run behavior and the unified customer-run binaries pattern. - `225d949fc` — Updates `.cursor/rules/go-standards.mdc` to extend the no-fatal guidance to include `os.Exit` for the same graceful-shutdown/telemetry-flush rationale. - `0b73e34ea` — Replaces `log.Fatal` in the provider entry point with explicit stderr output plus process exit handling to align with project standards. - `d4b2b5314` — Fixes `contextcheck` in tests by using the provided function context in `testProvider.Configure`. - `7669b3fbb` — Refreshes `.cursor/commands/create-implementation-plan.md` via the implementation-plan workflow command (repo command metadata update). ## Test plan - [x] `go test -v ./aws-source/module/provider/` passes (2 unit tests) - [x] `go build ./aws-source/module/provider/` compiles cleanly - [ ] Run full Terraform module against staging instance to validate end-to-end --- > [!NOTE] > **Medium Risk** > Introduces a new customer-facing Terraform provider that performs authenticated remote source CRUD; correctness and error handling impact customer IaC workflows, though changes are mostly additive and covered by unit tests. > > **Overview** > Adds a new Terraform/OpenTofu provider under `aws-source/module/provider` that lets customers manage Overmind AWS sources as IaC via an `overmind_aws_source` resource (CRUD + import) and an `overmind_aws_external_id` data source, backed by ConnectRPC `ManagementService` calls. > > Provider configuration now supports `OVERMIND_API_KEY` and `OVERMIND_APP_URL` (resolving API URL dynamically), includes baseline OpenTelemetry/logrus hook wiring with an opt-out `HONEYCOMB_API_KEY`, and ships unit tests using `terraform-plugin-testing` with a mock ConnectRPC server. Documentation is updated in `aws-source/README.md`, and `go.mod`/`go.sum` add the Terraform plugin framework/testing dependencies. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 510380ddd4385a5693f67339d40a6a02c6656706. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: e27bd430894b02b0ddff29f24e4ef2c091b80b1d --- .../provider/datasource_aws_external_id.go | 81 ++++ aws-source/module/provider/main.go | 53 +++ aws-source/module/provider/provider.go | 132 +++++++ aws-source/module/provider/provider_test.go | 243 ++++++++++++ .../module/provider/resource_aws_source.go | 374 ++++++++++++++++++ go.mod | 53 ++- go.sum | 108 ++++- 7 files changed, 1027 insertions(+), 17 deletions(-) create mode 100644 aws-source/module/provider/datasource_aws_external_id.go create mode 100644 aws-source/module/provider/main.go create mode 100644 aws-source/module/provider/provider.go create mode 100644 aws-source/module/provider/provider_test.go create mode 100644 aws-source/module/provider/resource_aws_source.go diff --git a/aws-source/module/provider/datasource_aws_external_id.go b/aws-source/module/provider/datasource_aws_external_id.go new file mode 100644 index 00000000..cf5f1a0b --- /dev/null +++ b/aws-source/module/provider/datasource_aws_external_id.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + "github.com/hashicorp/terraform-plugin-framework/datasource" + dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +var _ datasource.DataSource = (*awsExternalIdDataSource)(nil) + +type awsExternalIdDataSource struct { + mgmt sdpconnect.ManagementServiceClient +} + +type awsExternalIdDataSourceModel struct { + ExternalID types.String `tfsdk:"external_id"` +} + +func NewAWSExternalIdDataSource() datasource.DataSource { + return &awsExternalIdDataSource{} +} + +func (d *awsExternalIdDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_external_id" +} + +func (d *awsExternalIdDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = dsschema.Schema{ + Description: "Retrieves the stable AWS STS external ID for the current Overmind account. " + + "Use this to configure the trust policy on an IAM role before creating the source.", + Attributes: map[string]dsschema.Attribute{ + "external_id": dsschema.StringAttribute{ + Description: "AWS STS external ID, stable per Overmind account.", + Computed: true, + }, + }, + } +} + +func (d *awsExternalIdDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + mgmt, ok := req.ProviderData.(sdpconnect.ManagementServiceClient) + if !ok { + resp.Diagnostics.AddError("Unexpected DataSource Configure Type", + fmt.Sprintf("Expected sdpconnect.ManagementServiceClient, got %T", req.ProviderData)) + return + } + d.mgmt = mgmt +} + +func (d *awsExternalIdDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSExternalId Read") + defer span.End() + + extIDResp, err := d.mgmt.GetOrCreateAWSExternalId(ctx, + connect.NewRequest(&sdp.GetOrCreateAWSExternalIdRequest{})) + if err != nil { + resp.Diagnostics.AddError("Failed to get AWS external ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetOrCreateAWSExternalId failed") + return + } + + externalID := extIDResp.Msg.GetAwsExternalId() + span.SetAttributes(attribute.String("ovm.externalId", externalID)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &awsExternalIdDataSourceModel{ + ExternalID: types.StringValue(externalID), + })...) +} diff --git a/aws-source/module/provider/main.go b/aws-source/module/provider/main.go new file mode 100644 index 00000000..66be91c6 --- /dev/null +++ b/aws-source/module/provider/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/overmindtech/cli/go/tracing" + log "github.com/sirupsen/logrus" + "github.com/uptrace/opentelemetry-go-extra/otellogrus" +) + +const ( + version = "0.1.0" + + defaultHoneycombAPIKey = "hcaik_01j03qe0exnn2jxpj2vxkqb7yrqtr083kyk9rxxt2wzjamz8be94znqmwa" //nolint:gosec // public ingest key, same as CLI +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + //nolint:gocritic // os.Exit in main after deferred cleanup is the only option + os.Exit(1) + } +} + +func run() error { + formatter := new(log.TextFormatter) + formatter.DisableTimestamp = true + log.SetFormatter(formatter) + log.SetOutput(os.Stderr) + log.SetLevel(log.ErrorLevel) + + honeycombAPIKey := defaultHoneycombAPIKey + if v, ok := os.LookupEnv("HONEYCOMB_API_KEY"); ok { + honeycombAPIKey = v + } + if honeycombAPIKey != "" { + if err := tracing.InitTracerWithUpstreams("overmind-terraform-provider", honeycombAPIKey, ""); err != nil { + return fmt.Errorf("initialising tracing: %w", err) + } + defer tracing.ShutdownTracer(context.Background()) + + log.AddHook(otellogrus.NewHook(otellogrus.WithLevels( + log.AllLevels[:log.GetLevel()+1]..., + ))) + } + + return providerserver.Serve(context.Background(), NewProvider(version), providerserver.ServeOpts{ + Address: "registry.terraform.io/overmindtech/overmind", + }) +} diff --git a/aws-source/module/provider/provider.go b/aws-source/module/provider/provider.go new file mode 100644 index 00000000..a228fe3b --- /dev/null +++ b/aws-source/module/provider/provider.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/overmindtech/cli/go/auth" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "golang.org/x/oauth2" +) + +var _ provider.Provider = (*overmindProvider)(nil) + +type overmindProvider struct { + version string +} + +type overmindProviderModel struct { + AppURL types.String `tfsdk:"app_url"` + APIKey types.String `tfsdk:"api_key"` +} + +func NewProvider(version string) func() provider.Provider { + return func() provider.Provider { + return &overmindProvider{version: version} + } +} + +func (p *overmindProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "overmind" + resp.Version = p.version +} + +func (p *overmindProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "The Overmind provider manages infrastructure sources via the Overmind API. " + + "Configuration is read from the OVERMIND_API_KEY and OVERMIND_APP_URL environment variables by default.", + Attributes: map[string]schema.Attribute{ + "api_key": schema.StringAttribute{ + Description: "Overmind API key. Can also be set via the OVERMIND_API_KEY environment variable.", + Optional: true, + Sensitive: true, + }, + "app_url": schema.StringAttribute{ + Description: "Overmind application URL (e.g. https://app.overmind.tech). " + + "Can also be set via the OVERMIND_APP_URL environment variable.", + Optional: true, + }, + }, + } +} + +func (p *overmindProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + ctx, span := tracing.Tracer().Start(ctx, "Provider Configure") + defer span.End() + + var config overmindProviderModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + apiKey := os.Getenv("OVERMIND_API_KEY") + if !config.APIKey.IsNull() { + apiKey = config.APIKey.ValueString() + } + + appURL := os.Getenv("OVERMIND_APP_URL") + if !config.AppURL.IsNull() { + appURL = config.AppURL.ValueString() + } + if appURL == "" { + appURL = "https://app.overmind.tech" + } + + span.SetAttributes(attribute.String("ovm.provider.appUrl", appURL)) + + if apiKey == "" { + resp.Diagnostics.AddError( + "Missing API Key", + "An Overmind API key must be provided via the api_key provider attribute or the OVERMIND_API_KEY environment variable.", + ) + span.SetStatus(codes.Error, "missing API key") + return + } + + oi, err := sdp.NewOvermindInstance(ctx, appURL) + if err != nil { + resp.Diagnostics.AddError("Failed to resolve Overmind instance", + fmt.Sprintf("Could not resolve instance data from %s: %s", appURL, err)) + span.RecordError(err) + span.SetStatus(codes.Error, "instance resolution failed") + return + } + + apiURL := oi.ApiUrl.String() + span.SetAttributes(attribute.String("ovm.provider.apiUrl", apiURL)) + + tokenSource := auth.NewAPIKeyTokenSource(apiKey, apiURL) + httpClient := tracing.HTTPClient() + httpClient.Transport = &oauth2.Transport{ + Source: tokenSource, + Base: httpClient.Transport, + } + + mgmtClient := sdpconnect.NewManagementServiceClient(httpClient, apiURL) + + resp.DataSourceData = mgmtClient + resp.ResourceData = mgmtClient +} + +func (p *overmindProvider) Resources(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + NewAWSSourceResource, + } +} + +func (p *overmindProvider) DataSources(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewAWSExternalIdDataSource, + } +} diff --git a/aws-source/module/provider/provider_test.go b/aws-source/module/provider/provider_test.go new file mode 100644 index 00000000..db9ca1d8 --- /dev/null +++ b/aws-source/module/provider/provider_test.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "regexp" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + tfresource "github.com/hashicorp/terraform-plugin-testing/helper/resource" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "golang.org/x/oauth2" +) + +// --- mock ManagementService handler --- + +type mockMgmtHandler struct { + sdpconnect.UnimplementedManagementServiceHandler + mu sync.Mutex + sources map[string]*sdp.Source + externalID string +} + +func newMockMgmtHandler() *mockMgmtHandler { + return &mockMgmtHandler{ + sources: make(map[string]*sdp.Source), + externalID: "test-external-id-12345", + } +} + +func (m *mockMgmtHandler) GetOrCreateAWSExternalId(_ context.Context, _ *connect.Request[sdp.GetOrCreateAWSExternalIdRequest]) (*connect.Response[sdp.GetOrCreateAWSExternalIdResponse], error) { + return connect.NewResponse(&sdp.GetOrCreateAWSExternalIdResponse{ + AwsExternalId: m.externalID, + }), nil +} + +func (m *mockMgmtHandler) CreateSource(_ context.Context, req *connect.Request[sdp.CreateSourceRequest]) (*connect.Response[sdp.CreateSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id := uuid.New() + source := &sdp.Source{ + Metadata: &sdp.SourceMetadata{UUID: id[:]}, + Properties: req.Msg.GetProperties(), + } + m.sources[id.String()] = source + return connect.NewResponse(&sdp.CreateSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) GetSource(_ context.Context, req *connect.Request[sdp.GetSourceRequest]) (*connect.Response[sdp.GetSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + source, ok := m.sources[id.String()] + if !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + return connect.NewResponse(&sdp.GetSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) UpdateSource(_ context.Context, req *connect.Request[sdp.UpdateSourceRequest]) (*connect.Response[sdp.UpdateSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + source, ok := m.sources[id.String()] + if !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + source.Properties = req.Msg.GetProperties() + return connect.NewResponse(&sdp.UpdateSourceResponse{Source: source}), nil +} + +func (m *mockMgmtHandler) DeleteSource(_ context.Context, req *connect.Request[sdp.DeleteSourceRequest]) (*connect.Response[sdp.DeleteSourceResponse], error) { + m.mu.Lock() + defer m.mu.Unlock() + id, err := uuid.FromBytes(req.Msg.GetUUID()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + if _, ok := m.sources[id.String()]; !ok { + return nil, connect.NewError(connect.CodeNotFound, nil) + } + delete(m.sources, id.String()) + return connect.NewResponse(&sdp.DeleteSourceResponse{}), nil +} + +// --- test provider that bypasses auth --- + +// testProvider wraps the real provider but overrides Configure to inject a +// pre-built client backed by the mock server. This avoids needing the +// instance-data endpoint, ApiKeyService, or real JWTs in unit tests. +type testProvider struct { + overmindProvider + serverURL string +} + +var _ provider.Provider = (*testProvider)(nil) + +func (p *testProvider) Configure(ctx context.Context, _ provider.ConfigureRequest, resp *provider.ConfigureResponse) { + httpClient := oauth2.NewClient(ctx, + oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "test"})) + mgmtClient := sdpconnect.NewManagementServiceClient(httpClient, p.serverURL) + resp.DataSourceData = mgmtClient + resp.ResourceData = mgmtClient +} + +func (p *testProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{}, + } +} + +func (p *testProvider) Resources(ctx context.Context) []func() resource.Resource { + return p.overmindProvider.Resources(ctx) +} + +func (p *testProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return p.overmindProvider.DataSources(ctx) +} + +// --- test helpers --- + +func startTestServer(t *testing.T) string { + t.Helper() + handler := newMockMgmtHandler() + path, h := sdpconnect.NewManagementServiceHandler(handler) + mux := http.NewServeMux() + mux.Handle(path, h) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv.URL +} + +func unitTestProviderFactories(serverURL string) map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "overmind": providerserver.NewProtocol6WithError(&testProvider{ + overmindProvider: overmindProvider{version: "test"}, + serverURL: serverURL, + }), + } +} + +func accTestProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "overmind": providerserver.NewProtocol6WithError(NewProvider("test")()), + } +} + +// --- unit tests (mock server, always run) --- + +func TestAWSSourceResource_CRUD(t *testing.T) { + serverURL := startTestServer(t) + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: unitTestProviderFactories(serverURL), + Steps: []tfresource.TestStep{ + { + Config: testAccAWSSourceConfig("test-source", "arn:aws:iam::123456789012:role/test", `["us-east-1", "eu-west-1"]`), + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttrSet("overmind_aws_source.test", "id"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "name", "test-source"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_role_arn", "arn:aws:iam::123456789012:role/test"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "external_id", "test-external-id-12345"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.#", "2"), + ), + }, + { + Config: testAccAWSSourceConfig("updated-source", "arn:aws:iam::123456789012:role/test", `["us-west-2"]`), + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "name", "updated-source"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.#", "1"), + tfresource.TestCheckResourceAttr("overmind_aws_source.test", "aws_regions.0", "us-west-2"), + ), + }, + { + ResourceName: "overmind_aws_source.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestProviderConfigure_MissingAPIKey(t *testing.T) { + t.Setenv("OVERMIND_API_KEY", "") + t.Setenv("OVERMIND_APP_URL", "") + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: accTestProviderFactories(), + Steps: []tfresource.TestStep{ + { + Config: ` +resource "overmind_aws_source" "test" { + name = "x" + aws_role_arn = "arn" + aws_regions = ["us-east-1"] +} +`, + ExpectError: regexp.MustCompile(`Missing API Key`), + }, + }, + }) +} + +func TestAWSExternalIdDataSource_Read(t *testing.T) { + serverURL := startTestServer(t) + + tfresource.UnitTest(t, tfresource.TestCase{ + ProtoV6ProviderFactories: unitTestProviderFactories(serverURL), + Steps: []tfresource.TestStep{ + { + Config: `data "overmind_aws_external_id" "test" {}`, + Check: tfresource.ComposeAggregateTestCheckFunc( + tfresource.TestCheckResourceAttr( + "data.overmind_aws_external_id.test", "external_id", "test-external-id-12345"), + ), + }, + }, + }) +} + +func testAccAWSSourceConfig(name, roleARN, regions string) string { + return `resource "overmind_aws_source" "test" { + name = "` + name + `" + aws_role_arn = "` + roleARN + `" + aws_regions = ` + regions + ` +}` +} diff --git a/aws-source/module/provider/resource_aws_source.go b/aws-source/module/provider/resource_aws_source.go new file mode 100644 index 00000000..86d5b9c3 --- /dev/null +++ b/aws-source/module/provider/resource_aws_source.go @@ -0,0 +1,374 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + sdp "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdp-go/sdpconnect" + "github.com/overmindtech/cli/go/tracing" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "google.golang.org/protobuf/types/known/structpb" +) + +var ( + _ resource.Resource = (*awsSourceResource)(nil) + _ resource.ResourceWithImportState = (*awsSourceResource)(nil) +) + +type awsSourceResource struct { + mgmt sdpconnect.ManagementServiceClient +} + +type awsSourceResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AWSRoleARN types.String `tfsdk:"aws_role_arn"` + AWSRegions types.List `tfsdk:"aws_regions"` + ExternalID types.String `tfsdk:"external_id"` +} + +func NewAWSSourceResource() resource.Resource { + return &awsSourceResource{} +} + +func (r *awsSourceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_source" +} + +func (r *awsSourceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages an Overmind AWS infrastructure source.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Source UUID assigned by the Overmind API.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "Human-readable name for this source.", + Required: true, + }, + "aws_role_arn": schema.StringAttribute{ + Description: "ARN of the IAM role to assume in the customer's AWS account.", + Required: true, + }, + "aws_regions": schema.ListAttribute{ + Description: "AWS regions this source should discover resources in.", + Required: true, + ElementType: types.StringType, + }, + "external_id": schema.StringAttribute{ + Description: "AWS STS external ID for the IAM trust policy, stable per Overmind account.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *awsSourceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + mgmt, ok := req.ProviderData.(sdpconnect.ManagementServiceClient) + if !ok { + resp.Diagnostics.AddError("Unexpected Resource Configure Type", + fmt.Sprintf("Expected sdpconnect.ManagementServiceClient, got %T", req.ProviderData)) + return + } + r.mgmt = mgmt +} + +func (r *awsSourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Create") + defer span.End() + + var plan awsSourceResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes( + attribute.String("ovm.source.name", plan.Name.ValueString()), + attribute.String("ovm.source.roleArn", plan.AWSRoleARN.ValueString()), + ) + + extIDResp, err := r.mgmt.GetOrCreateAWSExternalId(ctx, + connect.NewRequest(&sdp.GetOrCreateAWSExternalIdRequest{})) + if err != nil { + resp.Diagnostics.AddError("Failed to get AWS external ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetOrCreateAWSExternalId failed") + return + } + externalID := extIDResp.Msg.GetAwsExternalId() + + regions, diags := regionsFromList(ctx, plan.AWSRegions) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + sourceConfig, err := structpb.NewStruct(map[string]any{ + "aws-access-strategy": "external-id", + "aws-external-id": externalID, + "aws-target-role-arn": plan.AWSRoleARN.ValueString(), + "aws-regions": strings.Join(regions, ","), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to build source config", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "config build failed") + return + } + + createResp, err := r.mgmt.CreateSource(ctx, connect.NewRequest(&sdp.CreateSourceRequest{ + Properties: &sdp.SourceProperties{ + DescriptiveName: plan.Name.ValueString(), + Type: "aws", + Config: sourceConfig, + }, + })) + if err != nil { + resp.Diagnostics.AddError("Failed to create source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "CreateSource failed") + return + } + + source := createResp.Msg.GetSource() + sourceUUID, err := uuid.FromBytes(source.GetMetadata().GetUUID()) + if err != nil { + resp.Diagnostics.AddError("Failed to parse source UUID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "UUID parse failed") + return + } + + plan.ID = types.StringValue(sourceUUID.String()) + plan.ExternalID = types.StringValue(externalID) + + span.SetAttributes(attribute.String("ovm.source.id", sourceUUID.String())) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *awsSourceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Read") + defer span.End() + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes(attribute.String("ovm.source.id", state.ID.ValueString())) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + getResp, err := r.mgmt.GetSource(ctx, connect.NewRequest(&sdp.GetSourceRequest{ + UUID: uuidBytes, + })) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + span.SetAttributes(attribute.Bool("ovm.source.removed", true)) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Failed to read source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "GetSource failed") + return + } + + source := getResp.Msg.GetSource() + props := source.GetProperties() + + state.Name = types.StringValue(props.GetDescriptiveName()) + + if cfg := props.GetConfig(); cfg != nil { + fields := cfg.GetFields() + if v, ok := fields["aws-target-role-arn"]; ok { + state.AWSRoleARN = types.StringValue(v.GetStringValue()) + } + if v, ok := fields["aws-regions"]; ok { + regionStr := v.GetStringValue() + regionVals := splitNonEmpty(regionStr, ",") + listVal, diags := types.ListValueFrom(ctx, types.StringType, regionVals) + resp.Diagnostics.Append(diags...) + state.AWSRegions = listVal + } + if v, ok := fields["aws-external-id"]; ok { + state.ExternalID = types.StringValue(v.GetStringValue()) + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *awsSourceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Update") + defer span.End() + + var plan awsSourceResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes( + attribute.String("ovm.source.id", state.ID.ValueString()), + attribute.String("ovm.source.name", plan.Name.ValueString()), + ) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + regions, diags := regionsFromList(ctx, plan.AWSRegions) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + externalID := state.ExternalID.ValueString() + + sourceConfig, err := structpb.NewStruct(map[string]any{ + "aws-access-strategy": "external-id", + "aws-external-id": externalID, + "aws-target-role-arn": plan.AWSRoleARN.ValueString(), + "aws-regions": strings.Join(regions, ","), + }) + if err != nil { + resp.Diagnostics.AddError("Failed to build source config", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "config build failed") + return + } + + _, err = r.mgmt.UpdateSource(ctx, connect.NewRequest(&sdp.UpdateSourceRequest{ + UUID: uuidBytes, + Properties: &sdp.SourceProperties{ + DescriptiveName: plan.Name.ValueString(), + Type: "aws", + Config: sourceConfig, + }, + })) + if err != nil { + resp.Diagnostics.AddError("Failed to update source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "UpdateSource failed") + return + } + + plan.ID = state.ID + plan.ExternalID = state.ExternalID + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *awsSourceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Delete") + defer span.End() + + var state awsSourceResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + span.SetAttributes(attribute.String("ovm.source.id", state.ID.ValueString())) + + uuidBytes, err := uuidToBytes(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Invalid source ID", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "invalid UUID") + return + } + + _, err = r.mgmt.DeleteSource(ctx, connect.NewRequest(&sdp.DeleteSourceRequest{ + UUID: uuidBytes, + })) + if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + span.SetAttributes(attribute.Bool("ovm.source.alreadyGone", true)) + return + } + resp.Diagnostics.AddError("Failed to delete source", err.Error()) + span.RecordError(err) + span.SetStatus(codes.Error, "DeleteSource failed") + } +} + +func (r *awsSourceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + ctx, span := tracing.Tracer().Start(ctx, "AWSSource Import") + defer span.End() + + span.SetAttributes(attribute.String("ovm.source.id", req.ID)) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// --- helpers --- + +func uuidToBytes(s string) ([]byte, error) { + parsed, err := uuid.Parse(s) + if err != nil { + return nil, fmt.Errorf("parsing UUID %q: %w", s, err) + } + b := parsed[:] + return b, nil +} + +func regionsFromList(ctx context.Context, list types.List) ([]string, diag.Diagnostics) { + var regions []string + diags := list.ElementsAs(ctx, ®ions, false) + return regions, diags +} + +func splitNonEmpty(s, sep string) []string { + parts := strings.Split(s, sep) + result := make([]string, 0, len(parts)) + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} diff --git a/go.mod b/go.mod index b869fb14..120b4e1b 100644 --- a/go.mod +++ b/go.mod @@ -116,6 +116,9 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 + github.com/hashicorp/terraform-plugin-framework v1.17.0 + github.com/hashicorp/terraform-plugin-go v0.29.0 + github.com/hashicorp/terraform-plugin-testing v1.14.0 github.com/invopop/jsonschema v0.13.0 github.com/jackc/pgx/v5 v5.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 @@ -212,8 +215,12 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/PuerkitoBio/rehttp v1.4.0 // indirect github.com/STARRY-S/zip v0.2.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect @@ -261,6 +268,7 @@ require ( github.com/cheggaaa/pb/v3 v3.1.4 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/containerd/console v1.0.4 // indirect github.com/corpix/uarand v0.2.0 // indirect @@ -272,6 +280,8 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/extism/go-sdk v1.7.1 // indirect github.com/fatih/color v1.16.0 // indirect @@ -296,6 +306,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect @@ -314,10 +325,25 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-cty v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-exec v0.24.0 // indirect + github.com/hashicorp/terraform-json v0.27.2 // indirect + github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 // indirect + github.com/hashicorp/terraform-registry-address v0.4.0 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect @@ -350,7 +376,11 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -359,11 +389,13 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.7 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/projectdiscovery/blackrock v0.0.1 // indirect @@ -396,12 +428,12 @@ require ( github.com/samber/lo v1.52.0 // indirect github.com/samber/slog-common v0.20.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/sergi/go-diff v1.3.1 // indirect github.com/shirou/gopsutil/v3 v3.23.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect @@ -423,6 +455,9 @@ require ( github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/weppos/publicsuffix-go v0.30.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect @@ -437,11 +472,13 @@ require ( github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect go.devnw.com/structs v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -458,6 +495,7 @@ require ( golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect @@ -476,16 +514,3 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) - -require ( - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect -) diff --git a/go.sum b/go.sum index d0592667..1b0cecf6 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/1password/onepassword-sdk-go v0.4.0 h1:Nou39yuC6Q0om03irkh5UurfPdX3wx26qZZhQeC9TBU= github.com/1password/onepassword-sdk-go v0.4.0/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0= @@ -167,6 +169,8 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/MrAlias/otel-schema-utils v0.4.0-alpha h1:6ZG9rw4NvxKwRp2Bmnfr8WJZVWLhK4e5n3+ezXE6Z2g= github.com/MrAlias/otel-schema-utils v0.4.0-alpha/go.mod h1:baehOhES9qiLv9xMcsY6ZQlKLBRR89XVJEvU7Yz3qJk= github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= @@ -174,6 +178,8 @@ github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2piv github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/PuerkitoBio/rehttp v1.4.0 h1:rIN7A2s+O9fmHUM1vUcInvlHj9Ysql4hE+Y0wcl/xk8= github.com/PuerkitoBio/rehttp v1.4.0/go.mod h1:LUwKPoDbDIA2RL5wYZCNsQ90cx4OJ4AWBmq6KzWZL1s= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= @@ -212,6 +218,7 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -341,6 +348,8 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk= github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -396,6 +405,8 @@ github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6 github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE= github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -418,6 +429,8 @@ github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8r github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= @@ -436,6 +449,7 @@ github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZ github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -461,6 +475,12 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01 github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -510,10 +530,13 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -603,24 +626,60 @@ github.com/harness/harness-go-sdk v0.7.9/go.mod h1:iEAGFfIm0MOFJxN6tqMQSPZiEO/Dz github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0= +github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 h1:p+oHuSCXvfFBFAejlPswDa7i5fi3r3+03jeW9mJs4qM= github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= +github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/terraform-plugin-framework v1.17.0 h1:JdX50CFrYcYFY31gkmitAEAzLKoBgsK+iaJjDC8OexY= +github.com/hashicorp/terraform-plugin-framework v1.17.0/go.mod h1:4OUXKdHNosX+ys6rLgVlgklfxN3WHR5VHSOABeS/BM0= +github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= +github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= +github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= +github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU= +github.com/hashicorp/terraform-plugin-testing v1.14.0 h1:5t4VKrjOJ0rg0sVuSJ86dz5K7PHsMO6OKrHFzDBerWA= +github.com/hashicorp/terraform-plugin-testing v1.14.0/go.mod h1:1qfWkecyYe1Do2EEOK/5/WnTyvC8wQucUkkhiGLg5nk= +github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= +github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -643,8 +702,12 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -661,6 +724,8 @@ github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= github.com/kaptinlin/jsonrepair v0.2.8 h1:BjiyVcJDwGrz01/9cvtX1ArNVvtybydGFDxoaU/6lsU= github.com/kaptinlin/jsonrepair v0.2.8/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -715,8 +780,12 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -742,12 +811,20 @@ github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -782,6 +859,8 @@ github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKi github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -808,6 +887,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -925,8 +1006,8 @@ github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adO github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda h1:w3JksZEWJDI7x+No5yh2/8S86hq1dmJy7n5btakG30U= github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda/go.mod h1:xVL4PnCuCPwkXhVVQysVrX3hEv7nWnIbfnDj2B+hsPw= github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= @@ -941,6 +1022,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -972,6 +1055,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1037,6 +1121,13 @@ github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z7 github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db/go.mod h1:aiQaH1XpzIfgrJq3S1iw7w+3EDbRP7mF5fmwUhWyRUs= github.com/weppos/publicsuffix-go v0.30.1 h1:8q+QwBS1MY56Zjfk/50ycu33NN8aa1iCCEQwo/71Oos= @@ -1045,6 +1136,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b h1:ajy6PPLDeQaf7xf4P/4Ie/wsUTEqjy3Irl+xFelmjk0= @@ -1258,7 +1351,9 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1268,10 +1363,13 @@ golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1307,6 +1405,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1370,6 +1469,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1425,10 +1526,11 @@ gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From f52278ba1ceb5ebf40a6081d2dbe1f296aef62af Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 18:50:38 +0100 Subject: [PATCH 39/51] [ENG-2668] Add Terraform module for AWS source setup (#3956) GitOrigin-RevId: fe718a08347d18423354cb344c2facf552aee85f --- aws-source/module/terraform/README.md | 99 +++++++++++++++++++ .../terraform/examples/multi-account/main.tf | 48 +++++++++ .../terraform/examples/single-account/main.tf | 19 ++++ aws-source/module/terraform/main.tf | 95 ++++++++++++++++++ aws-source/module/terraform/outputs.tf | 14 +++ aws-source/module/terraform/variables.tf | 40 ++++++++ aws-source/module/terraform/versions.tf | 14 +++ 7 files changed, 329 insertions(+) create mode 100644 aws-source/module/terraform/README.md create mode 100644 aws-source/module/terraform/examples/multi-account/main.tf create mode 100644 aws-source/module/terraform/examples/single-account/main.tf create mode 100644 aws-source/module/terraform/main.tf create mode 100644 aws-source/module/terraform/outputs.tf create mode 100644 aws-source/module/terraform/variables.tf create mode 100644 aws-source/module/terraform/versions.tf diff --git a/aws-source/module/terraform/README.md b/aws-source/module/terraform/README.md new file mode 100644 index 00000000..ab07108f --- /dev/null +++ b/aws-source/module/terraform/README.md @@ -0,0 +1,99 @@ +# Overmind AWS Source Setup + +Terraform module that configures an AWS account for +[Overmind](https://overmind.tech) infrastructure discovery. A single +`terraform apply` creates: + +1. An IAM role with a read-only policy in the target AWS account +2. A trust policy allowing Overmind to assume the role via STS external ID +3. An Overmind source registration pointing at the role + +## Usage + +```hcl +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source-setup/overmind" + + name = "production" +} +``` + +## Inputs + +| Name | Description | Type | Default | Required | +| --- | --- | --- | --- | --- | +| `name` | Descriptive name for the source in Overmind | `string` | n/a | yes | +| `regions` | AWS regions to discover (defaults to all non-opt-in regions) | `list(string)` | All 17 standard regions | no | +| `role_name` | Name for the IAM role created in this account | `string` | `"overmind-read-only"` | no | +| `tags` | Additional tags to apply to IAM resources | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +| --- | --- | +| `role_arn` | ARN of the created IAM role | +| `source_id` | UUID of the Overmind source | +| `external_id` | AWS STS external ID used in the trust policy | + +## Multi-account example + +Use AWS provider aliases to onboard several accounts at once: + +```hcl +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + assume_role { role_arn = "arn:aws:iam::111111111111:role/terraform" } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + assume_role { role_arn = "arn:aws:iam::222222222222:role/terraform" } +} + +module "overmind_production" { + source = "overmindtech/aws-source-setup/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source-setup/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} +``` + +## Authentication + +The Overmind provider reads `OVERMIND_API_KEY` from the environment. The API key +must have `sources:write` scope. + +The AWS provider must have permissions to create IAM roles and policies in the +target account. + +## Requirements + +| Name | Version | +| --- | --- | +| terraform | >= 1.5.0 | +| aws | >= 6.0 | +| overmind | >= 0.1.0 | diff --git a/aws-source/module/terraform/examples/multi-account/main.tf b/aws-source/module/terraform/examples/multi-account/main.tf new file mode 100644 index 00000000..3bd989e2 --- /dev/null +++ b/aws-source/module/terraform/examples/multi-account/main.tf @@ -0,0 +1,48 @@ +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::111111111111:role/terraform" + } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::222222222222:role/terraform" + } +} + +module "overmind_production" { + source = "overmindtech/aws-source-setup/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source-setup/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} + +output "production_role_arn" { + value = module.overmind_production.role_arn +} + +output "staging_role_arn" { + value = module.overmind_staging.role_arn +} diff --git a/aws-source/module/terraform/examples/single-account/main.tf b/aws-source/module/terraform/examples/single-account/main.tf new file mode 100644 index 00000000..06adf4be --- /dev/null +++ b/aws-source/module/terraform/examples/single-account/main.tf @@ -0,0 +1,19 @@ +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source-setup/overmind" + + name = "production" +} + +output "role_arn" { + value = module.overmind_aws_source.role_arn +} + +output "source_id" { + value = module.overmind_aws_source.source_id +} diff --git a/aws-source/module/terraform/main.tf b/aws-source/module/terraform/main.tf new file mode 100644 index 00000000..7e90bf3e --- /dev/null +++ b/aws-source/module/terraform/main.tf @@ -0,0 +1,95 @@ +data "overmind_aws_external_id" "this" {} + +resource "aws_iam_role" "overmind" { + name = var.role_name + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::942836531449:root" } + Action = "sts:AssumeRole" + Condition = { + StringEquals = { + "sts:ExternalId" = data.overmind_aws_external_id.this.external_id + } + } + }, + { + Effect = "Allow" + Principal = { AWS = "arn:aws:iam::942836531449:root" } + Action = "sts:TagSession" + }, + ] + }) + + tags = merge(var.tags, { + "overmind.version" = "2026-02-17" + }) +} + +resource "aws_iam_role_policy" "overmind" { + name = "OvmReadOnly" + role = aws_iam_role.overmind.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "apigateway:Get*", + "autoscaling:Describe*", + "cloudfront:Get*", + "cloudfront:List*", + "cloudwatch:Describe*", + "cloudwatch:GetMetricData", + "cloudwatch:ListTagsForResource", + "directconnect:Describe*", + "dynamodb:Describe*", + "dynamodb:List*", + "ec2:Describe*", + "ecs:Describe*", + "ecs:List*", + "eks:Describe*", + "eks:List*", + "elasticfilesystem:Describe*", + "elasticloadbalancing:Describe*", + "iam:Get*", + "iam:List*", + "kms:Describe*", + "kms:Get*", + "kms:List*", + "lambda:Get*", + "lambda:List*", + "network-firewall:Describe*", + "network-firewall:List*", + "networkmanager:Describe*", + "networkmanager:Get*", + "networkmanager:List*", + "rds:Describe*", + "rds:ListTagsForResource", + "route53:Get*", + "route53:List*", + "s3:GetBucket*", + "s3:ListAllMyBuckets", + "sns:Get*", + "sns:List*", + "sqs:Get*", + "sqs:List*", + "ssm:Describe*", + "ssm:Get*", + "ssm:ListTagsForResource", + ] + Resource = "*" + }, + ] + }) +} + +resource "overmind_aws_source" "this" { + name = var.name + aws_role_arn = aws_iam_role.overmind.arn + aws_regions = var.regions +} diff --git a/aws-source/module/terraform/outputs.tf b/aws-source/module/terraform/outputs.tf new file mode 100644 index 00000000..1521b43d --- /dev/null +++ b/aws-source/module/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "role_arn" { + description = "ARN of the created IAM role." + value = aws_iam_role.overmind.arn +} + +output "source_id" { + description = "UUID of the Overmind source." + value = overmind_aws_source.this.id +} + +output "external_id" { + description = "AWS STS external ID used in the trust policy." + value = data.overmind_aws_external_id.this.external_id +} diff --git a/aws-source/module/terraform/variables.tf b/aws-source/module/terraform/variables.tf new file mode 100644 index 00000000..46e385b0 --- /dev/null +++ b/aws-source/module/terraform/variables.tf @@ -0,0 +1,40 @@ +variable "name" { + type = string + description = "Descriptive name for the source in Overmind." +} + +variable "regions" { + type = list(string) + default = [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ap-south-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-northeast-3", + "ap-southeast-1", + "ap-southeast-2", + "ca-central-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "sa-east-1", + ] + description = "AWS regions to discover. Defaults to all non-opt-in regions." +} + +variable "role_name" { + type = string + default = "overmind-read-only" + description = "Name for the IAM role created in this account." +} + +variable "tags" { + type = map(string) + default = {} + description = "Additional tags to apply to IAM resources." +} diff --git a/aws-source/module/terraform/versions.tf b/aws-source/module/terraform/versions.tf new file mode 100644 index 00000000..e5facdcb --- /dev/null +++ b/aws-source/module/terraform/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + overmind = { + source = "overmindtech/overmind" + version = ">= 0.1.0" + } + } +} From 007cbcfca670a7ab7107657728b70cfd2df684f0 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 19:12:55 +0100 Subject: [PATCH 40/51] [ENG-2673] Add Copybara and publishing pipeline for Terraform provider and module (#3958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add Copybara workflows, GoReleaser config, GPG signing, and GitHub Actions release pipelines to publish the Terraform provider and HCL module to public repos and registries - Provision per-repo GitHub Actions secrets (`OP_RO_TOKEN`, `RELEASE_PAT`) via Terraform, following the existing `homebrew-overmind`/`actions` pattern - Public repos ([terraform-provider-overmind](https://github.com/overmindtech/terraform-provider-overmind), [terraform-overmind-aws-source](https://github.com/overmindtech/terraform-overmind-aws-source)) have been created and seeded with workflow files ## Linear Ticket - **Ticket**: [ENG-2673](https://linear.app/overmind/issue/ENG-2673/phase-5-copybara-and-publishing-for-terraform-provider-and-module) — Phase 5: Copybara and Publishing for Terraform Provider & Module - **Purpose**: Set up the full automated release pipeline from monorepo tags to Terraform/OpenTofu registries - **Plan approval**: [ENG-2674](https://linear.app/overmind/issue/ENG-2674/approve-plan-phase-5-copybara-and-publishing-for-terraform-provider) assigned to Lionel Wilson ## Changes ### Copybara (`copy.bara.sky`) Two new workflows: `terraform-provider` (syncs provider + Go libs with import rewriting) and `terraform-aws-source-module` (syncs HCL module with directory flattening). ### Monorepo sync workflows (`.github/workflows/`) - `terraform-provider-sync.yml` — triggers on `terraform-provider/v*` tags - `terraform-aws-source-module-sync.yml` — triggers on `terraform-aws-source-module/v*` tags ### Provider release files (`aws-source/module/provider/`) - `.goreleaser.yml` — cross-platform builds, zip archives, SHA256 checksums, GPG signing - `terraform-registry-manifest.json` — protocol version 6.0 - `.github/workflows/release.yml` — loads GPG key from 1Password, runs GoReleaser - `.github/workflows/finalize-copybara-sync.yml` — runs `go mod tidy`, creates PR - `.github/workflows/tag-on-merge.yml` — creates version tag on merge ### Module release files (`aws-source/module/terraform/`) - `.github/workflows/finalize-copybara-sync.yml` — creates PR (no `go mod tidy`) - `.github/workflows/tag-on-merge.yml` — creates version tag on merge ### Terraform / secrets - `deploy/1password.tf` — 4 new `github_actions_secret` resources for both public repos - `deploy/variables.tf` — new `terraform_provider_release_pat` and `terraform_module_release_pat` variables - `deploy/.env.op`, `deploy/.github/env/op.local.secret`, `.devcontainer/devcontainer.json` — wire new PAT variables through 1Password and devcontainer ### Provider code - `aws-source/module/provider/main.go` — `const version` changed to `var version = "dev"` for GoReleaser ldflags injection ## Before first release The following manual steps remain (documented in the plan): 1. Create 1Password items: `Terraform Provider Release Github Token`, `Terraform Module Release Github Token`, `Terraform Provider GPG Key` 2. Register GPG public key at registry.terraform.io/settings/gpg-keys 3. After merge, `terraform apply` provisions the repo secrets 4. Push monorepo tags to trigger first automated release 5. Enroll in Terraform Registry and OpenTofu Registry Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Mostly CI/release automation and secret provisioning changes, but misconfiguration could leak or break release/tagging flows for the public Terraform repos. > > **Overview** > Adds end-to-end **Copybara-based publishing pipelines** for the Terraform provider and AWS source Terraform module, driven by new tag-triggered GitHub Actions workflows (`terraform-provider/v*`, `terraform-aws-source-module/v*`) that sync code to public repos on `copybara/vX.Y.Z` branches. > > Introduces release automation in the provider/module repos: Copybara finalization workflows that open PRs from `copybara/v*`, `tag-on-merge` workflows that create version tags using a `RELEASE_PAT`, and (for the provider) a GoReleaser-based release with GPG-signed checksums plus a Terraform registry manifest; provider `main.go` now uses an ldflags-injected `version` variable. > > Updates `copy.bara.sky` with two new workflows (`terraform-provider`, `terraform-aws-source-module`) and wires new Terraform-managed GitHub Actions secrets/inputs (including new PAT variables) through `deploy/` and the devcontainer to support the public repo automation; ADR index is updated to include newly accepted ADRs. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d3a131760eadca87088922bf8eca86de2c1be730. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 800dbd7acd6e954106b6a2f1125fc7526c0b2634 --- .../workflows/finalize-copybara-sync.yml | 112 ++++++++++++++++++ .../provider/.github/workflows/release.yml | 52 ++++++++ .../.github/workflows/tag-on-merge.yml | 52 ++++++++ aws-source/module/provider/.goreleaser.yml | 56 +++++++++ aws-source/module/provider/main.go | 6 +- .../provider/terraform-registry-manifest.json | 8 ++ .../workflows/finalize-copybara-sync.yml | 84 +++++++++++++ .../.github/workflows/tag-on-merge.yml | 52 ++++++++ 8 files changed, 418 insertions(+), 4 deletions(-) create mode 100644 aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml create mode 100644 aws-source/module/provider/.github/workflows/release.yml create mode 100644 aws-source/module/provider/.github/workflows/tag-on-merge.yml create mode 100644 aws-source/module/provider/.goreleaser.yml create mode 100644 aws-source/module/provider/terraform-registry-manifest.json create mode 100644 aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml create mode 100644 aws-source/module/terraform/.github/workflows/tag-on-merge.yml diff --git a/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml new file mode 100644 index 00000000..3a59a73a --- /dev/null +++ b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml @@ -0,0 +1,112 @@ +name: Finalize Copybara Sync + +on: + push: + branches: + - 'copybara/v*' + +concurrency: + group: copybara-sync-${{ github.ref }} + cancel-in-progress: true + +jobs: + finalize: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Extract version from branch name + id: version + run: | + VERSION=$(echo "$GITHUB_REF" | sed 's|refs/heads/copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Run go mod tidy + run: go mod tidy + + - name: Commit and push go mod tidy changes + run: | + if ! git diff --quiet go.mod go.sum; then + git add go.mod go.sum + git commit -m "Run go mod tidy" + git push origin ${{ github.ref_name }} + else + echo "No changes from go mod tidy" + fi + + - name: Extract original commit author + id: author + run: | + AUTHOR_EMAIL=$(git log -1 --format='%ae' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%ae') + AUTHOR_NAME=$(git log -1 --format='%an' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%an') + echo "email=$AUTHOR_EMAIL" >> $GITHUB_OUTPUT + echo "name=$AUTHOR_NAME" >> $GITHUB_OUTPUT + + if [[ "$AUTHOR_EMAIL" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then + GITHUB_USER=$(echo "${BASH_REMATCH[1]}" | sed 's/^[0-9]*+//') + echo "github_user=$GITHUB_USER" >> $GITHUB_OUTPUT + else + echo "github_user=" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_EMAIL: ${{ steps.author.outputs.email }} + GITHUB_USER: ${{ steps.author.outputs.github_user }} + run: | + PR_BODY="## Copybara Sync - Release ${VERSION} + + This PR was automatically created by Copybara, syncing changes from the [overmindtech/workspace](https://github.com/overmindtech/workspace) monorepo. + + **Original author:** ${AUTHOR_NAME} (${AUTHOR_EMAIL}) + + ### What happens when this PR is merged? + + 1. The \`tag-on-merge\` workflow will automatically create the \`${VERSION}\` tag on main + 2. This tag will trigger the release workflow, which will: + - Build provider binaries for all platforms via GoReleaser + - Sign checksums with GPG + - Create a GitHub release + - Terraform Registry will detect the release and publish the provider + + ### Review Checklist + + - [ ] Changes look correct and match the expected monorepo sync + - [ ] CI checks pass + " + + PR_URL=$(gh pr create \ + --base main \ + --head "${{ github.ref_name }}" \ + --title "Release ${VERSION}" \ + --body "$PR_BODY") + + echo "Created PR: $PR_URL" + + if [ -n "$GITHUB_USER" ]; then + echo "Requesting review from original author: $GITHUB_USER" + gh pr edit "$PR_URL" --add-reviewer "$GITHUB_USER" || true + fi + + echo "Requesting review from Engineering team" + gh pr edit "$PR_URL" --add-reviewer "overmindtech/Engineering" || true diff --git a/aws-source/module/provider/.github/workflows/release.yml b/aws-source/module/provider/.github/workflows/release.yml new file mode 100644 index 00000000..ff0ca190 --- /dev/null +++ b/aws-source/module/provider/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v2 + + - name: Load GPG secrets from 1Password + uses: 1password/load-secrets-action@v3 + with: + export-env: true + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_RO_TOKEN }} + GPG_PRIVATE_KEY: 'op://global/Terraform Provider GPG Key/private-key' + PASSPHRASE: 'op://global/Terraform Provider GPG Key/passphrase' + GPG_FINGERPRINT: 'op://global/Terraform Provider GPG Key/fingerprint' + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + id: import_gpg + with: + gpg_private_key: ${{ env.GPG_PRIVATE_KEY }} + passphrase: ${{ env.PASSPHRASE }} + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/aws-source/module/provider/.github/workflows/tag-on-merge.yml b/aws-source/module/provider/.github/workflows/tag-on-merge.yml new file mode 100644 index 00000000..800ccb96 --- /dev/null +++ b/aws-source/module/provider/.github/workflows/tag-on-merge.yml @@ -0,0 +1,52 @@ +name: Tag Release on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tag-release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'copybara/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Extract version from branch name + id: version + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + VERSION=$(echo "$BRANCH" | sed 's|copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Create and push tag + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + echo "Creating tag: $VERSION" + git tag "$VERSION" + git push origin "$VERSION" + echo "Successfully pushed tag $VERSION" + + - name: Delete copybara branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Deleting branch: $BRANCH" + git push origin --delete "$BRANCH" || echo "Branch may have already been deleted" diff --git a/aws-source/module/provider/.goreleaser.yml b/aws-source/module/provider/.goreleaser.yml new file mode 100644 index 00000000..4aa546e3 --- /dev/null +++ b/aws-source/module/provider/.goreleaser.yml @@ -0,0 +1,56 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +version: 2 + +builds: + - binary: "{{ .ProjectName }}_v{{ .Version }}" + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -s -w -X main.version={{ .Version }} + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - "386" + ignore: + - goos: darwin + goarch: "386" + +archives: + - formats: [zip] + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" + algorithm: sha256 + extra_files: + - glob: terraform-registry-manifest.json + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + +signs: + - artifacts: checksum + args: + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" + +release: + extra_files: + - glob: terraform-registry-manifest.json + name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/aws-source/module/provider/main.go b/aws-source/module/provider/main.go index 66be91c6..c7e80f55 100644 --- a/aws-source/module/provider/main.go +++ b/aws-source/module/provider/main.go @@ -11,11 +11,9 @@ import ( "github.com/uptrace/opentelemetry-go-extra/otellogrus" ) -const ( - version = "0.1.0" +var version = "dev" //nolint:gochecknoglobals // injected by GoReleaser ldflags - defaultHoneycombAPIKey = "hcaik_01j03qe0exnn2jxpj2vxkqb7yrqtr083kyk9rxxt2wzjamz8be94znqmwa" //nolint:gosec // public ingest key, same as CLI -) +const defaultHoneycombAPIKey = "hcaik_01j03qe0exnn2jxpj2vxkqb7yrqtr083kyk9rxxt2wzjamz8be94znqmwa" //nolint:gosec // public ingest key, same as CLI func main() { if err := run(); err != nil { diff --git a/aws-source/module/provider/terraform-registry-manifest.json b/aws-source/module/provider/terraform-registry-manifest.json new file mode 100644 index 00000000..3ffa7837 --- /dev/null +++ b/aws-source/module/provider/terraform-registry-manifest.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "metadata": { + "protocol_versions": [ + "6.0" + ] + } +} diff --git a/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml new file mode 100644 index 00000000..e6c0f5e0 --- /dev/null +++ b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml @@ -0,0 +1,84 @@ +name: Finalize Copybara Sync + +on: + push: + branches: + - 'copybara/v*' + +concurrency: + group: copybara-sync-${{ github.ref }} + cancel-in-progress: true + +jobs: + finalize: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Extract version from branch name + id: version + run: | + VERSION=$(echo "$GITHUB_REF" | sed 's|refs/heads/copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: Extract original commit author + id: author + run: | + AUTHOR_EMAIL=$(git log -1 --format='%ae' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%ae') + AUTHOR_NAME=$(git log -1 --format='%an' --author='^(?!.*actions@github.com)' --perl-regexp 2>/dev/null || git log -1 --format='%an') + echo "email=$AUTHOR_EMAIL" >> $GITHUB_OUTPUT + echo "name=$AUTHOR_NAME" >> $GITHUB_OUTPUT + + if [[ "$AUTHOR_EMAIL" =~ ^([^@]+)@users\.noreply\.github\.com$ ]]; then + GITHUB_USER=$(echo "${BASH_REMATCH[1]}" | sed 's/^[0-9]*+//') + echo "github_user=$GITHUB_USER" >> $GITHUB_OUTPUT + else + echo "github_user=" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_EMAIL: ${{ steps.author.outputs.email }} + GITHUB_USER: ${{ steps.author.outputs.github_user }} + run: | + PR_BODY="## Copybara Sync - Release ${VERSION} + + This PR was automatically created by Copybara, syncing changes from the [overmindtech/workspace](https://github.com/overmindtech/workspace) monorepo. + + **Original author:** ${AUTHOR_NAME} (${AUTHOR_EMAIL}) + + ### What happens when this PR is merged? + + 1. The \`tag-on-merge\` workflow will automatically create the \`${VERSION}\` tag on main + 2. Terraform Registry will detect the tag via webhook and publish the module + + ### Review Checklist + + - [ ] Changes look correct and match the expected monorepo sync + " + + PR_URL=$(gh pr create \ + --base main \ + --head "${{ github.ref_name }}" \ + --title "Release ${VERSION}" \ + --body "$PR_BODY") + + echo "Created PR: $PR_URL" + + if [ -n "$GITHUB_USER" ]; then + echo "Requesting review from original author: $GITHUB_USER" + gh pr edit "$PR_URL" --add-reviewer "$GITHUB_USER" || true + fi + + echo "Requesting review from Engineering team" + gh pr edit "$PR_URL" --add-reviewer "overmindtech/Engineering" || true diff --git a/aws-source/module/terraform/.github/workflows/tag-on-merge.yml b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml new file mode 100644 index 00000000..800ccb96 --- /dev/null +++ b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml @@ -0,0 +1,52 @@ +name: Tag Release on Merge + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + tag-release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'copybara/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Extract version from branch name + id: version + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + VERSION=$(echo "$BRANCH" | sed 's|copybara/||') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.RELEASE_PAT }} + + - name: Configure Git + run: | + git config user.name "GitHub Actions Bot" + git config user.email "actions@github.com" + + - name: Create and push tag + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + echo "Creating tag: $VERSION" + git tag "$VERSION" + git push origin "$VERSION" + echo "Successfully pushed tag $VERSION" + + - name: Delete copybara branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + echo "Deleting branch: $BRANCH" + git push origin --delete "$BRANCH" || echo "Branch may have already been deleted" From 86c7194e492dee9a4bf3f1e50223684fb48759e9 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 19:15:07 +0100 Subject: [PATCH 41/51] Eng 2679 phase 6 documentation for terraform aws source module (#3960) Re-do https://github.com/overmindtech/workspace/pull/3959 as the change was merged out of order and got lost in rebasing; this depends on https://github.com/overmindtech/workspace/pull/3958 getting merged first. --- > [!NOTE] > **Low Risk** > Documentation and workflow tooling changes only; no runtime code paths or security-sensitive logic are modified. > > **Overview** > Updates Terraform AWS source module documentation and publishing references to use the new registry address `overmindtech/aws-source/overmind`, and adds clearer module development/testing guidance. > > Adds customer-facing docs at `docs.overmind.tech/docs/sources/aws/terraform.md`, expands the module README with import instructions, and introduces `aws-source/module/.cursor/BUGBOT.md` review rules to keep IAM policy changes read-only and Terraform provider errors using `diag.Diagnostics`. > > Enhances `.cursor/commands/open-pull-request.md` to capture an approved plan from Linear tickets and require a PR section explicitly documenting *deviations from the approved plan*. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9356970fb0d18ac929d804484030d0cafa6621f5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 1a5dc90ab5d93ecf70bf377cf58172a34f763314 --- aws-source/module/.cursor/BUGBOT.md | 16 ++ aws-source/module/terraform/README.md | 22 ++- .../terraform/examples/multi-account/main.tf | 4 +- .../terraform/examples/single-account/main.tf | 2 +- .../docs/sources/aws/terraform.md | 182 ++++++++++++++++++ 5 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 aws-source/module/.cursor/BUGBOT.md create mode 100644 docs.overmind.tech/docs/sources/aws/terraform.md diff --git a/aws-source/module/.cursor/BUGBOT.md b/aws-source/module/.cursor/BUGBOT.md new file mode 100644 index 00000000..08d7fca8 --- /dev/null +++ b/aws-source/module/.cursor/BUGBOT.md @@ -0,0 +1,16 @@ +# Terraform Module Review Rules + +## HCL: IAM policy must stay read-only + +If any changed `.tf` file modifies an IAM policy statement's `Action` list: + +- Verify every action uses only read-only prefixes: `Get*`, `Describe*`, `List*`, `GetBucket*`, `ListAllMyBuckets`, `ListTagsForResource`, `GetMetricData`. +- Add a blocking Bug titled "IAM policy contains write actions" if any action allows mutation (e.g., `Put*`, `Create*`, `Delete*`, `Update*`, `Attach*`, `Detach*`). +- Body: "The Overmind IAM role must be strictly read-only. Write actions violate customer trust policies and the principle of least privilege. Remove the offending actions." + +## Provider Go: Use diag.Diagnostics for errors + +If any changed `.go` file in `provider/` returns an error from a resource or data source CRUD function using bare `fmt.Errorf` or `errors.New`: + +- Add a warning titled "Use diag.Diagnostics instead of bare errors" +- Body: "Terraform provider resource and data source functions should return errors via `diag.Diagnostics` (e.g., `diag.FromErr(err)`) so that Terraform can display structured error output to users. See the [Terraform Plugin Framework documentation](https://developer.hashicorp.com/terraform/plugin/framework/diagnostics) for guidance." diff --git a/aws-source/module/terraform/README.md b/aws-source/module/terraform/README.md index ab07108f..9d6065f4 100644 --- a/aws-source/module/terraform/README.md +++ b/aws-source/module/terraform/README.md @@ -18,7 +18,7 @@ provider "aws" { } module "overmind_aws_source" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "production" } @@ -41,7 +41,7 @@ module "overmind_aws_source" { | `source_id` | UUID of the Overmind source | | `external_id` | AWS STS external ID used in the trust policy | -## Multi-account example +## Multi-Account Example Use AWS provider aliases to onboard several accounts at once: @@ -61,7 +61,7 @@ provider "aws" { } module "overmind_production" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "production" providers = { @@ -71,7 +71,7 @@ module "overmind_production" { } module "overmind_staging" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "staging" regions = ["eu-west-1"] @@ -82,6 +82,20 @@ module "overmind_staging" { } ``` +## Importing Existing Sources + +If you already created an Overmind AWS source through the UI and want to manage it +with Terraform, you can import it using the source UUID (visible on the source +details page in [Settings > Sources](https://app.overmind.tech/settings/sources)): + +```shell +terraform import module.overmind_aws_source.overmind_aws_source.this +``` + +After importing, run `terraform plan` to verify the state matches your +configuration. Terraform will show any drift between the imported resource and +your HCL. + ## Authentication The Overmind provider reads `OVERMIND_API_KEY` from the environment. The API key diff --git a/aws-source/module/terraform/examples/multi-account/main.tf b/aws-source/module/terraform/examples/multi-account/main.tf index 3bd989e2..af9269f4 100644 --- a/aws-source/module/terraform/examples/multi-account/main.tf +++ b/aws-source/module/terraform/examples/multi-account/main.tf @@ -19,7 +19,7 @@ provider "aws" { } module "overmind_production" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "production" providers = { @@ -29,7 +29,7 @@ module "overmind_production" { } module "overmind_staging" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "staging" regions = ["eu-west-1"] diff --git a/aws-source/module/terraform/examples/single-account/main.tf b/aws-source/module/terraform/examples/single-account/main.tf index 06adf4be..8043364c 100644 --- a/aws-source/module/terraform/examples/single-account/main.tf +++ b/aws-source/module/terraform/examples/single-account/main.tf @@ -5,7 +5,7 @@ provider "aws" { } module "overmind_aws_source" { - source = "overmindtech/aws-source-setup/overmind" + source = "overmindtech/aws-source/overmind" name = "production" } diff --git a/docs.overmind.tech/docs/sources/aws/terraform.md b/docs.overmind.tech/docs/sources/aws/terraform.md new file mode 100644 index 00000000..e976c988 --- /dev/null +++ b/docs.overmind.tech/docs/sources/aws/terraform.md @@ -0,0 +1,182 @@ +--- +title: Configure with Terraform +sidebar_position: 2 +--- + +The [Overmind Terraform module](https://registry.terraform.io/modules/overmindtech/aws-source/overmind) configures an AWS account for Overmind infrastructure discovery in a single `terraform apply`. It creates an IAM role with a read-only policy, sets up the trust relationship, and registers the source with Overmind's API. The module is fully compatible with [OpenTofu](https://opentofu.org/). + +## Prerequisites + +- **Overmind API key** with `sources:write` scope. Create one in [Settings > API Keys](https://app.overmind.tech/settings/api-keys). +- **AWS credentials** with permission to create IAM roles and policies in the target account. +- **Terraform >= 1.5.0** or **OpenTofu >= 1.6.0**. + +## Quick Start + +```hcl +provider "overmind" {} + +provider "aws" { + region = "us-east-1" +} + +module "overmind_aws_source" { + source = "overmindtech/aws-source/overmind" + + name = "production" +} + +output "role_arn" { + value = module.overmind_aws_source.role_arn +} + +output "source_id" { + value = module.overmind_aws_source.source_id +} +``` + +Then run: + +```bash +export OVERMIND_API_KEY="your-api-key" +terraform init +terraform plan +terraform apply +``` + +## Authentication + +### Overmind Provider + +The Overmind provider reads `OVERMIND_API_KEY` from the environment. The API key must have `sources:write` scope. + +You can also set it in the provider block: + +```hcl +provider "overmind" { + api_key = var.overmind_api_key +} +``` + +### AWS Provider + +The AWS provider must have permissions to create IAM roles and policies in the target account. Any standard AWS authentication method works (environment variables, shared credentials file, SSO, etc.). See the [AWS provider documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration) for details. + +## Multi-Account Setup + +Use AWS provider aliases to onboard several accounts at once: + +```hcl +provider "overmind" {} + +provider "aws" { + alias = "production" + region = "us-east-1" + + assume_role { + role_arn = "arn:aws:iam::111111111111:role/terraform" + } +} + +provider "aws" { + alias = "staging" + region = "eu-west-1" + + assume_role { + role_arn = "arn:aws:iam::222222222222:role/terraform" + } +} + +module "overmind_production" { + source = "overmindtech/aws-source/overmind" + name = "production" + + providers = { + aws = aws.production + overmind = overmind + } +} + +module "overmind_staging" { + source = "overmindtech/aws-source/overmind" + name = "staging" + regions = ["eu-west-1"] + + providers = { + aws = aws.staging + overmind = overmind + } +} +``` + +## Inputs + +| Name | Description | Type | Default | Required | +| --- | --- | --- | --- | --- | +| `name` | Descriptive name for the source in Overmind | `string` | n/a | yes | +| `regions` | AWS regions to discover (defaults to all non-opt-in regions) | `list(string)` | All 17 standard regions | no | +| `role_name` | Name for the IAM role created in this account | `string` | `"overmind-read-only"` | no | +| `tags` | Additional tags to apply to IAM resources | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +| --- | --- | +| `role_arn` | ARN of the created IAM role | +| `source_id` | UUID of the Overmind source | +| `external_id` | AWS STS external ID used in the trust policy | + +## Importing Existing Sources + +If you already created an Overmind AWS source through the UI and want to bring it under Terraform management, you can import it using the source UUID. Find the UUID on the source details page in [Settings > Sources](https://app.overmind.tech/settings/sources). + +When using the module: + +```shell +terraform import module.overmind_aws_source.overmind_aws_source.this +``` + +When using the provider resource directly: + +```shell +terraform import overmind_aws_source.example +``` + +After importing, run `terraform plan` to verify the state matches your configuration. Terraform will show any drift between the imported resource and your HCL. + +Note that importing brings only the Overmind source under Terraform management. If the IAM role was also created outside of Terraform, you will need to import it separately with `terraform import aws_iam_role.overmind `. + +## Verify Your Source + +After `terraform apply` completes: + +1. Open [Settings > Sources](https://app.overmind.tech/settings/sources) in the Overmind app. +2. Your new source should appear with a green healthy status within about a minute. +3. Navigate to [Explore](https://app.overmind.tech/explore) to browse discovered resources. + +## Registry Links + +- **Terraform Registry**: [overmindtech/overmind provider](https://registry.terraform.io/providers/overmindtech/overmind/latest) | [overmindtech/aws-source module](https://registry.terraform.io/modules/overmindtech/aws-source/overmind/latest) +- **OpenTofu Registry**: coming soon + +## Troubleshooting + +### "Provider not found" during terraform init + +Ensure you are running Terraform >= 1.5.0 or OpenTofu >= 1.6.0, and that you have internet access to reach the registry. Run `terraform init -upgrade` to refresh provider caches. + +### "Unauthorized" or "invalid API key" + +Verify that `OVERMIND_API_KEY` is set and that the key has `sources:write` scope. You can check your API keys in [Settings > API Keys](https://app.overmind.tech/settings/api-keys). + +### "Access Denied" creating IAM resources + +The AWS credentials used by Terraform need permission to create IAM roles and policies. Verify your credentials have the `iam:CreateRole`, `iam:PutRolePolicy`, and `iam:CreatePolicy` permissions in the target account. + +### Source shows as unhealthy after apply + +The IAM role may take a few seconds to propagate. Wait one to two minutes and refresh the Sources page. If the source remains unhealthy, verify the role ARN in the AWS console matches the `role_arn` output. + +### Destroying resources + +`terraform destroy` cleanly removes both the IAM resources in AWS and the Overmind source registration. From 1aa0cec23ec4271ff2472afa1f019d5aedc098d9 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 20:40:00 +0100 Subject: [PATCH 42/51] Minor fixes (#3961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > **Low Risk** > Mostly administrative/test-stability changes; the only functional impact is allowing Terraform to use AWS provider v5, which could affect users depending on provider features/behavior. > > **Overview** > Adds Functional Source License (FSL 1.1 with Apache 2.0 future license) `LICENSE` files to the AWS provider and Terraform module directories. > > Relaxes the Terraform module’s AWS provider version constraint from `>= 6.0` to `>= 5.0`, and increases `TestCronJobAdapter`’s wait timeout from 60s to 120s to reduce CronJob-related test flakes. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit dcdd0b5c3c96e5b36e65c56be273f3453bf4cd3c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 9039e065ba8843a9059fc64027b21b8f278de48c --- aws-source/module/provider/LICENSE | 105 ++++++++++++++++++++++++ aws-source/module/terraform/LICENSE | 105 ++++++++++++++++++++++++ aws-source/module/terraform/versions.tf | 2 +- k8s-source/adapters/cronjob_test.go | 6 +- 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 aws-source/module/provider/LICENSE create mode 100644 aws-source/module/terraform/LICENSE diff --git a/aws-source/module/provider/LICENSE b/aws-source/module/provider/LICENSE new file mode 100644 index 00000000..bc298f07 --- /dev/null +++ b/aws-source/module/provider/LICENSE @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2024 Overmind Technology Inc. + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/aws-source/module/terraform/LICENSE b/aws-source/module/terraform/LICENSE new file mode 100644 index 00000000..bc298f07 --- /dev/null +++ b/aws-source/module/terraform/LICENSE @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, Apache 2.0 Future License + +## Abbreviation + +FSL-1.1-Apache-2.0 + +## Notice + +Copyright 2024 Overmind Technology Inc. + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/aws-source/module/terraform/versions.tf b/aws-source/module/terraform/versions.tf index e5facdcb..7c2e5efa 100644 --- a/aws-source/module/terraform/versions.tf +++ b/aws-source/module/terraform/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.0" + version = ">= 5.0" } overmind = { source = "overmindtech/overmind" diff --git a/k8s-source/adapters/cronjob_test.go b/k8s-source/adapters/cronjob_test.go index b78fd725..fe86adb6 100644 --- a/k8s-source/adapters/cronjob_test.go +++ b/k8s-source/adapters/cronjob_test.go @@ -50,8 +50,10 @@ func TestCronJobAdapter(t *testing.T) { // created it jobAdapter := newJobAdapter(CurrentCluster.ClientSet, sd.ClusterName, []string{sd.Namespace}, sdpcache.NewNoOpCache()) - // Wait for the job to be created - err := WaitFor(60*time.Second, func() bool { + // Wait for the CronJob controller to spawn a Job. The schedule is + // "* * * * *" (once per minute), so in the worst case we wait just over + // 60 seconds. 120 s gives comfortable headroom and avoids flakes. + err := WaitFor(120*time.Second, func() bool { jobs, err := jobAdapter.List(context.Background(), sd.String(), false) if err != nil { From 7930d69fb81e6100377a9afaef3506ba34fb6b61 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 20 Feb 2026 23:54:02 +0100 Subject: [PATCH 43/51] Test the new terraform module module (and another k8s-source test fix) (#3962) > [!NOTE] > **Medium Risk** > Introduces new Terraform provider/module wiring and secrets that affect source registration and deploy behavior. Test and CI runner changes are low risk but infra changes could fail applies if misconfigured. > > **Overview** > Adds dogfooding support for the `aws-source` Terraform module by introducing the `overmindtech/overmind` provider, a new sensitive `aws_source_api_key` variable (wired through `.env.op`, `op.local.secret`, and devcontainer env passthrough), and a new `module "aws_source"` invocation in `deploy/sources.tf`. > > Updates the aws-source provider release workflow to run on `depot-ubuntu-24.04-8`, and stabilizes `k8s-source` pod adapter tests by waiting (via `WaitFor`) for the bad pod to reach `HEALTH_ERROR` before asserting. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 90a96f885039e3f8d52e822ff2fa46b05de29ec7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 31e247ac69bc27f4489a7cda98bdaf0963dd2788 --- k8s-source/adapters/pods_test.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/k8s-source/adapters/pods_test.go b/k8s-source/adapters/pods_test.go index d229e928..d99e6b81 100644 --- a/k8s-source/adapters/pods_test.go +++ b/k8s-source/adapters/pods_test.go @@ -5,6 +5,7 @@ import ( "fmt" "regexp" "testing" + "time" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" @@ -183,23 +184,33 @@ func TestPodAdapter(t *testing.T) { } st.Execute(t) - // the pods are still running let check their health - // get the bad pod - item, err := adapter.Get(context.Background(), sd.String(), "pod-bad-pod", true) + // Wait for the bad pod's image pull to fail before asserting health. + // The kubelet needs time to attempt the pull and enter + // ErrImagePull / ImagePullBackOff, which surfaces as HEALTH_ERROR. + var badPodItem *sdp.Item + err := WaitFor(60*time.Second, func() bool { + item, err := adapter.Get(context.Background(), sd.String(), "pod-bad-pod", true) + if err != nil { + return false + } + badPodItem = item + return item.GetHealth() == sdp.Health_HEALTH_ERROR + }) if err != nil { - t.Fatal(fmt.Errorf("failed to get pod: %w", err)) - } - if item.GetHealth() != sdp.Health_HEALTH_ERROR { - t.Errorf("expected status to be unhealthy, got %s", item.GetHealth()) + health := sdp.Health_HEALTH_UNKNOWN + if badPodItem != nil { + health = badPodItem.GetHealth() + } + t.Fatalf("expected bad pod health to reach HEALTH_ERROR, still %s after timeout", health) } // get the healthy pod - item, err = adapter.Get(context.Background(), sd.String(), "pod-test-pod", true) + healthyItem, err := adapter.Get(context.Background(), sd.String(), "pod-test-pod", true) if err != nil { t.Fatal(fmt.Errorf("failed to get pod: %w", err)) } - if item.GetHealth() != sdp.Health_HEALTH_OK { - t.Errorf("expected status to be healthy, got %s", item.GetHealth()) + if healthyItem.GetHealth() != sdp.Health_HEALTH_OK { + t.Errorf("expected status to be healthy, got %s", healthyItem.GetHealth()) } } From f711948bc1e33623b60241ebf9baa0cbda6a3b86 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Sat, 21 Feb 2026 10:20:48 +0100 Subject: [PATCH 44/51] [ENG-2687] Make AWS trust policy account ID configurable for internal environments (#3964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix dogfood AWS source startup failure caused by the Terraform module hardcoding the prod AWS account (`942836531449`) in the IAM trust policy, while dogfood source pods run in a different account (`944651592624`) - Add a configurable `overmind_aws_account_id` variable (defaulting to prod) and wire it through the deploy module using `var.target_account` ## Linear Ticket - **Ticket**: [ENG-2687](https://linear.app/overmind/issue/ENG-2687/make-aws-trust-policy-account-id-configurable-for-internal) — Make AWS trust policy account ID configurable for internal environments - **Purpose**: Unblock dogfood AWS source by allowing the trust policy to reference the correct AWS account per environment ## Changes Three files changed, all in Terraform HCL: 1. **`aws-source/module/terraform/variables.tf`** — New `overmind_aws_account_id` variable with default `942836531449` and a description marking it as internal-only 2. **`aws-source/module/terraform/main.tf`** — Both `Principal` fields in the trust policy now use `var.overmind_aws_account_id` instead of the hardcoded account ID 3. **`deploy/sources.tf`** — The `aws_source` module block passes `overmind_aws_account_id = var.target_account`, which is `942836531449` for prod and `944651592624` for dogfood ## Deviations from Approved Plan Implementation matches the approved plan — no material deviations. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Changes the IAM role trust policy principal, so a misconfigured `overmind_aws_account_id` could unintentionally allow or block cross-account assume-role access; default preserves current behavior. > > **Overview** > Makes the AWS source Terraform module’s IAM role trust policy principal configurable by replacing the hardcoded Overmind AWS account ID with a new `overmind_aws_account_id` variable (defaulting to the current prod account). > > Wires `deploy/sources.tf` to pass `overmind_aws_account_id = var.target_account` for internal environments, unblocking non-prod deployments where source pods run in a different AWS account. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a2d8557e2c15f145864ca34670b47b45788ba8f7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor GitOrigin-RevId: 77a572596205e75e7f6cbae84ce7057287834ff5 --- aws-source/module/terraform/main.tf | 4 ++-- aws-source/module/terraform/variables.tf | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/aws-source/module/terraform/main.tf b/aws-source/module/terraform/main.tf index 7e90bf3e..871bcda5 100644 --- a/aws-source/module/terraform/main.tf +++ b/aws-source/module/terraform/main.tf @@ -8,7 +8,7 @@ resource "aws_iam_role" "overmind" { Statement = [ { Effect = "Allow" - Principal = { AWS = "arn:aws:iam::942836531449:root" } + Principal = { AWS = "arn:aws:iam::${var.overmind_aws_account_id}:root" } Action = "sts:AssumeRole" Condition = { StringEquals = { @@ -18,7 +18,7 @@ resource "aws_iam_role" "overmind" { }, { Effect = "Allow" - Principal = { AWS = "arn:aws:iam::942836531449:root" } + Principal = { AWS = "arn:aws:iam::${var.overmind_aws_account_id}:root" } Action = "sts:TagSession" }, ] diff --git a/aws-source/module/terraform/variables.tf b/aws-source/module/terraform/variables.tf index 46e385b0..e002c8d7 100644 --- a/aws-source/module/terraform/variables.tf +++ b/aws-source/module/terraform/variables.tf @@ -38,3 +38,9 @@ variable "tags" { default = {} description = "Additional tags to apply to IAM resources." } + +variable "overmind_aws_account_id" { + type = string + default = "942836531449" + description = "Internal override for the Overmind AWS account that runs source pods. Do not change this unless you are an Overmind engineer deploying to a non-production environment. All customers should use the default." +} From ca8c1b3db900c1c495d8bf61e697bfc645e1c413 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Sat, 21 Feb 2026 10:21:00 +0100 Subject: [PATCH 45/51] [ENG-2684] Fix Terraform provider aws-regions serialization breaking frontend (#3963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - The Terraform provider serialized `aws-regions` as a comma-separated string, but the frontend Zod schema expects a JSON array, causing "Invalid source data" in the UI for Terraform-created sources. - Fixed Create, Update, and Read paths in the provider to use proper array serialization via `structpb.ListValue`. - Intentionally omitted legacy CSV fallback in Read — existing sources will self-heal on the next `terraform apply`. ## Linear Ticket - **Ticket**: [ENG-2684](https://linear.app/overmind/issue/ENG-2684/fix-terraform-provider-aws-regions-serialization-breaking-frontend) — Fix Terraform provider aws-regions serialization breaking frontend - **Project**: Terraform Module for AWS Source Setup ## Changes **`aws-source/module/provider/resource_aws_source.go`** (core fix): - **Create & Update**: Replaced `strings.Join(regions, ",")` with `toAnySlice(regions)` so `structpb.NewStruct` produces a `ListValue` instead of a `StringValue`. - **Read**: Replaced string-based parsing with `regionsFromStructValue()` which only reads from `ListValue`. No legacy CSV fallback — this forces Terraform to detect drift on existing sources with the old format. Returns an empty slice (not nil) when the value isn't a list, so `ListValueFrom` produces a non-null empty list — correct for a `Required` schema attribute. - **Helpers**: Added `toAnySlice` (converts `[]string` to `[]any`) and `regionsFromStructValue` (extracts regions from protobuf `ListValue`). Removed unused `splitNonEmpty` and `strings` import. **`aws-source/module/provider/.github/workflows/release.yml`**: Minor release pipeline improvement. **`deploy/.terraform.lock.hcl`**: Updated lock file with new overmind provider hash. **`aws-source/README.md`** and **`aws-source/module/terraform/README.md`**: Documented `api_key` provider-block attribute for authentication. All existing provider tests pass without modification. ## Deviations from Approved Plan The plan in ENG-2684 described four changes, all scoped to `resource_aws_source.go`. The implementation includes those four items plus: 1. **Empty slice instead of nil on parse failure** — not in the plan. `regionsFromStructValue` returns `[]string{}` instead of `nil` when the stored value isn't a `ListValue`. This prevents `ListValueFrom` from producing a null list for a `Required` attribute, which could break refresh in future Terraform framework versions. Drift-based self-healing is preserved since an empty list still differs from the configured regions. 2. **`release.yml` pipeline tweak** — not in the plan. Minor CI change to the provider release workflow (1 line). Low risk, bundled for convenience. 3. **`deploy/.terraform.lock.hcl` update** — not in the plan. Updates the lock file to include the new provider version hash. Required for `deploy/` to use the updated provider. 4. **Documentation updates to `aws-source/README.md` and `aws-source/module/terraform/README.md`** — not in the plan. Adds documentation for the `api_key` provider-block attribute. Docs-only, no behavioral change. No planned items were omitted or modified. The core fix (items 1–4 in the plan) matches the approved approach exactly. GitOrigin-RevId: 67c7387e75d8b85bc14095b51d90215ba042da1f --- .../provider/.github/workflows/release.yml | 2 +- .../module/provider/resource_aws_source.go | 35 ++++++++++++------- aws-source/module/terraform/README.md | 9 ++++- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/aws-source/module/provider/.github/workflows/release.yml b/aws-source/module/provider/.github/workflows/release.yml index ff0ca190..130f5746 100644 --- a/aws-source/module/provider/.github/workflows/release.yml +++ b/aws-source/module/provider/.github/workflows/release.yml @@ -10,7 +10,7 @@ permissions: jobs: release: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-8 steps: - name: Checkout uses: actions/checkout@v6 diff --git a/aws-source/module/provider/resource_aws_source.go b/aws-source/module/provider/resource_aws_source.go index 86d5b9c3..a7d80e24 100644 --- a/aws-source/module/provider/resource_aws_source.go +++ b/aws-source/module/provider/resource_aws_source.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "strings" "connectrpc.com/connect" "github.com/google/uuid" @@ -130,7 +129,7 @@ func (r *awsSourceResource) Create(ctx context.Context, req resource.CreateReque "aws-access-strategy": "external-id", "aws-external-id": externalID, "aws-target-role-arn": plan.AWSRoleARN.ValueString(), - "aws-regions": strings.Join(regions, ","), + "aws-regions": toAnySlice(regions), }) if err != nil { resp.Diagnostics.AddError("Failed to build source config", err.Error()) @@ -216,8 +215,7 @@ func (r *awsSourceResource) Read(ctx context.Context, req resource.ReadRequest, state.AWSRoleARN = types.StringValue(v.GetStringValue()) } if v, ok := fields["aws-regions"]; ok { - regionStr := v.GetStringValue() - regionVals := splitNonEmpty(regionStr, ",") + regionVals := regionsFromStructValue(v) listVal, diags := types.ListValueFrom(ctx, types.StringType, regionVals) resp.Diagnostics.Append(diags...) state.AWSRegions = listVal @@ -271,7 +269,7 @@ func (r *awsSourceResource) Update(ctx context.Context, req resource.UpdateReque "aws-access-strategy": "external-id", "aws-external-id": externalID, "aws-target-role-arn": plan.AWSRoleARN.ValueString(), - "aws-regions": strings.Join(regions, ","), + "aws-regions": toAnySlice(regions), }) if err != nil { resp.Diagnostics.AddError("Failed to build source config", err.Error()) @@ -361,14 +359,25 @@ func regionsFromList(ctx context.Context, list types.List) ([]string, diag.Diagn return regions, diags } -func splitNonEmpty(s, sep string) []string { - parts := strings.Split(s, sep) - result := make([]string, 0, len(parts)) - for _, p := range parts { - trimmed := strings.TrimSpace(p) - if trimmed != "" { - result = append(result, trimmed) +func toAnySlice(ss []string) []any { + out := make([]any, len(ss)) + for i, s := range ss { + out[i] = s + } + return out +} + +func regionsFromStructValue(v *structpb.Value) []string { + lv := v.GetListValue() + if lv == nil { + return []string{} + } + vals := lv.GetValues() + out := make([]string, 0, len(vals)) + for _, item := range vals { + if s := item.GetStringValue(); s != "" { + out = append(out, s) } } - return result + return out } diff --git a/aws-source/module/terraform/README.md b/aws-source/module/terraform/README.md index 9d6065f4..65bb91af 100644 --- a/aws-source/module/terraform/README.md +++ b/aws-source/module/terraform/README.md @@ -98,9 +98,16 @@ your HCL. ## Authentication -The Overmind provider reads `OVERMIND_API_KEY` from the environment. The API key +The Overmind provider accepts an API key via the `api_key` attribute or the +`OVERMIND_API_KEY` environment variable. The attribute takes precedence. The key must have `sources:write` scope. +```hcl +provider "overmind" { + api_key = var.overmind_api_key +} +``` + The AWS provider must have permissions to create IAM roles and policies in the target account. From 54bc473d74e5593e6481ee0ad5d06699c87fdaba Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Mon, 23 Feb 2026 10:28:29 +0100 Subject: [PATCH 46/51] Blast propagation cleanup (#3940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all code references to `BlastPropagation` and `followOnlyBlastPropagation` as they are no longer used for blast radius calculation. These fields were previously used for hardcoded blast radius propagation in adapters but are now obsolete due to the adoption of an AI-driven approach for blast radius calculation. --- Linear Issue: [ENG-2647](https://linear.app/overmind/issue/ENG-2647/remove-all-code-references-to-blastpropagation-and)

Open in Cursor Open in Web

--- > [!NOTE] > **Medium Risk** > Large mechanical change across many adapters; while mostly field removals, it can alter relationship semantics if any runtime logic still depended on `BlastPropagation` being present. > > **Overview** > Removes `BlastPropagation` (and related guidance) from linked item query construction, reflecting that blast radius is no longer hardcoded in adapters. > > Updates internal Cursor docs/rules for Azure and GCP to drop `BlastPropagation` sections and examples, and strips `BlastPropagation` assignments from a wide set of AWS adapters’ `sdp.LinkedItemQuery` links (API Gateway, EC2, ECS/EKS, CloudFront, DirectConnect, etc.), leaving only the underlying `Query` definitions. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d8a16a43682a8a90b2bf5be2324ad7415e272357. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: 058630c9b24fe6f4cc01905702fc73345203b4ea --- aws-source/adapters/apigateway-api-key.go | 5 - aws-source/adapters/apigateway-authorizer.go | 5 - aws-source/adapters/apigateway-deployment.go | 17 -- aws-source/adapters/apigateway-domain-name.go | 32 ---- aws-source/adapters/apigateway-integration.go | 11 -- .../adapters/apigateway-method-response.go | 5 - aws-source/adapters/apigateway-method.go | 22 --- aws-source/adapters/apigateway-model.go | 5 - aws-source/adapters/apigateway-resource.go | 4 - aws-source/adapters/apigateway-rest-api.go | 37 ---- aws-source/adapters/apigateway-stage.go | 11 -- .../autoscaling-auto-scaling-group.go | 50 ----- .../autoscaling-auto-scaling-policy.go | 28 --- ...cloudfront-continuous-deployment-policy.go | 5 - .../adapters/cloudfront-distribution.go | 174 ------------------ .../cloudfront-realtime-log-config.go | 12 -- .../cloudfront-streaming-distribution.go | 26 --- aws-source/adapters/cloudwatch-alarm.go | 30 --- .../adapters/directconnect-connection.go | 24 --- ...ct-connect-gateway-association-proposal.go | 7 - ...nect-direct-connect-gateway-association.go | 12 -- ...nnect-direct-connect-gateway-attachment.go | 13 -- .../directconnect-hosted-connection.go | 24 --- .../adapters/directconnect-interconnect.go | 24 --- aws-source/adapters/directconnect-lag.go | 18 -- .../directconnect-router-configuration.go | 5 - .../directconnect-virtual-interface.go | 36 ---- aws-source/adapters/dynamodb-backup.go | 8 - aws-source/adapters/dynamodb-table.go | 30 --- .../ec2-capacity-reservation-fleet.go | 6 - .../adapters/ec2-capacity-reservation.go | 18 -- .../ec2-egress-only-internet-gateway.go | 6 - .../ec2-iam-instance-profile-association.go | 12 -- .../adapters/ec2-instance-event-window.go | 12 -- aws-source/adapters/ec2-instance-status.go | 6 - aws-source/adapters/ec2-instance.go | 129 ------------- aws-source/adapters/ec2-internet-gateway.go | 6 - .../adapters/ec2-launch-template-version.go | 77 -------- aws-source/adapters/ec2-nat-gateway.go | 29 --- aws-source/adapters/ec2-network-acl.go | 12 -- .../ec2-network-interface-permission.go | 5 - aws-source/adapters/ec2-network-interface.go | 60 ------ aws-source/adapters/ec2-route-table.go | 56 ------ .../adapters/ec2-security-group-rule.go | 10 - aws-source/adapters/ec2-security-group.go | 18 -- aws-source/adapters/ec2-snapshot.go | 8 - aws-source/adapters/ec2-subnet.go | 6 - ...transit-gateway-route-table-association.go | 6 - ...transit-gateway-route-table-propagation.go | 7 - .../ec2-transit-gateway-route-table.go | 5 - .../adapters/ec2-transit-gateway-route.go | 9 - aws-source/adapters/ec2-volume-status.go | 10 - aws-source/adapters/ec2-volume.go | 5 - aws-source/adapters/ec2-vpc-endpoint.go | 35 ---- .../adapters/ec2-vpc-peering-connection.go | 12 -- aws-source/adapters/ecs-capacity-provider.go | 5 - aws-source/adapters/ecs-cluster.go | 40 ---- aws-source/adapters/ecs-container-instance.go | 5 - aws-source/adapters/ecs-service.go | 73 -------- aws-source/adapters/ecs-task-definition.go | 24 --- aws-source/adapters/ecs-task.go | 33 ---- aws-source/adapters/efs-access-point.go | 5 - aws-source/adapters/efs-file-system.go | 18 -- aws-source/adapters/efs-mount-target.go | 22 --- .../adapters/efs-replication-configuration.go | 12 -- aws-source/adapters/eks-cluster.go | 62 ------- aws-source/adapters/eks-fargate-profile.go | 12 -- aws-source/adapters/eks-nodegroup.go | 35 ---- aws-source/adapters/elb-instance-health.go | 5 - aws-source/adapters/elbv2-listener.go | 16 -- aws-source/adapters/elbv2-load-balancer.go | 66 ------- aws-source/adapters/elbv2-rule.go | 5 - aws-source/adapters/elbv2-target-group.go | 16 -- aws-source/adapters/elbv2-target-health.go | 17 -- aws-source/adapters/elbv2.go | 40 ---- aws-source/adapters/iam-instance-profile.go | 12 -- aws-source/adapters/iam-policy.go | 18 -- aws-source/adapters/iam-role.go | 6 - aws-source/adapters/iam-user.go | 6 - aws-source/adapters/iam.go | 15 -- aws-source/adapters/kms-alias.go | 6 - aws-source/adapters/kms-custom-key-store.go | 12 -- aws-source/adapters/kms-grant.go | 13 -- aws-source/adapters/kms-key-policy.go | 5 - aws-source/adapters/kms-key.go | 16 -- .../adapters/lambda-event-source-mapping.go | 11 -- aws-source/adapters/lambda-function.go | 112 ----------- aws-source/adapters/lambda-layer-version.go | 12 -- aws-source/adapters/lambda-layer.go | 5 - .../network-firewall-firewall-policy.go | 8 - .../adapters/network-firewall-firewall.go | 36 ---- .../adapters/network-firewall-rule-group.go | 8 - ...k-firewall-tls-inspection-configuration.go | 16 -- aws-source/adapters/networkfirewall.go | 8 - .../networkmanager-connect-attachment.go | 8 - ...networkmanager-connect-peer-association.go | 16 -- .../adapters/networkmanager-connect-peer.go | 36 ---- .../adapters/networkmanager-connection.go | 20 -- .../networkmanager-core-network-policy.go | 4 - .../adapters/networkmanager-core-network.go | 16 -- aws-source/adapters/networkmanager-device.go | 20 -- .../adapters/networkmanager-global-network.go | 36 ---- .../networkmanager-link-association.go | 12 -- aws-source/adapters/networkmanager-link.go | 16 -- ...rkmanager-network-resource-relationship.go | 52 ------ ...workmanager-site-to-site-vpn-attachment.go | 8 - aws-source/adapters/networkmanager-site.go | 12 -- ...ransit-gateway-connect-peer-association.go | 12 -- .../networkmanager-transit-gateway-peering.go | 12 -- ...orkmanager-transit-gateway-registration.go | 8 - ...-transit-gateway-route-table-attachment.go | 12 -- .../adapters/networkmanager-vpc-attachment.go | 4 - aws-source/adapters/rds-db-cluster.go | 84 --------- aws-source/adapters/rds-db-instance.go | 107 ----------- aws-source/adapters/rds-db-subnet-group.go | 18 -- aws-source/adapters/route53-health-check.go | 5 - aws-source/adapters/route53-hosted-zone.go | 6 - .../adapters/route53-resource-record-set.go | 19 -- aws-source/adapters/s3.go | 35 ---- .../adapters/sns-data-protection-policy.go | 8 - .../adapters/sns-platform-application.go | 6 - aws-source/adapters/sns-subscription.go | 12 -- aws-source/adapters/sns-topic.go | 6 - aws-source/adapters/sqs-queue.go | 10 - aws-source/adapters/sqs-queue_test.go | 14 -- cmd/request_query.go | 4 +- go/discovery/enginerequests.go | 1 - go/sdp-go/items.go | 7 +- go/sdp-go/link_extract.go | 21 --- go/sdp-go/progress.go | 10 +- go/sdp-go/proto_clone_test.go | 16 +- go/sdpcache/item_generator_test.go | 3 +- k8s-source/adapters/clusterrolebinding.go | 13 -- k8s-source/adapters/deployment.go | 5 - k8s-source/adapters/endpoints.go | 22 +-- k8s-source/adapters/endpointslice.go | 27 +-- k8s-source/adapters/generic_source.go | 14 +- k8s-source/adapters/generic_source_test.go | 10 +- .../adapters/horizontalpodautoscaler.go | 6 - k8s-source/adapters/ingress.go | 39 ---- k8s-source/adapters/job.go | 6 - k8s-source/adapters/networkpolicy.go | 14 -- k8s-source/adapters/node.go | 16 -- k8s-source/adapters/persistentvolume.go | 25 +-- k8s-source/adapters/persistentvolumeclaim.go | 7 - k8s-source/adapters/poddisruptionbudget.go | 6 - k8s-source/adapters/pods.go | 97 ---------- k8s-source/adapters/replicaset.go | 6 - k8s-source/adapters/replicationcontroller.go | 6 - k8s-source/adapters/rolebinding.go | 14 -- k8s-source/adapters/service.go | 32 ---- k8s-source/adapters/serviceaccount.go | 14 -- k8s-source/adapters/statefulset.go | 12 -- k8s-source/adapters/volumeattachment.go | 11 -- .../manual/authorization-role-assignment.go | 12 -- sources/azure/manual/batch-batch-accounts.go | 87 --------- .../azure/manual/compute-availability-set.go | 8 - .../compute-capacity-reservation-group.go | 10 - .../manual/compute-dedicated-host-group.go | 5 - sources/azure/manual/compute-disk-access.go | 8 - .../manual/compute-disk-encryption-set.go | 30 --- sources/azure/manual/compute-disk.go | 82 --------- sources/azure/manual/compute-disk_test.go | 6 - .../compute-gallery-application-version.go | 28 --- sources/azure/manual/compute-gallery-image.go | 4 - sources/azure/manual/compute-image.go | 54 ------ .../compute-proximity-placement-group.go | 12 -- .../manual/compute-shared-gallery-image.go | 4 - sources/azure/manual/compute-snapshot.go | 96 ---------- .../compute-virtual-machine-extension.go | 21 --- .../compute-virtual-machine-extension_test.go | 9 - .../compute-virtual-machine-run-command.go | 24 --- ...ompute-virtual-machine-run-command_test.go | 6 - .../compute-virtual-machine-scale-set.go | 117 ------------ .../azure/manual/compute-virtual-machine.go | 88 --------- .../azure/manual/dbforpostgresql-database.go | 4 - .../manual/dbforpostgresql-flexible-server.go | 116 ------------ .../dbforpostgresql-flexible-server_test.go | 30 --- sources/azure/manual/dns_links.go | 1 - .../manual/documentdb-database-accounts.go | 20 -- sources/azure/manual/keyvault-managed-hsm.go | 33 ---- sources/azure/manual/keyvault-secret.go | 20 -- sources/azure/manual/keyvault-vault.go | 22 --- sources/azure/manual/links_helpers.go | 4 - .../managedidentity-user-assigned-identity.go | 6 - .../manual/network-application-gateway.go | 128 ------------- sources/azure/manual/network-load-balancer.go | 73 -------- .../azure/manual/network-network-interface.go | 72 -------- .../manual/network-network-security-group.go | 36 ---- .../azure/manual/network-public-ip-address.go | 38 ---- .../manual/network-public-ip-address_test.go | 6 - sources/azure/manual/network-route-table.go | 13 -- .../azure/manual/network-virtual-network.go | 36 ---- sources/azure/manual/network-zone.go | 28 --- sources/azure/manual/sql-database.go | 56 ------ sources/azure/manual/sql-server.go | 128 ------------- sources/azure/manual/sql-server_test.go | 8 - sources/azure/manual/storage-account.go | 77 -------- .../azure/manual/storage-blob-container.go | 16 -- .../manual/storage-blob-container_test.go | 12 -- sources/azure/manual/storage-fileshare.go | 6 - .../azure/manual/storage-fileshare_test.go | 6 - sources/azure/manual/storage-queues.go | 4 - sources/azure/manual/storage-queues_test.go | 6 - sources/azure/manual/storage-table.go | 4 - sources/azure/manual/storage-table_test.go | 6 - sources/example/custom_searchable_listable.go | 4 - .../example/standard_searchable_listable.go | 4 - .../manual/certificate-manager-certificate.go | 26 --- .../gcp/manual/storage-bucket-iam-policy.go | 20 -- 210 files changed, 18 insertions(+), 4941 deletions(-) diff --git a/aws-source/adapters/apigateway-api-key.go b/aws-source/adapters/apigateway-api-key.go index 5efd2493..2c4659b0 100644 --- a/aws-source/adapters/apigateway-api-key.go +++ b/aws-source/adapters/apigateway-api-key.go @@ -64,11 +64,6 @@ func apiKeyOutputMapper(scope string, awsItem *types.ApiKey) (*sdp.Item, error) Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate both ways - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-authorizer.go b/aws-source/adapters/apigateway-authorizer.go index f7d99eeb..3653cce1 100644 --- a/aws-source/adapters/apigateway-authorizer.go +++ b/aws-source/adapters/apigateway-authorizer.go @@ -48,11 +48,6 @@ func authorizerOutputMapper(query, scope string, awsItem *types.Authorizer) (*sd Query: strings.Split(query, "/")[0], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-deployment.go b/aws-source/adapters/apigateway-deployment.go index f0dcb3c5..29ce47a2 100644 --- a/aws-source/adapters/apigateway-deployment.go +++ b/aws-source/adapters/apigateway-deployment.go @@ -44,11 +44,6 @@ func deploymentOutputMapper(query, scope string, awsItem *types.Deployment) (*sd Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -58,18 +53,6 @@ func deploymentOutputMapper(query, scope string, awsItem *types.Deployment) (*sd Query: fmt.Sprintf("%s/%s", restAPIID, *awsItem.Id), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - /* - If an aws_api_gateway_deployment is deleted, - any stage that references this deployment will be affected - because the stage will no longer have a valid deployment to point to. - However, if an aws_api_gateway_stage is deleted, - it does not affect the aws_api_gateway_deployment itself, - but it will remove the specific environment where the deployment was available. - */ - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-domain-name.go b/aws-source/adapters/apigateway-domain-name.go index c96883a4..97cc6c7a 100644 --- a/aws-source/adapters/apigateway-domain-name.go +++ b/aws-source/adapters/apigateway-domain-name.go @@ -74,12 +74,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the domain name - In: true, - // The domain name won't affect the hosted zone - Out: false, - }, }) } @@ -92,12 +86,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.DistributionHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the domain name - In: true, - // The domain name won't affect the hosted zone - Out: false, - }, }) } @@ -111,11 +99,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -130,11 +113,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalCertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -148,11 +126,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.RegionalDomainName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } @@ -166,11 +139,6 @@ func domainNameOutputMapper(_, scope string, awsItem *types.DomainName) (*sdp.It Query: *awsItem.OwnershipVerificationCertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-integration.go b/aws-source/adapters/apigateway-integration.go index 4424306b..a973f6c2 100644 --- a/aws-source/adapters/apigateway-integration.go +++ b/aws-source/adapters/apigateway-integration.go @@ -61,11 +61,6 @@ func apiGatewayIntegrationGetFunc(ctx context.Context, client apiGatewayIntegrat Query: fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) if output.ConnectionId != nil { @@ -76,12 +71,6 @@ func apiGatewayIntegrationGetFunc(ctx context.Context, client apiGatewayIntegrat Query: *output.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If VPC link goes away, so does the integration - In: true, - // If integration goes away, VPC link is still there - Out: false, - }, }) } diff --git a/aws-source/adapters/apigateway-method-response.go b/aws-source/adapters/apigateway-method-response.go index 9d3609d3..7c8f185b 100644 --- a/aws-source/adapters/apigateway-method-response.go +++ b/aws-source/adapters/apigateway-method-response.go @@ -56,11 +56,6 @@ func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClien Query: fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/apigateway-method.go b/aws-source/adapters/apigateway-method.go index bee6dbd0..8c2a7658 100644 --- a/aws-source/adapters/apigateway-method.go +++ b/aws-source/adapters/apigateway-method.go @@ -61,11 +61,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: methodID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } @@ -77,12 +72,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.AuthorizerId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting authorizer will affect the method - In: true, - // Deleting method won't affect the authorizer - Out: false, - }, }) } @@ -94,12 +83,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.RequestValidatorId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting request validator will affect the method - In: true, - // Deleting method won't affect the request validator - Out: false, - }, }) } @@ -112,11 +95,6 @@ func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope Query: fmt.Sprintf("%s/%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod, statusCode), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-model.go b/aws-source/adapters/apigateway-model.go index 51093764..ddb710a0 100644 --- a/aws-source/adapters/apigateway-model.go +++ b/aws-source/adapters/apigateway-model.go @@ -49,11 +49,6 @@ func modelOutputMapper(query, scope string, awsItem *types.Model) (*sdp.Item, er Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-resource.go b/aws-source/adapters/apigateway-resource.go index e0dc22d8..f4a453ce 100644 --- a/aws-source/adapters/apigateway-resource.go +++ b/aws-source/adapters/apigateway-resource.go @@ -65,10 +65,6 @@ func resourceOutputMapper(query, scope string, awsItem *types.Resource) (*sdp.It Query: fmt.Sprintf("%s/%s/%s", restApiID, *awsItem.Id, methodString), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/apigateway-rest-api.go b/aws-source/adapters/apigateway-rest-api.go index ac7bf41d..b7f8fe64 100644 --- a/aws-source/adapters/apigateway-rest-api.go +++ b/aws-source/adapters/apigateway-rest-api.go @@ -98,12 +98,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: vpcEndpointID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Any change on the VPC endpoint should affect the REST API - In: true, - // We can't affect the VPC endpoint - Out: false, - }, }) } } @@ -116,11 +110,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: fmt.Sprintf("%s/%s", *awsItem.Id, *awsItem.RootResourceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } @@ -131,12 +120,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Updating a resource won't affect the REST API - In: false, - // Updating the REST API will affect the resources - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -146,11 +129,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -160,11 +138,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: false, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -174,11 +147,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -188,11 +156,6 @@ func restApiOutputMapper(scope string, awsItem *types.RestApi) (*sdp.Item, error Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/apigateway-stage.go b/aws-source/adapters/apigateway-stage.go index c2fae5d8..52309923 100644 --- a/aws-source/adapters/apigateway-stage.go +++ b/aws-source/adapters/apigateway-stage.go @@ -65,12 +65,6 @@ func stageOutputMapper(query, scope string, awsItem *types.Stage) (*sdp.Item, er Query: fmt.Sprintf("%s/%s", restAPIID, *awsItem.DeploymentId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a deployment will impact the stage - In: true, - // Deleting a stage won't impact the deployment - Out: false, - }, }) } @@ -81,11 +75,6 @@ func stageOutputMapper(query, scope string, awsItem *types.Stage) (*sdp.Item, er Query: restAPIID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled, so we need to propagate the blast to the linked item - In: true, - Out: true, - }, }) return &item, nil diff --git a/aws-source/adapters/autoscaling-auto-scaling-group.go b/aws-source/adapters/autoscaling-auto-scaling-group.go index 68eac0b9..9601ab14 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group.go @@ -52,12 +52,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -76,12 +70,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: tgARN, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a target group won't affect the ASG - In: false, - // Changes to an ASG will affect the target group - Out: true, - }, }) } } @@ -95,14 +83,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to an instance could affect the ASG since it - // could cause it to scale - In: true, - // Changes to an ASG can definitely affect an instance - // since it might be terminated - Out: true, - }, }) } @@ -115,12 +95,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *instance.LaunchTemplate.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -135,13 +109,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.ServiceLinkedRoleARN, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a role can affect the functioning of the - // ASG - In: true, - // ASG changes wont affect the role though - Out: false, - }, }) } } @@ -154,11 +121,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.LaunchConfigurationName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Very tightly coupled - In: true, - Out: true, - }, }) } @@ -171,12 +133,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.LaunchTemplate.LaunchTemplateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a launch template will affect the ASG - In: true, - // Changes to an ASG won't affect the template - Out: false, - }, }) } } @@ -189,12 +145,6 @@ func autoScalingGroupOutputMapper(_ context.Context, _ *autoscaling.Client, scop Query: *asg.PlacementGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a placement group can affect the ASG - In: true, - // Changes to an ASG can affect the placement group - Out: true, - }, }) } diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy.go b/aws-source/adapters/autoscaling-auto-scaling-policy.go index 16954579..5f60fadb 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy.go @@ -51,13 +51,6 @@ func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope s Query: *policy.AutoScalingGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the ASG can affect the policy (e.g., if the - // ASG is deleted, the policy is also deleted) - In: true, - // Changes to the policy can affect the ASG behavior - Out: true, - }, }) // Link to CloudWatch Alarms @@ -70,13 +63,6 @@ func scalingPolicyOutputMapper(_ context.Context, _ *autoscaling.Client, scope s Query: *alarm.AlarmName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Alarms trigger policies, so changes to alarms can - // affect policy execution - In: true, - // Changes to the policy don't affect the alarm itself - Out: false, - }, }) } } @@ -146,13 +132,6 @@ func parseResourceLabelLinks(resourceLabel string, scope string) []*sdp.LinkedIt Query: sections[1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the load balancer can affect the scaling policy - // (e.g., if the LB is deleted or modified) - In: true, - // The scaling policy doesn't directly affect the load balancer - Out: false, - }, }) } @@ -166,13 +145,6 @@ func parseResourceLabelLinks(resourceLabel string, scope string) []*sdp.LinkedIt Query: sections[i+1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the target group can affect the scaling policy - // (e.g., request count metrics come from the target group) - In: true, - // The scaling policy doesn't directly affect the target group - Out: false, - }, }) break } diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy.go b/aws-source/adapters/cloudfront-continuous-deployment-policy.go index 2895ef10..15546a8b 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy.go @@ -33,11 +33,6 @@ func continuousDeploymentPolicyItemMapper(_, scope string, awsItem *types.Contin Query: name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/cloudfront-distribution.go b/aws-source/adapters/cloudfront-distribution.go index a21d5c6b..fd7d1025 100644 --- a/aws-source/adapters/cloudfront-distribution.go +++ b/aws-source/adapters/cloudfront-distribution.go @@ -72,11 +72,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *d.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } @@ -90,12 +85,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *keyGroup.KeyGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The distribution won't affect the key group - Out: false, - // The key group could affect the distribution - In: true, - }, }) } } @@ -110,11 +99,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *record.CNAME, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -129,11 +113,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: alias, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -146,11 +125,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ContinuousDeploymentPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - Out: true, - In: true, - }, }) } @@ -164,12 +138,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.CachePolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -181,12 +149,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.FieldLevelEncryptionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the encryption will affect the distribution - In: true, - // The distribution won't affect the encryption - Out: false, - }, }) } @@ -198,12 +160,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.OriginRequestPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -216,12 +172,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.RealtimeLogConfigArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config will affect the distribution - In: true, - // The distribution won't affect the config - Out: false, - }, }) } } @@ -234,12 +184,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *behavior.ResponseHeadersPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -252,12 +196,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key group will affect the distribution - In: true, - // The distribution won't affect the key group - Out: false, - }, }) } } @@ -273,12 +211,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.FunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -295,12 +227,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.LambdaFunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -318,11 +244,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -334,12 +255,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.OriginAccessControlId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } @@ -357,12 +272,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: matches[1], Scope: FormatScope(scope, ""), // S3 buckets are global }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the bucket could affect the distribution - In: true, - // The distribution could affect the bucket - Out: true, - }, }) } } @@ -375,12 +284,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *origin.S3OriginConfig.OriginAccessIdentity, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } } @@ -396,12 +299,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.CachePolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -413,12 +310,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.FieldLevelEncryptionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the encryption will affect the distribution - In: true, - // The distribution won't affect the encryption - Out: false, - }, }) } @@ -430,12 +321,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.OriginRequestPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -448,12 +333,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.RealtimeLogConfigArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config will affect the distribution - In: true, - // The distribution won't affect the config - Out: false, - }, }) } } @@ -466,12 +345,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.DefaultCacheBehavior.ResponseHeadersPolicyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the distribution - In: true, - // The distribution won't affect the policy - Out: false, - }, }) } @@ -484,12 +357,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key group will affect the distribution - In: true, - // The distribution won't affect the key group - Out: false, - }, }) } } @@ -505,12 +372,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.FunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -527,12 +388,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *function.LambdaFunctionARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the function could affect the distribution - In: true, - // The distribution could affect the function - Out: true, - }, }) } } @@ -547,11 +402,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.Logging.Bucket, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -565,12 +415,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ViewerCertificate.ACMCertificateArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the certificate could affect the distribution - In: true, - // The distribution could not affect the certificate - Out: false, - }, }) } } @@ -582,12 +426,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.ViewerCertificate.IAMCertificateId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the certificate could affect the distribution - In: true, - // The distribution could not affect the certificate - Out: false, - }, }) } } @@ -601,12 +439,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.WebACLId, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the ACL could affect the distribution - In: true, - // The distribution could not affect the ACL - Out: false, - }, }) } else { // Else assume it's a V1 ID @@ -617,12 +449,6 @@ func distributionGetFunc(ctx context.Context, client CloudFrontClient, scope str Query: *dc.WebACLId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the ACL could affect the distribution - In: true, - // The distribution could not affect the ACL - Out: false, - }, }) } } diff --git a/aws-source/adapters/cloudfront-realtime-log-config.go b/aws-source/adapters/cloudfront-realtime-log-config.go index 069de999..be53fd7a 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config.go +++ b/aws-source/adapters/cloudfront-realtime-log-config.go @@ -35,12 +35,6 @@ func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogCon Query: *endpoint.KinesisStreamConfig.RoleARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect us - In: true, - // We can't affect the role - Out: false, - }, }) } } @@ -54,12 +48,6 @@ func realtimeLogConfigsItemMapper(_, scope string, awsItem *types.RealtimeLogCon Query: *endpoint.KinesisStreamConfig.StreamARN, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to this will affect the stream - Out: true, - // The stream can affect us - In: true, - }, }) } } diff --git a/aws-source/adapters/cloudfront-streaming-distribution.go b/aws-source/adapters/cloudfront-streaming-distribution.go index e0eb306c..024ba933 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution.go +++ b/aws-source/adapters/cloudfront-streaming-distribution.go @@ -74,11 +74,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *d.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always linked - In: true, - Out: true, - }, }) } @@ -92,11 +87,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.S3Origin.DomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } @@ -108,12 +98,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.S3Origin.OriginAccessIdentity, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the access identity will affect the distribution - In: true, - // The distribution won't affect the access identity - Out: false, - }, }) } } @@ -127,11 +111,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: alias, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -144,11 +123,6 @@ func streamingDistributionGetFunc(ctx context.Context, client CloudFrontClient, Query: *dc.Logging.Bucket, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/cloudwatch-alarm.go b/aws-source/adapters/cloudwatch-alarm.go index 1b0d4f8d..1d906def 100644 --- a/aws-source/adapters/cloudwatch-alarm.go +++ b/aws-source/adapters/cloudwatch-alarm.go @@ -154,12 +154,6 @@ func alarmOutputMapper(ctx context.Context, client CloudwatchClient, scope strin Query: arn.ResourceID(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the suppressor alarm will affect this alarm - In: true, - // Changes to this alarm won't affect the suppressor alarm - Out: false, - }, }) } } @@ -308,12 +302,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the policy won't affect the alarm - In: false, - // Changes to the metric alarm will affect the policy - Out: true, - }, }, nil case "sns": return &sdp.LinkedItemQuery{ @@ -323,12 +311,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the topic won't affect the alarm - In: false, - // Changes to the alarm will affect the topic - Out: true, - }, }, nil case "ssm": return &sdp.LinkedItemQuery{ @@ -338,12 +320,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to an ops item won't affect the alarm - In: false, - // Changes to the alarm will affect the ops item - Out: true, - }, }, nil case "ssm-incidents": return &sdp.LinkedItemQuery{ @@ -353,12 +329,6 @@ func actionToLink(action string) (*sdp.LinkedItemQuery, error) { Query: action, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a response plan won't affect the alarm - In: false, - // Changes to the alarm will affect the response plan - Out: true, - }, }, nil default: return nil, errors.New("unknown service in ARN: " + arn.Service) diff --git a/aws-source/adapters/directconnect-connection.go b/aws-source/adapters/directconnect-connection.go index f1303c1a..73fd78dc 100644 --- a/aws-source/adapters/directconnect-connection.go +++ b/aws-source/adapters/directconnect-connection.go @@ -34,12 +34,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -51,12 +45,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } @@ -68,12 +56,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -85,12 +67,6 @@ func directconnectConnectionOutputMapper(_ context.Context, _ *directconnect.Cli Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the virtual interface won't affect this - In: false, - // We cannot delete a connection if it has virtual interfaces - Out: true, - }, }) items = append(items, &item) diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go index c92bf20f..8fdfc4cd 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association-proposal.go @@ -34,13 +34,6 @@ func directConnectGatewayAssociationProposalOutputMapper(_ context.Context, _ *d Query: fmt.Sprintf("%s/%s", *associationProposal.DirectConnectGatewayId, *associationProposal.AssociatedGateway.Id), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Any change on the association won't have an impact on the proposal - // Its life cycle ends when the association is accepted or rejected - In: true, - // Accepting a proposal will establish the association - Out: true, - }, }) } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-association.go b/aws-source/adapters/directconnect-direct-connect-gateway-association.go index 95efa3cd..4ac0a0f9 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association.go @@ -47,12 +47,6 @@ func directConnectGatewayAssociationOutputMapper(_ context.Context, _ *directcon Query: *association.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a direct connect gateway will change the state of the association - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -64,12 +58,6 @@ func directConnectGatewayAssociationOutputMapper(_ context.Context, _ *directcon Query: *association.VirtualGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting a virtual gateway will change the state of the association - In: true, - // We can't affect the virtual gateway - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go index 22d7bdab..aa2b3fa8 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment.go @@ -50,12 +50,6 @@ func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconn Query: *attachment.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // This is not clearly documented, but it seems that if the gateway is deleted, the attachment state will change to detaching - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -67,13 +61,6 @@ func directConnectGatewayAttachmentOutputMapper(_ context.Context, _ *directconn Query: *attachment.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - In: true, - // We can't affect the virtual interface - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-hosted-connection.go b/aws-source/adapters/directconnect-hosted-connection.go index c64fbb1b..cc6d552b 100644 --- a/aws-source/adapters/directconnect-hosted-connection.go +++ b/aws-source/adapters/directconnect-hosted-connection.go @@ -34,12 +34,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -51,12 +45,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } @@ -68,12 +56,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -84,12 +66,6 @@ func hostedConnectionOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the virtual interface won't affect this - In: false, - // We cannot delete a hosted connection if it has virtual interfaces - Out: true, - }, }) items = append(items, &item) diff --git a/aws-source/adapters/directconnect-interconnect.go b/aws-source/adapters/directconnect-interconnect.go index 161dfd3d..aef9d008 100644 --- a/aws-source/adapters/directconnect-interconnect.go +++ b/aws-source/adapters/directconnect-interconnect.go @@ -53,12 +53,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.InterconnectId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Interconnect and hosted connections are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -70,12 +64,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Interconnect and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -87,12 +75,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.InterconnectId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the loa will affect this - In: true, - // We can't affect the loa - Out: false, - }, }) } @@ -104,12 +86,6 @@ func interconnectOutputMapper(_ context.Context, _ *directconnect.Client, scope Query: *interconnect.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-lag.go b/aws-source/adapters/directconnect-lag.go index 9b3831e1..e18044ae 100644 --- a/aws-source/adapters/directconnect-lag.go +++ b/aws-source/adapters/directconnect-lag.go @@ -54,12 +54,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *connection.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Connection and LAG are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } } @@ -72,12 +66,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *lag.LagId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // LAG and hosted connections are tightly coupled - // Changing one will affect the other - In: true, - Out: true, - }, }) } @@ -90,12 +78,6 @@ func lagOutputMapper(_ context.Context, _ *directconnect.Client, scope string, _ Query: *lag.Location, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the location will affect this, i.e., its speed, provider, etc. - In: true, - // We can't affect the location - Out: false, - }, }) } diff --git a/aws-source/adapters/directconnect-router-configuration.go b/aws-source/adapters/directconnect-router-configuration.go index 4a8e4d83..60d904b8 100644 --- a/aws-source/adapters/directconnect-router-configuration.go +++ b/aws-source/adapters/directconnect-router-configuration.go @@ -34,11 +34,6 @@ func routerConfigurationOutputMapper(_ context.Context, _ *directconnect.Client, Query: *output.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/directconnect-virtual-interface.go b/aws-source/adapters/directconnect-virtual-interface.go index 906fa032..aa57efc4 100644 --- a/aws-source/adapters/directconnect-virtual-interface.go +++ b/aws-source/adapters/directconnect-virtual-interface.go @@ -37,12 +37,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.ConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We cannot delete a connection if it has virtual interfaces - In: true, - // We can't affect the connection - Out: false, - }, }) } @@ -54,12 +48,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.DirectConnectGatewayId, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // We cannot delete a direct connect gateway if it has virtual interfaces - In: true, - // We can't affect the direct connect gateway - Out: false, - }, }) } @@ -71,11 +59,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.AmazonAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } @@ -87,11 +70,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.CustomerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // do not link through rdap definitions to avoid huge blast radius - In: false, - Out: false, - }, }) } @@ -105,13 +83,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: fmt.Sprintf("%s/%s", *virtualInterface.DirectConnectGatewayId, *virtualInterface.VirtualInterfaceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the attachment won't affect virtual interface - In: false, - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - Out: true, - }, }) } @@ -125,13 +96,6 @@ func virtualInterfaceOutputMapper(_ context.Context, _ *directconnect.Client, sc Query: *virtualInterface.VirtualInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the attachment won't affect virtual interface - In: false, - // If virtual interface is deleted, the attachment state will change to detaching - // https://docs.aws.amazon.com/directconnect/latest/APIReference/API_DirectConnectGatewayAttachment.html#API_DirectConnectGatewayAttachment_Contents - Out: true, - }, }) } diff --git a/aws-source/adapters/dynamodb-backup.go b/aws-source/adapters/dynamodb-backup.go index 9d1c599c..79905bdb 100644 --- a/aws-source/adapters/dynamodb-backup.go +++ b/aws-source/adapters/dynamodb-backup.go @@ -55,14 +55,6 @@ func backupGetFunc(ctx context.Context, client Client, scope string, input *dyna Query: *out.BackupDescription.SourceTableDetails.TableName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the table could probably affect the backup - In: true, - // Changing the backup won't exactly affect the table in - // that it won't break it. But it could mean that it's no - // longer backed up so, blast propagation should be here too - Out: true, - }, }) } } diff --git a/aws-source/adapters/dynamodb-table.go b/aws-source/adapters/dynamodb-table.go index 2bb55dfe..5e4e06c0 100644 --- a/aws-source/adapters/dynamodb-table.go +++ b/aws-source/adapters/dynamodb-table.go @@ -83,14 +83,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *dest.StreamArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If you change the stream, it could mean the table - // is no longer replicated - In: true, - // Changing this table will affect the stream and - // whatever is listening to it - Out: true, - }, }) } } @@ -107,14 +99,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.RestoreSummary.SourceBackupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The backup is just the source from which the table - // was created, so I guess you'd say that the recovery - // point affects the table - In: true, - // Changing the table won't affect the recovery point - Out: false, - }, }) } } @@ -128,14 +112,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.RestoreSummary.SourceTableArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If the table was restored from another table, and - // this is normal, then changing the source table could - // affect this one - In: true, - // Changing this table won't affect the source table - Out: false, - }, }) } } @@ -151,12 +127,6 @@ func tableGetFunc(ctx context.Context, client Client, scope string, input *dynam Query: *table.SSEDescription.KMSMasterKeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key could affect the table - In: true, - // Changing the table won't affect the key - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet.go b/aws-source/adapters/ec2-capacity-reservation-fleet.go index 75898c34..aa33087f 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet.go @@ -37,12 +37,6 @@ func capacityReservationFleetOutputMapper(_ context.Context, _ *ec2.Client, scop Query: *spec.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the fleet will affect the reservation - Out: true, - // The reservation won't affect us - In: false, - }, }) } } diff --git a/aws-source/adapters/ec2-capacity-reservation.go b/aws-source/adapters/ec2-capacity-reservation.go index 5585deb1..2034c63a 100644 --- a/aws-source/adapters/ec2-capacity-reservation.go +++ b/aws-source/adapters/ec2-capacity-reservation.go @@ -35,12 +35,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.CapacityReservationFleetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the fleet will affect this - In: true, - // We can't affect the fleet - Out: false, - }, }) } @@ -53,12 +47,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.OutpostArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the outpost will affect this - In: true, - // We can't affect the outpost - Out: false, - }, }) } } @@ -72,12 +60,6 @@ func capacityReservationOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: *cr.PlacementGroupArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the placement group will affect this - In: true, - // We can't affect the placement group - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-egress-only-internet-gateway.go b/aws-source/adapters/ec2-egress-only-internet-gateway.go index 703181f8..0fc882b9 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway.go @@ -54,12 +54,6 @@ func egressOnlyInternetGatewayOutputMapper(_ context.Context, _ *ec2.Client, sco Query: *attachment.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the gateway - In: false, - // Changing the gateway will affect the VPC - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-iam-instance-profile-association.go b/aws-source/adapters/ec2-iam-instance-profile-association.go index ded6b4c2..7dcc6bd7 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association.go @@ -35,12 +35,6 @@ func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, Query: *assoc.IamInstanceProfile.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this - In: true, - // We can't affect the profile - Out: false, - }, }) } } @@ -53,12 +47,6 @@ func iamInstanceProfileAssociationOutputMapper(_ context.Context, _ *ec2.Client, Query: *assoc.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the instance will not affect the association - In: false, - // changes to the association will affect the instance - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-instance-event-window.go b/aws-source/adapters/ec2-instance-event-window.go index ce71fd37..671f545d 100644 --- a/aws-source/adapters/ec2-instance-event-window.go +++ b/aws-source/adapters/ec2-instance-event-window.go @@ -52,12 +52,6 @@ func instanceEventWindowOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host won't affect the window - In: false, - // Changing the windows will affect the host - Out: true, - }, }) } @@ -69,12 +63,6 @@ func instanceEventWindowOutputMapper(_ context.Context, _ *ec2.Client, scope str Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host won't affect the window - In: false, - // Changing the windows will affect the instance - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-instance-status.go b/aws-source/adapters/ec2-instance-status.go index ef7ee70a..4609c94d 100644 --- a/aws-source/adapters/ec2-instance-status.go +++ b/aws-source/adapters/ec2-instance-status.go @@ -49,12 +49,6 @@ func instanceStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, Query: *instanceStatus.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The statius and the instance are closely linked and - // affect each other - In: true, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/ec2-instance.go b/aws-source/adapters/ec2-instance.go index 71a88f96..a6fde2ea 100644 --- a/aws-source/adapters/ec2-instance.go +++ b/aws-source/adapters/ec2-instance.go @@ -62,12 +62,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The status and the instance are closely linked and - // affect each other - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -77,12 +71,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Metrics inform decisions about the instance - In: true, - // Instance changes don't affect historical metrics - Out: false, - }, }, }, } @@ -113,12 +101,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.IamInstanceProfile.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this instance - In: true, - // We can't affect the profile - Out: false, - }, }) } } else if instance.IamInstanceProfile.Id != nil { @@ -129,12 +111,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.IamInstanceProfile.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the profile will affect this instance - In: true, - // We can't affect the profile - Out: false, - }, }) } } @@ -147,12 +123,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the reservation will affect the instance - In: true, - // Changing the instance won't affect the reservation - Out: false, - }, }) } @@ -165,12 +135,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *assoc.ElasticGpuId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the GPU will affect the instance - In: true, - // Changing the instance won't affect the GPU - Out: false, - }, }) } } @@ -185,12 +149,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *assoc.ElasticInferenceAcceleratorArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the accelerator will affect the instance - In: true, - // Changing the instance won't affect the accelerator - Out: false, - }, }) } } @@ -206,12 +164,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *license.LicenseConfigurationArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the license will affect the instance - In: true, - // Changing the instance won't affect the license - Out: false, - }, }) } } @@ -226,12 +178,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.OutpostArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the outpost will affect the instance - In: true, - // Changing the instance won't affect the outpost - Out: false, - }, }) } } @@ -244,12 +190,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.SpotInstanceRequestId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the spot request will affect the instance - In: true, - // Changing the instance won't affect the spot request - Out: false, - }, }) } @@ -261,12 +201,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.ImageId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image can't affect the instance once it - // has been created - In: false, - Out: false, - }, }) } @@ -278,13 +212,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.KeyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key pair will affect your ability to - // connect to the instance - In: true, - // Changing the instance won't affect the key pair - Out: false, - }, }) } @@ -297,12 +224,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.Placement.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing a placement group will affect instances - In: true, - // Changing an instance won't affect the group - Out: false, - }, }) } } @@ -315,11 +236,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -334,11 +250,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -352,11 +263,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -370,12 +276,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *nic.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the instance - In: true, - // Changing the instance won't affect the subnet - Out: false, - }, }) } @@ -388,12 +288,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *nic.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC will affect the instance - In: true, - // Changing the instance won't affect the VPC - Out: false, - }, }) } } @@ -406,11 +300,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.PublicDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS records are always linked - In: true, - Out: true, - }, }) } @@ -422,11 +311,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *instance.PublicIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always propagating - In: true, - Out: true, - }, }) } @@ -440,12 +324,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *group.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the instance - In: true, - // Changing the instance won't affect the security group - Out: false, - }, }) } } @@ -459,13 +337,6 @@ func instanceOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *mapping.Ebs.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the volume will affect the instance - In: true, - // Changing the instance could also affect the - // volume since it's writing to it - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-internet-gateway.go b/aws-source/adapters/ec2-internet-gateway.go index 58b6adec..40f28ee1 100644 --- a/aws-source/adapters/ec2-internet-gateway.go +++ b/aws-source/adapters/ec2-internet-gateway.go @@ -55,12 +55,6 @@ func internetGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, Query: *attachment.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the gateway - In: false, - // Changing the gateway will affect the VPC - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-launch-template-version.go b/aws-source/adapters/ec2-launch-template-version.go index 3f0cfbc9..82f93636 100644 --- a/aws-source/adapters/ec2-launch-template-version.go +++ b/aws-source/adapters/ec2-launch-template-version.go @@ -79,11 +79,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -96,12 +91,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ni.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the network interface will affect the - // template and vice versa - In: true, - Out: true, - }, }) } @@ -114,11 +103,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -131,12 +115,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *ni.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the template - In: true, - // Changing the template won't affect the subnet - Out: false, - }, }) } @@ -148,14 +126,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: group, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the - // template - In: true, - // Changing the template won't affect the security - // group - Out: false, - }, }) } } @@ -168,12 +138,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.ImageId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the template - In: true, - // Changing the template won't affect the image - Out: false, - }, }) } @@ -185,12 +149,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.KeyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key pair will affect the template - In: true, - // Changing the template won't affect the key pair - Out: false, - }, }) } @@ -203,12 +161,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *mapping.Ebs.SnapshotId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the snapshot will affect the template - In: true, - // Changing the template won't affect the snapshot - Out: false, - }, }) } } @@ -223,14 +175,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *target.CapacityReservationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the capacity reservation will affect - // the template - In: true, - // Changing the template could affect the - // capacity reservation since it uses it up - Out: true, - }, }) } } @@ -245,14 +189,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.Placement.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the placement group will affect the - // template - In: true, - // Changing the template won't affect the placement - // group - Out: false, - }, }) } @@ -264,12 +200,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: *lt.Placement.HostId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the host will affect the template - In: true, - // Changing the template could affect the host also - Out: true, - }, }) } } @@ -282,13 +212,6 @@ func launchTemplateVersionOutputMapper(_ context.Context, _ *ec2.Client, scope s Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the template - In: true, - // Changing the template won't affect the security - // group - Out: false, - }, }) } } diff --git a/aws-source/adapters/ec2-nat-gateway.go b/aws-source/adapters/ec2-nat-gateway.go index 9738e4c0..4c236431 100644 --- a/aws-source/adapters/ec2-nat-gateway.go +++ b/aws-source/adapters/ec2-nat-gateway.go @@ -54,12 +54,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The nat gateway and it's interfaces will affect each - // other - In: true, - Out: true, - }, }) } @@ -71,11 +65,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.PrivateIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -87,11 +76,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *address.PublicIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } } @@ -104,13 +88,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *ng.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet won't affect the gateway - In: false, - // Changing the gateway will affect the subnet since this - // will be gateway that subnet uses to access the internet - Out: true, - }, }) } @@ -122,12 +99,6 @@ func natGatewayOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *ng.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC could affect the gateway - In: true, - // Changing the gateway won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-network-acl.go b/aws-source/adapters/ec2-network-acl.go index ec35b197..f9952258 100644 --- a/aws-source/adapters/ec2-network-acl.go +++ b/aws-source/adapters/ec2-network-acl.go @@ -54,12 +54,6 @@ func networkAclOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet won't affect the ACL - In: false, - // Changing the ACL will affect the subnet - Out: true, - }, }) } } @@ -72,12 +66,6 @@ func networkAclOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *networkAcl.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC won't affect the ACL - In: false, - // Changing the ACL will affect the VPC - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-network-interface-permission.go b/aws-source/adapters/ec2-network-interface-permission.go index e914ea0b..d7739457 100644 --- a/aws-source/adapters/ec2-network-interface-permission.go +++ b/aws-source/adapters/ec2-network-interface-permission.go @@ -52,11 +52,6 @@ func networkInterfacePermissionOutputMapper(_ context.Context, _ *ec2.Client, sc Query: *ni.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These permissions are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-network-interface.go b/aws-source/adapters/ec2-network-interface.go index 73c28e20..4330c164 100644 --- a/aws-source/adapters/ec2-network-interface.go +++ b/aws-source/adapters/ec2-network-interface.go @@ -96,12 +96,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.Attachment.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The instance and the interface are closely linked - // and affect each other - In: true, - Out: true, - }, }) } } @@ -115,12 +109,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *sg.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // A security group will affect an interface - In: true, - // An interface won't affect a security group - Out: false, - }, }) } } @@ -134,11 +122,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -153,11 +136,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.PublicDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -169,11 +147,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.PublicIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -185,11 +158,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.CarrierIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -201,11 +169,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *assoc.CustomerOwnedIp, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -218,11 +181,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.PrivateDnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -234,11 +192,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ip.PrivateIpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -251,13 +204,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect interfaces within that - // subnet - In: true, - // Changing the interface won't affect the subnet - Out: false, - }, }) } @@ -269,12 +215,6 @@ func networkInterfaceOutputMapper(_ context.Context, _ *ec2.Client, scope string Query: *ni.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC will affect interfaces within that VPC - In: true, - // Changing the interface won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-route-table.go b/aws-source/adapters/ec2-route-table.go index 6dc36780..850d78f6 100644 --- a/aws-source/adapters/ec2-route-table.go +++ b/aws-source/adapters/ec2-route-table.go @@ -55,14 +55,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // All things in a route table could affect each other - // since changing the target could affect the - // traffic that is routed to it. And changing the route - // table could affect the target - In: true, - Out: true, - }, }) } @@ -74,10 +66,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *assoc.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -92,10 +80,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if strings.HasPrefix(*route.GatewayId, "vpce") { @@ -106,10 +90,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.GatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -121,10 +101,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.CarrierGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.EgressOnlyInternetGatewayId != nil { @@ -135,10 +111,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.EgressOnlyInternetGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.InstanceId != nil { @@ -149,10 +121,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.LocalGatewayId != nil { @@ -163,10 +131,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.LocalGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.NatGatewayId != nil { @@ -177,10 +141,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.NatGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.NetworkInterfaceId != nil { @@ -191,10 +151,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.TransitGatewayId != nil { @@ -205,10 +161,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.TransitGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if route.VpcPeeringConnectionId != nil { @@ -219,10 +171,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *route.VpcPeeringConnectionId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -235,10 +183,6 @@ func routeTableOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *e Query: *rt.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-security-group-rule.go b/aws-source/adapters/ec2-security-group-rule.go index 7c3261b9..c725c93b 100644 --- a/aws-source/adapters/ec2-security-group-rule.go +++ b/aws-source/adapters/ec2-security-group-rule.go @@ -53,11 +53,6 @@ func securityGroupRuleOutputMapper(_ context.Context, _ *ec2.Client, scope strin Query: *securityGroupRule.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -70,11 +65,6 @@ func securityGroupRuleOutputMapper(_ context.Context, _ *ec2.Client, scope strin Query: *rg.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-security-group.go b/aws-source/adapters/ec2-security-group.go index e4967640..ee5db475 100644 --- a/aws-source/adapters/ec2-security-group.go +++ b/aws-source/adapters/ec2-security-group.go @@ -55,12 +55,6 @@ func securityGroupOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *securityGroup.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC could affect the security group - In: true, - // The security group won't affect the VPC though - Out: false, - }, }) } @@ -75,13 +69,6 @@ func securityGroupOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *securityGroup.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Network interfaces don't affect the security group - In: false, - // Changes to the security group affect network interfaces - // (and through them, EC2 instances) - Out: true, - }, }) } @@ -165,11 +152,6 @@ func extractLinkedSecurityGroups(permissions []types.IpPermission, scope string) Query: *idGroup.GroupId, Scope: FormatScope(relatedAccount, region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Linked security groups affect each other - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-snapshot.go b/aws-source/adapters/ec2-snapshot.go index c94536ac..d9c5b93f 100644 --- a/aws-source/adapters/ec2-snapshot.go +++ b/aws-source/adapters/ec2-snapshot.go @@ -61,14 +61,6 @@ func snapshotOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2 Query: *snapshot.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the volume will probably affect the snapshot - In: true, - // Changing the snapshot will affect the volume indirectly - // as applications might rely on snapshots as backups - // or other use-cases - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-subnet.go b/aws-source/adapters/ec2-subnet.go index b34598f5..1d2a7da0 100644 --- a/aws-source/adapters/ec2-subnet.go +++ b/aws-source/adapters/ec2-subnet.go @@ -53,12 +53,6 @@ func subnetOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.D Query: *subnet.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC would affect the subnet - In: true, - // Changing the subnet won't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-association.go b/aws-source/adapters/ec2-transit-gateway-route-table-association.go index 3c6c9255..421becf5 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table-association.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association.go @@ -161,7 +161,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: awsItem.RouteTableID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) if a.TransitGatewayAttachmentId != nil { item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -171,7 +170,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: *a.TransitGatewayAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } if a.ResourceId != nil && *a.ResourceId != "" { @@ -184,7 +182,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: *a.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpn: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -194,7 +191,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: *a.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -204,7 +200,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: *a.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypePeering, types.TransitGatewayAttachmentResourceTypeTgwPeering: @@ -216,7 +211,6 @@ func transitGatewayRouteTableAssociationItemMapper(query, scope string, awsItem Query: *a.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, types.TransitGatewayAttachmentResourceTypeConnect, diff --git a/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go index 916474f3..4a321847 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation.go @@ -147,7 +147,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: awsItem.RouteTableID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) // Link to the route table association (same route table + attachment). if p.TransitGatewayAttachmentId != nil { @@ -158,7 +157,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *p.TransitGatewayAttachmentId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } if p.TransitGatewayAttachmentId != nil { @@ -169,7 +167,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: *p.TransitGatewayAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } if p.ResourceId != nil && *p.ResourceId != "" { @@ -182,7 +179,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: *p.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpn: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -192,7 +188,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: *p.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -202,7 +197,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: *p.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypePeering, types.TransitGatewayAttachmentResourceTypeTgwPeering: @@ -214,7 +208,6 @@ func transitGatewayRouteTablePropagationItemMapper(query, scope string, awsItem Query: *p.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, types.TransitGatewayAttachmentResourceTypeConnect, diff --git a/aws-source/adapters/ec2-transit-gateway-route-table.go b/aws-source/adapters/ec2-transit-gateway-route-table.go index f9194d63..8cdeb0c8 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table.go @@ -57,10 +57,6 @@ func transitGatewayRouteTableOutputMapper(_ context.Context, _ *ec2.Client, scop Query: *rt.TransitGatewayId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -75,7 +71,6 @@ func transitGatewayRouteTableOutputMapper(_ context.Context, _ *ec2.Client, scop Query: rtID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } } diff --git a/aws-source/adapters/ec2-transit-gateway-route.go b/aws-source/adapters/ec2-transit-gateway-route.go index 904ee50d..c7f10314 100644 --- a/aws-source/adapters/ec2-transit-gateway-route.go +++ b/aws-source/adapters/ec2-transit-gateway-route.go @@ -174,7 +174,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: awsItem.RouteTableID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) for i := range r.TransitGatewayAttachments { att := &r.TransitGatewayAttachments[i] @@ -186,7 +185,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *att.TransitGatewayAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) // Link to the route table association (same route table + attachment). item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -196,7 +194,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: transitGatewayRouteTableAssociationID(awsItem.RouteTableID, *att.TransitGatewayAttachmentId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } if att.ResourceId != nil && *att.ResourceId != "" { @@ -209,7 +206,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *att.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpn: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -219,7 +215,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *att.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeDirectConnectGateway: item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -229,7 +224,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *att.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypePeering, types.TransitGatewayAttachmentResourceTypeTgwPeering: @@ -241,7 +235,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *att.ResourceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) case types.TransitGatewayAttachmentResourceTypeVpnConcentrator, types.TransitGatewayAttachmentResourceTypeConnect, @@ -258,7 +251,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *r.PrefixListId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } if r.TransitGatewayRouteTableAnnouncementId != nil && *r.TransitGatewayRouteTableAnnouncementId != "" { @@ -269,7 +261,6 @@ func transitGatewayRouteItemMapper(query, scope string, awsItem *transitGatewayR Query: *r.TransitGatewayRouteTableAnnouncementId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, }) } return item, nil diff --git a/aws-source/adapters/ec2-volume-status.go b/aws-source/adapters/ec2-volume-status.go index 227c8bc9..e2eec450 100644 --- a/aws-source/adapters/ec2-volume-status.go +++ b/aws-source/adapters/ec2-volume-status.go @@ -52,11 +52,6 @@ func volumeStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *volume.VolumeId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Volume and status are tightly coupled - In: true, - Out: true, - }, }, }, } @@ -83,11 +78,6 @@ func volumeStatusOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ Query: *event.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Instances and volumes can affect each other - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ec2-volume.go b/aws-source/adapters/ec2-volume.go index 18360be3..469b27ce 100644 --- a/aws-source/adapters/ec2-volume.go +++ b/aws-source/adapters/ec2-volume.go @@ -53,11 +53,6 @@ func volumeOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2.D Query: *attachment.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The instance and the volume are closely linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-vpc-endpoint.go b/aws-source/adapters/ec2-vpc-endpoint.go index 05c7a156..96bb7018 100644 --- a/aws-source/adapters/ec2-vpc-endpoint.go +++ b/aws-source/adapters/ec2-vpc-endpoint.go @@ -95,11 +95,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *endpoint.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the VPC overall - In: true, - Out: false, - }, }) } @@ -115,11 +110,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: routeTableID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the route table overall - In: true, - Out: false, - }, }) } @@ -131,11 +121,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: subnetID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the subnet overall - In: true, - Out: false, - }, }) } @@ -148,11 +133,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *group.GroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the security group overall - In: true, - Out: false, - }, }) } } @@ -166,11 +146,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *dnsEntry.DnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -182,11 +157,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: *dnsEntry.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // We can't affect the hosted zone overall - In: true, - Out: false, - }, }) } } @@ -199,11 +169,6 @@ func vpcEndpointOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ * Query: networkInterfaceID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ec2-vpc-peering-connection.go b/aws-source/adapters/ec2-vpc-peering-connection.go index 90149772..eccdc459 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection.go +++ b/aws-source/adapters/ec2-vpc-peering-connection.go @@ -63,12 +63,6 @@ func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope st Query: *connection.AccepterVpcInfo.VpcId, Scope: pairedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC will affect everything in it - In: true, - // We can't affect the VPC - Out: false, - }, }) } } @@ -87,12 +81,6 @@ func vpcPeeringConnectionOutputMapper(_ context.Context, _ *ec2.Client, scope st Query: *connection.RequesterVpcInfo.VpcId, Scope: pairedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC will affect everything in it - In: true, - // We can't affect the VPC - Out: false, - }, }) } } diff --git a/aws-source/adapters/ecs-capacity-provider.go b/aws-source/adapters/ecs-capacity-provider.go index a9e234c1..38583089 100644 --- a/aws-source/adapters/ecs-capacity-provider.go +++ b/aws-source/adapters/ecs-capacity-provider.go @@ -43,11 +43,6 @@ func capacityProviderOutputMapper(_ context.Context, _ ECSClient, scope string, Query: *provider.AutoScalingGroupProvider.AutoScalingGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/ecs-cluster.go b/aws-source/adapters/ecs-cluster.go index e7248788..ba973d2e 100644 --- a/aws-source/adapters/ecs-cluster.go +++ b/aws-source/adapters/ecs-cluster.go @@ -75,13 +75,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Container instances can affect the cluster - In: true, - // The cluster will definitely affect the container - // instances - Out: true, - }, }, { Query: &sdp.Query{ @@ -90,12 +83,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Services won't affect the cluster - In: false, - // The cluster will definitely affect the services - Out: true, - }, }, { Query: &sdp.Query{ @@ -104,12 +91,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.ClusterName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tasks won't affect the cluster - In: false, - // The cluster will definitely affect the tasks - Out: true, - }, }, }, } @@ -140,12 +121,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.KmsKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key will probably affect the cluster - In: true, - // The cluster won't affect the KMS key though - Out: false, - }, }) } @@ -158,11 +133,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.LogConfiguration.CloudWatchLogGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } @@ -174,11 +144,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: *cluster.Configuration.ExecuteCommandConfiguration.LogConfiguration.S3BucketName, Scope: FormatScope(accountID, ""), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -193,11 +158,6 @@ func ecsClusterGetFunc(ctx context.Context, client ECSClient, scope string, inpu Query: provider, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-container-instance.go b/aws-source/adapters/ecs-container-instance.go index ec144590..021c1a3e 100644 --- a/aws-source/adapters/ecs-container-instance.go +++ b/aws-source/adapters/ecs-container-instance.go @@ -73,11 +73,6 @@ func containerInstanceGetFunc(ctx context.Context, client ECSClient, scope strin Query: *containerInstance.Ec2InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These represent the same thing - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-service.go b/aws-source/adapters/ecs-service.go index 58ef6325..fcbbc3ff 100644 --- a/aws-source/adapters/ecs-service.go +++ b/aws-source/adapters/ecs-service.go @@ -91,12 +91,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *service.ClusterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the cluster will affect the service - In: true, - // The service should be able to affect the cluster - Out: false, - }, }) } } @@ -111,11 +105,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *lb.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -131,11 +120,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *sr.RegistryArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -150,12 +134,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *service.TaskDefinition, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the task definition will affect the service - In: true, - // The service shouldn't affect the task definition itself - Out: false, - }, }) } } @@ -170,12 +148,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *deployment.TaskDefinition, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the task definition will affect the service - In: true, - // The service shouldn't affect the task definition itself - Out: false, - }, }) } } @@ -189,12 +161,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *strategy.CapacityProvider, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the capacity provider will affect the service - In: true, - // The service shouldn't affect the capacity provider itself - Out: false, - }, }) } } @@ -209,12 +175,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the service - In: true, - // The service shouldn't affect the subnet - Out: false, - }, }) } @@ -226,12 +186,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the service - In: true, - // The service shouldn't affect the security group - Out: false, - }, }) } } @@ -248,11 +202,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *alias.DnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } } @@ -269,11 +218,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: *cr.DiscoveryArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -290,12 +234,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the service - In: true, - // The service shouldn't affect the subnet - Out: false, - }, }) } @@ -307,12 +245,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the service - In: true, - // The service shouldn't affect the security group - Out: false, - }, }) } } @@ -326,11 +258,6 @@ func serviceGetFunc(ctx context.Context, client ECSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/ecs-task-definition.go b/aws-source/adapters/ecs-task-definition.go index 531bf167..41e691f6 100644 --- a/aws-source/adapters/ecs-task-definition.go +++ b/aws-source/adapters/ecs-task-definition.go @@ -95,12 +95,6 @@ func taskDefinitionGetFunc(ctx context.Context, client ECSClient, scope string, Query: *td.ExecutionRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the task definition - In: true, - // The task definition can't affect the role - Out: false, - }, }) } } @@ -114,12 +108,6 @@ func taskDefinitionGetFunc(ctx context.Context, client ECSClient, scope string, Query: *td.TaskRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the task definition - In: true, - // The task definition can't affect the role - Out: false, - }, }) } } @@ -145,12 +133,6 @@ func getSecretLinkedItem(secret types.Secret) *sdp.LinkedItemQuery { Query: *secret.ValueFrom, Scope: secretScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The secret can affect the task definition - In: true, - // The task definition can't affect the secret - Out: false, - }, } case "ssm": return &sdp.LinkedItemQuery{ @@ -160,12 +142,6 @@ func getSecretLinkedItem(secret types.Secret) *sdp.LinkedItemQuery { Query: *secret.ValueFrom, Scope: secretScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The secret can affect the task definition - In: true, - // The task definition can't affect the secret - Out: false, - }, } } } diff --git a/aws-source/adapters/ecs-task.go b/aws-source/adapters/ecs-task.go index da3852cd..b301f1a7 100644 --- a/aws-source/adapters/ecs-task.go +++ b/aws-source/adapters/ecs-task.go @@ -79,11 +79,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *attachment.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -99,12 +94,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *task.ClusterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The cluster can affect the task - In: true, - // The task can't affect the cluster - Out: false, - }, }) } } @@ -118,12 +107,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: a.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The container instance can affect the task - In: true, - // The task can't affect the container instance - Out: false, - }, }) } } @@ -138,11 +121,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *ni.Ipv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -154,11 +132,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *ni.PrivateIpv4Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -173,12 +146,6 @@ func taskGetFunc(ctx context.Context, client ECSClient, scope string, input *ecs Query: *task.TaskDefinitionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The task definition can affect the task - In: true, - // The task can't affect the task definition - Out: false, - }, }) } } diff --git a/aws-source/adapters/efs-access-point.go b/aws-source/adapters/efs-access-point.go index d9614a4e..15666b83 100644 --- a/aws-source/adapters/efs-access-point.go +++ b/aws-source/adapters/efs-access-point.go @@ -41,11 +41,6 @@ func AccessPointOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *ap.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Access points are tightly coupled with filesystems - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/efs-file-system.go b/aws-source/adapters/efs-file-system.go index 2721dfae..63fcac44 100644 --- a/aws-source/adapters/efs-file-system.go +++ b/aws-source/adapters/efs-file-system.go @@ -43,13 +43,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the backup policy could effect the file - // system in that it might no longer be backed up - In: true, - // Changing the file system will not effect the backup - Out: false, - }, }, { Query: &sdp.Query{ @@ -58,11 +51,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.FileSystemId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }, }, } @@ -77,12 +65,6 @@ func FileSystemOutputMapper(_ context.Context, _ *efs.Client, scope string, inpu Query: *fs.KmsKeyId, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect us - In: true, - // We can't affect the key - Out: false, - }, }) } } diff --git a/aws-source/adapters/efs-mount-target.go b/aws-source/adapters/efs-mount-target.go index d8a40bb3..67f7374c 100644 --- a/aws-source/adapters/efs-mount-target.go +++ b/aws-source/adapters/efs-mount-target.go @@ -58,12 +58,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the subnet could affect the mount but no the - // other way around - In: true, - Out: false, - }, }) } @@ -75,11 +69,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.IpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } @@ -91,11 +80,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -107,12 +91,6 @@ func MountTargetOutputMapper(_ context.Context, _ *efs.Client, scope string, inp Query: *mt.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC will affect us - In: true, - // We can't affect the VPC - Out: false, - }, }) } diff --git a/aws-source/adapters/efs-replication-configuration.go b/aws-source/adapters/efs-replication-configuration.go index e70457cc..c966ef22 100644 --- a/aws-source/adapters/efs-replication-configuration.go +++ b/aws-source/adapters/efs-replication-configuration.go @@ -66,12 +66,6 @@ func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scop Query: *destination.FileSystemId, Scope: FormatScope(accountID, *destination.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the destination shouldn't affect the source - In: false, - // Changes to this can affect the destination - Out: true, - }, }) } } @@ -107,12 +101,6 @@ func ReplicationConfigurationOutputMapper(_ context.Context, _ *efs.Client, scop Query: *replication.OriginalSourceFileSystemArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the source file system will affect its replication - In: true, - // Changing replication shouldn't affect the filesystem itself - Out: false, - }, }) } diff --git a/aws-source/adapters/eks-cluster.go b/aws-source/adapters/eks-cluster.go index 806bdb0d..078512d2 100644 --- a/aws-source/adapters/eks-cluster.go +++ b/aws-source/adapters/eks-cluster.go @@ -46,11 +46,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -59,11 +54,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -72,11 +62,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, }, } @@ -108,12 +93,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ConnectorConfig.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the cluster - In: true, - // The cluster can't affect the role - Out: false, - }, }) } } @@ -130,12 +109,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *conf.Provider.KeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The key can affect the cluster - In: true, - // The cluster can't affect the key - Out: false, - }, }) } } @@ -150,11 +123,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.Endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // HTTP should be linked bidirectionally - In: true, - Out: true, - }, }) } @@ -167,12 +135,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ResourcesVpcConfig.ClusterSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The SG can affect the cluster - In: true, - // The cluster can't affect the SG - Out: false, - }, }) } @@ -184,12 +146,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The SG can affect the cluster - In: true, - // The cluster can't affect the SG - Out: false, - }, }) } @@ -201,12 +157,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet can affect the cluster - In: true, - // The cluster can't affect the subnet - Out: false, - }, }) } @@ -218,12 +168,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.ResourcesVpcConfig.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The VPC can affect the cluster - In: true, - // The cluster can't affect the VPC - Out: false, - }, }) } } @@ -237,12 +181,6 @@ func clusterGetFunc(ctx context.Context, client EKSClient, scope string, input * Query: *cluster.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The role can affect the cluster - In: true, - // The cluster can't affect the role - Out: false, - }, }) } } diff --git a/aws-source/adapters/eks-fargate-profile.go b/aws-source/adapters/eks-fargate-profile.go index cd54ab90..b4af7ff4 100644 --- a/aws-source/adapters/eks-fargate-profile.go +++ b/aws-source/adapters/eks-fargate-profile.go @@ -51,12 +51,6 @@ func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, Query: *out.FargateProfile.PodExecutionRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // The execution role will affect the fargate profile - In: true, - // The fargate profile can't affect the execution role - Out: false, - }, }) } } @@ -69,12 +63,6 @@ func fargateProfileGetFunc(ctx context.Context, client EKSClient, scope string, Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet will affect the fargate profile - In: true, - // The fargate profile can't affect the subnet - Out: false, - }, }) } diff --git a/aws-source/adapters/eks-nodegroup.go b/aws-source/adapters/eks-nodegroup.go index 1df7c710..b1a41fa7 100644 --- a/aws-source/adapters/eks-nodegroup.go +++ b/aws-source/adapters/eks-nodegroup.go @@ -65,12 +65,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.RemoteAccess.Ec2SshKey, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The key pair can affect the node group - In: true, - // The node group can't affect the key pair - Out: false, - }, }) } @@ -82,12 +76,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The security group can affect the node group - In: true, - // The node group can't affect the security group - Out: false, - }, }) } } @@ -100,12 +88,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: subnet, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The subnet can affect the node group - In: true, - // The node group can't affect the subnet - Out: false, - }, }) } @@ -119,11 +101,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *g.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) } } @@ -136,12 +113,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.Resources.RemoteAccessSecurityGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The security group can affect the node group - In: true, - // The node group can't affect the security group - Out: false, - }, }) } } @@ -155,12 +126,6 @@ func nodegroupGetFunc(ctx context.Context, client EKSClient, scope string, input Query: *ng.LaunchTemplate.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The launch template can affect the node group - In: true, - // The node group can't affect the launch template - Out: false, - }, }) } } diff --git a/aws-source/adapters/elb-instance-health.go b/aws-source/adapters/elb-instance-health.go index bd03ff61..bc17f5ff 100644 --- a/aws-source/adapters/elb-instance-health.go +++ b/aws-source/adapters/elb-instance-health.go @@ -73,11 +73,6 @@ func instanceHealthOutputMapper(_ context.Context, _ *elb.Client, scope string, Query: *is.InstanceId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/elbv2-listener.go b/aws-source/adapters/elbv2-listener.go index 98e491c4..a6950275 100644 --- a/aws-source/adapters/elbv2-listener.go +++ b/aws-source/adapters/elbv2-listener.go @@ -74,11 +74,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *listener.LoadBalancerArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their listeners are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -88,11 +83,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *listener.ListenerArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -107,12 +97,6 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, Query: *cert.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the cert will affect the LB - In: true, - // The LB won't affect the cert - Out: false, - }, }) } } diff --git a/aws-source/adapters/elbv2-load-balancer.go b/aws-source/adapters/elbv2-load-balancer.go index 5876349f..dfc3dabe 100644 --- a/aws-source/adapters/elbv2-load-balancer.go +++ b/aws-source/adapters/elbv2-load-balancer.go @@ -52,11 +52,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.LoadBalancerArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their target groups are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -66,11 +61,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.LoadBalancerArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their listeners are tightly coupled - In: true, - Out: true, - }, }) } @@ -82,11 +72,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.DNSName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -98,12 +83,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.CanonicalHostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone could affect the LB - In: true, - // The LB won't affect the hosted zone - Out: false, - }, }) } @@ -115,12 +94,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC could affect the LB - In: true, - // The LB won't affect the VPC - Out: false, - }, }) } @@ -133,12 +106,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *az.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet could affect the LB - In: true, - // The LB won't affect the subnet - Out: false, - }, }) } @@ -151,12 +118,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.AllocationId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the address could affect the LB - In: true, - // The LB can also affect the address - Out: true, - }, }) } @@ -168,11 +129,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.IPv6Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -184,11 +140,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.IpAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } @@ -200,11 +151,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *address.PrivateIPv4Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs always link - In: true, - Out: true, - }, }) } } @@ -218,12 +164,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: sg, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group could affect the LB - In: true, - // The LB won't affect the security group - Out: false, - }, }) } @@ -235,12 +175,6 @@ func elbv2LoadBalancerOutputMapper(ctx context.Context, client elbv2Client, scop Query: *lb.CustomerOwnedIpv4Pool, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the COIP pool could affect the LB - In: true, - // The LB won't affect the COIP pool - Out: false, - }, }) } diff --git a/aws-source/adapters/elbv2-rule.go b/aws-source/adapters/elbv2-rule.go index 03ed6af0..14975ff9 100644 --- a/aws-source/adapters/elbv2-rule.go +++ b/aws-source/adapters/elbv2-rule.go @@ -60,11 +60,6 @@ func ruleOutputMapper(ctx context.Context, client elbv2Client, scope string, _ * Query: value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2-target-group.go b/aws-source/adapters/elbv2-target-group.go index e6f2da38..09af3e31 100644 --- a/aws-source/adapters/elbv2-target-group.go +++ b/aws-source/adapters/elbv2-target-group.go @@ -52,11 +52,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: *tg.TargetGroupArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Target groups and their target health are tightly coupled - In: true, - Out: true, - }, }) } @@ -68,12 +63,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: *tg.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC can affect the target group - In: true, - // The target group won't affect the VPC - Out: false, - }, }) } @@ -86,11 +75,6 @@ func targetGroupOutputMapper(ctx context.Context, client elbv2Client, scope stri Query: lbArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Load balancers and their target groups are tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2-target-health.go b/aws-source/adapters/elbv2-target-health.go index 851e834a..ce68427d 100644 --- a/aws-source/adapters/elbv2-target-health.go +++ b/aws-source/adapters/elbv2-target-health.go @@ -152,11 +152,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Everything is tightly coupled with target health - In: true, - Out: true, - }, }) case "elasticloadbalancing": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -166,10 +161,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } else { @@ -184,10 +175,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { // If all else fails it must be an instance ID @@ -198,10 +185,6 @@ func targetHealthOutputMapper(_ context.Context, _ *elbv2.Client, scope string, Query: *desc.Target.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/elbv2.go b/aws-source/adapters/elbv2.go index 3152431c..f214aefa 100644 --- a/aws-source/adapters/elbv2.go +++ b/aws-source/adapters/elbv2.go @@ -72,12 +72,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateCognitoConfig.UserPoolArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the user pool could affect the LB - In: true, - // The LB won't affect the user pool - Out: false, - }, }) } } @@ -92,12 +86,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.AuthorizationEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } @@ -109,12 +97,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.TokenEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } @@ -126,12 +108,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.AuthenticateOidcConfig.UserInfoEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the authorization endpoint could affect the LB - In: true, - // The LB won't affect the authorization endpoint - Out: false, - }, }) } } @@ -147,12 +123,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *tg.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the target group could affect the LB - In: true, - // The LB could also affect the target group - Out: true, - }, }) } } @@ -194,11 +164,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: u.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are closely linked - In: true, - Out: true, - }, }) } } @@ -212,11 +177,6 @@ func ActionToRequests(action types.Action) []*sdp.LinkedItemQuery { Query: *action.TargetGroupArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are closely linked - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/iam-instance-profile.go b/aws-source/adapters/iam-instance-profile.go index 4d691449..5c26f9c8 100644 --- a/aws-source/adapters/iam-instance-profile.go +++ b/aws-source/adapters/iam-instance-profile.go @@ -46,12 +46,6 @@ func instanceProfileItemMapper(_ *string, scope string, awsItem *types.InstanceP Query: *role.Arn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect this - In: true, - // We can't affect the role - Out: false, - }, }) } @@ -64,12 +58,6 @@ func instanceProfileItemMapper(_ *string, scope string, awsItem *types.InstanceP Query: *role.PermissionsBoundary.PermissionsBoundaryArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the policy will affect this - In: true, - // We can't affect the policy - Out: false, - }, }) } } diff --git a/aws-source/adapters/iam-policy.go b/aws-source/adapters/iam-policy.go index 15bbe465..5a48ad15 100644 --- a/aws-source/adapters/iam-policy.go +++ b/aws-source/adapters/iam-policy.go @@ -172,12 +172,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the group won't affect the policy - In: false, - // Changing the policy will affect the group - Out: true, - }, }) } @@ -189,12 +183,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Query: *user.UserName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the user won't affect the policy - In: false, - // Changing the policy will affect the user - Out: true, - }, }) } @@ -206,12 +194,6 @@ func policyItemMapper(_ *string, scope string, awsItem *PolicyDetails) (*sdp.Ite Query: *role.RoleName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role won't affect the policy - In: false, - // Changing the policy will affect the role - Out: true, - }, }) } diff --git a/aws-source/adapters/iam-role.go b/aws-source/adapters/iam-role.go index 2fefdc11..515f389a 100644 --- a/aws-source/adapters/iam-role.go +++ b/aws-source/adapters/iam-role.go @@ -201,12 +201,6 @@ func roleItemMapper(_ *string, scope string, awsItem *RoleDetails) (*sdp.Item, e Query: *policy.PolicyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the policy will affect the role - In: true, - // Changing the role won't affect the policy - Out: false, - }, }) } } diff --git a/aws-source/adapters/iam-user.go b/aws-source/adapters/iam-user.go index cc74d326..3afd374c 100644 --- a/aws-source/adapters/iam-user.go +++ b/aws-source/adapters/iam-user.go @@ -99,12 +99,6 @@ func userItemMapper(_ *string, scope string, awsItem *UserDetails) (*sdp.Item, e Query: *group.GroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the group can affect the user - In: true, - // Changing the user won't affect the group - Out: false, - }, }) } diff --git a/aws-source/adapters/iam.go b/aws-source/adapters/iam.go index 465daebc..94744a03 100644 --- a/aws-source/adapters/iam.go +++ b/aws-source/adapters/iam.go @@ -69,10 +69,6 @@ var ssmQueryExtractor = QueryExtractor{ Query: a.String() + "*", // Wildcard at the end Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } }, @@ -118,10 +114,6 @@ var fallbackQueryExtractor = QueryExtractor{ Query: arn.String(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, } }, @@ -171,13 +163,6 @@ func LinksFromPolicy(document *policy.Policy) []*sdp.LinkedItemQuery { Query: arn.String(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If a user or role iex explicitly - // referenced, I think it's reasonable to - // assume that they are tightly bound - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/kms-alias.go b/aws-source/adapters/kms-alias.go index fbd810b9..ce0c8fd2 100644 --- a/aws-source/adapters/kms-alias.go +++ b/aws-source/adapters/kms-alias.go @@ -64,12 +64,6 @@ func aliasOutputMapper(_ context.Context, _ *kms.Client, scope string, _ *kms.Li Query: *alias.TargetKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding, deleting, or updating an alias can allow or deny permission to the KMS key. - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/kms-custom-key-store.go b/aws-source/adapters/kms-custom-key-store.go index ad5673dc..b9c759fd 100644 --- a/aws-source/adapters/kms-custom-key-store.go +++ b/aws-source/adapters/kms-custom-key-store.go @@ -54,12 +54,6 @@ func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, Query: *customKeyStore.CloudHsmClusterId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the CloudHSM cluster will affect the custom key store - In: true, - // Updating the custom key store will not affect the CloudHSM cluster - Out: false, - }, }) } @@ -72,12 +66,6 @@ func customKeyStoreOutputMapper(_ context.Context, _ *kms.Client, scope string, Query: fmt.Sprintf("name|%s", *customKeyStore.XksProxyConfiguration.VpcEndpointServiceName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC endpoint service will affect the custom key store - In: true, - // Updating the custom key store will not affect the VPC endpoint service - Out: false, - }, }) } diff --git a/aws-source/adapters/kms-grant.go b/aws-source/adapters/kms-grant.go index 11a92659..2d3e90cc 100644 --- a/aws-source/adapters/kms-grant.go +++ b/aws-source/adapters/kms-grant.go @@ -63,12 +63,6 @@ func grantOutputMapper(ctx context.Context, _ *kms.Client, scope string, _ *kms. Query: keyID, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding or revoking/retiring a grant can allow or deny permission to the KMS key for the grantee. - In: true, - Out: true, - }, }) var principals []string @@ -123,13 +117,6 @@ func grantOutputMapper(ctx context.Context, _ *kms.Client, scope string, _ *kms. Method: sdp.QueryMethod_GET, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - // Adding or revoking/retiring a grant can allow or deny permission to the KMS key for the grantee. - // Or, disabling a role will make the grant redundant. - In: true, - Out: true, - }, } arn, errA := ParseARN(principal) diff --git a/aws-source/adapters/kms-key-policy.go b/aws-source/adapters/kms-key-policy.go index 6fbcb5d1..6ed8eef4 100644 --- a/aws-source/adapters/kms-key-policy.go +++ b/aws-source/adapters/kms-key-policy.go @@ -75,11 +75,6 @@ func getKeyPolicyFunc(ctx context.Context, client keyPolicyClient, scope string, Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/kms-key.go b/aws-source/adapters/kms-key.go index a066927a..ca9bc3e9 100644 --- a/aws-source/adapters/kms-key.go +++ b/aws-source/adapters/kms-key.go @@ -62,12 +62,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *output.KeyMetadata.CustomKeyStoreId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // A keystore cannot be deleted if it contains a key. - In: true, - // Any change on the key won't affect the keystore. - Out: false, - }, }) } @@ -78,11 +72,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - }, }) item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -92,11 +81,6 @@ func kmsKeyGetFunc(ctx context.Context, client kmsClient, scope string, input *k Query: *input.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) switch output.KeyMetadata.KeyState { diff --git a/aws-source/adapters/lambda-event-source-mapping.go b/aws-source/adapters/lambda-event-source-mapping.go index f97aeea3..8a3f134f 100644 --- a/aws-source/adapters/lambda-event-source-mapping.go +++ b/aws-source/adapters/lambda-event-source-mapping.go @@ -99,11 +99,6 @@ func eventSourceMappingOutputMapper(query, scope string, awsItem *types.EventSou Query: *awsItem.FunctionArn, Scope: FormatScope(parsedARN.AccountID, parsedARN.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // They are tightly linked - In: true, - Out: true, - }, }) } } @@ -143,12 +138,6 @@ func eventSourceMappingOutputMapper(query, scope string, awsItem *types.EventSou Query: *awsItem.EventSourceArn, Scope: FormatScope(parsedARN.AccountID, parsedARN.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the event source will affect the mapping - In: true, - // Changing the mapping won't affect the event source - Out: false, - }, }) } } diff --git a/aws-source/adapters/lambda-function.go b/aws-source/adapters/lambda-function.go index c118cd20..8f363eeb 100644 --- a/aws-source/adapters/lambda-function.go +++ b/aws-source/adapters/lambda-function.go @@ -148,11 +148,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: u.String(), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: false, - }, }) } } @@ -165,12 +160,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Code.ImageUri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the function - In: true, - // Changing the function won't affect the image - Out: false, - }, }) } @@ -182,12 +171,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Code.ResolvedImageUri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the image will affect the function - In: true, - // Changing the function won't affect the image - Out: false, - }, }) } } @@ -223,12 +206,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.Role, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role will affect the function - In: true, - // Changing the function won't affect the role - Out: false, - }, }) } } @@ -259,11 +236,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *fsConfig.Arn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are really tightly linked - In: true, - Out: true, - }, }) } } @@ -278,12 +250,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.KMSKeyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect the function - In: true, - // Changing the function won't affect the key - Out: false, - }, }) } } @@ -301,11 +267,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: name, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -319,12 +280,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *layer.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -338,12 +293,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *layer.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -358,11 +307,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.MasterArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly linked - In: true, - Out: true, - }, }) } } @@ -376,12 +320,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -395,12 +333,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the signing will affect the function - In: true, - // Changing the function won't affect the signing - Out: false, - }, }) } } @@ -414,12 +346,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group will affect the function - In: true, - // Changing the function won't affect the security group - Out: false, - }, }) } @@ -431,12 +357,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet will affect the function - In: true, - // Changing the function won't affect the subnet - Out: false, - }, }) } @@ -448,7 +368,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *function.Configuration.VpcConfig.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{}, }) } } @@ -463,11 +382,6 @@ func functionGetFunc(ctx context.Context, client LambdaClient, scope string, inp Query: *config.FunctionUrl, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }) } } @@ -550,12 +464,6 @@ func ExtractLinksFromPolicy(policy *PolicyDocument) []*sdp.LinkedItemQuery { Query: statement.Condition.ArnLike.AWSSourceArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing a lambda shouldn't affect the upstream source - Out: false, - // Changing the source should affect the lambda - In: true, - }, }) } @@ -582,11 +490,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "sqs": return &sdp.LinkedItemQuery{ @@ -596,11 +499,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "lambda": return &sdp.LinkedItemQuery{ @@ -610,11 +508,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil case "events": return &sdp.LinkedItemQuery{ @@ -624,11 +517,6 @@ func GetEventLinkedItem(destinationARN string) (*sdp.LinkedItemQuery, error) { Query: destinationARN, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly linked - In: true, - Out: true, - }, }, nil } diff --git a/aws-source/adapters/lambda-layer-version.go b/aws-source/adapters/lambda-layer-version.go index 6eb374b0..a62e6d71 100644 --- a/aws-source/adapters/lambda-layer-version.go +++ b/aws-source/adapters/lambda-layer-version.go @@ -75,12 +75,6 @@ func layerVersionGetFunc(ctx context.Context, client LambdaClient, scope string, Query: *out.Content.SigningJobArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Signing jobs can affect layers - In: true, - // Changing the layer won't affect the signing job - Out: false, - }, }) } } @@ -94,12 +88,6 @@ func layerVersionGetFunc(ctx context.Context, client LambdaClient, scope string, Query: *out.Content.SigningProfileVersionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Signing profiles can affect layers - In: true, - // Changing the layer won't affect the signing profile - Out: false, - }, }) } } diff --git a/aws-source/adapters/lambda-layer.go b/aws-source/adapters/lambda-layer.go index c7b73e46..2b68992d 100644 --- a/aws-source/adapters/lambda-layer.go +++ b/aws-source/adapters/lambda-layer.go @@ -53,11 +53,6 @@ func layerItemMapper(_, scope string, awsItem *types.LayersListItem) (*sdp.Item, Query: fmt.Sprintf("%v:%v", *awsItem.LayerName, awsItem.LatestMatchingVersion.Version), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/network-firewall-firewall-policy.go b/aws-source/adapters/network-firewall-firewall-policy.go index 7c427195..a2621015 100644 --- a/aws-source/adapters/network-firewall-firewall-policy.go +++ b/aws-source/adapters/network-firewall-firewall-policy.go @@ -88,10 +88,6 @@ func firewallPolicyGetFunc(ctx context.Context, client networkFirewallClient, sc Method: sdp.QueryMethod_SEARCH, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -106,10 +102,6 @@ func firewallPolicyGetFunc(ctx context.Context, client networkFirewallClient, sc Query: *resp.FirewallPolicy.TLSInspectionConfigurationArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-firewall.go b/aws-source/adapters/network-firewall-firewall.go index d0074735..ad366de3 100644 --- a/aws-source/adapters/network-firewall-firewall.go +++ b/aws-source/adapters/network-firewall-firewall.go @@ -118,10 +118,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: logGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } case types.LogDestinationTypeS3: @@ -136,10 +132,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: bucketName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } case types.LogDestinationTypeKinesisDataFirehose: @@ -154,10 +146,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: deliveryStream, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -173,10 +161,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *uf.ResourcePolicy, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -190,11 +174,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *config.FirewallPolicyArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Policy will affect the firewall but not the other way around - In: true, - Out: false, - }, }) } } @@ -209,11 +188,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *mapping.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to public subnets could affect the firewall - In: true, - Out: false, - }, }) } } @@ -227,11 +201,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *config.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the VPC could affect the firewall - In: true, - Out: false, - }, }) } @@ -248,11 +217,6 @@ func firewallGetFunc(ctx context.Context, client networkFirewallClient, scope st Query: *state.Attachment.SubnetId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to public subnets could affect the firewall - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-rule-group.go b/aws-source/adapters/network-firewall-rule-group.go index 4e0c994b..a8c5fdca 100644 --- a/aws-source/adapters/network-firewall-rule-group.go +++ b/aws-source/adapters/network-firewall-rule-group.go @@ -79,10 +79,6 @@ func ruleGroupGetFunc(ctx context.Context, client networkFirewallClient, scope s Query: *resp.RuleGroupResponse.SnsTopic, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) } } @@ -97,10 +93,6 @@ func ruleGroupGetFunc(ctx context.Context, client networkFirewallClient, scope s Query: *resp.RuleGroupResponse.SourceMetadata.SourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: false, - }, }) } } diff --git a/aws-source/adapters/network-firewall-tls-inspection-configuration.go b/aws-source/adapters/network-firewall-tls-inspection-configuration.go index 460f45e4..90b16bfd 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration.go @@ -82,10 +82,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *utic.Properties.CertificateAuthority.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -102,10 +98,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *cert.CertificateArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -124,10 +116,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *config.CertificateAuthorityArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -143,10 +131,6 @@ func tlsInspectionConfigurationGetFunc(ctx context.Context, client networkFirewa Query: *serverCert.ResourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkfirewall.go b/aws-source/adapters/networkfirewall.go index c0c91b9f..18ead662 100644 --- a/aws-source/adapters/networkfirewall.go +++ b/aws-source/adapters/networkfirewall.go @@ -36,10 +36,6 @@ func encryptionConfigurationLink(config *types.EncryptionConfiguration, scope st Query: *config.KeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } } else { return &sdp.LinkedItemQuery{ @@ -49,10 +45,6 @@ func encryptionConfigurationLink(config *types.EncryptionConfiguration, scope st Query: *config.KeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, } } } diff --git a/aws-source/adapters/networkmanager-connect-attachment.go b/aws-source/adapters/networkmanager-connect-attachment.go index f55bd4cc..5c2afb45 100644 --- a/aws-source/adapters/networkmanager-connect-attachment.go +++ b/aws-source/adapters/networkmanager-connect-attachment.go @@ -50,10 +50,6 @@ func connectAttachmentItemMapper(_, scope string, ca *types.ConnectAttachment) ( Query: *ca.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -66,10 +62,6 @@ func connectAttachmentItemMapper(_, scope string, ca *types.ConnectAttachment) ( Query: *ca.Attachment.CoreNetworkArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-connect-peer-association.go b/aws-source/adapters/networkmanager-connect-peer-association.go index 5e59589b..115c0b93 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-connect-peer-association.go @@ -46,10 +46,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: *a.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -58,10 +54,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: *a.ConnectPeerId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -85,10 +77,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -100,10 +88,6 @@ func connectPeerAssociationsOutputMapper(_ context.Context, _ *networkmanager.Cl Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } diff --git a/aws-source/adapters/networkmanager-connect-peer.go b/aws-source/adapters/networkmanager-connect-peer.go index 2d178120..5e71b859 100644 --- a/aws-source/adapters/networkmanager-connect-peer.go +++ b/aws-source/adapters/networkmanager-connect-peer.go @@ -43,10 +43,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.Configuration.CoreNetworkAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -59,10 +55,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.Configuration.PeerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -76,10 +68,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *config.CoreNetworkAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) if config.PeerAddress != nil { @@ -91,10 +79,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *config.PeerAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -107,10 +91,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*config.CoreNetworkAsn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -123,10 +103,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*config.PeerAsn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -141,10 +117,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -158,10 +130,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.SubnetArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -175,10 +143,6 @@ func connectPeerGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.ConnectAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-connection.go b/aws-source/adapters/networkmanager-connection.go index 653e79da..a439d146 100644 --- a/aws-source/adapters/networkmanager-connection.go +++ b/aws-source/adapters/networkmanager-connection.go @@ -47,10 +47,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -63,10 +59,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -78,10 +70,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.ConnectedLinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -93,10 +81,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -108,10 +92,6 @@ func connectionOutputMapper(_ context.Context, _ *networkmanager.Client, scope s Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.ConnectedDeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-core-network-policy.go b/aws-source/adapters/networkmanager-core-network-policy.go index 6b016a46..c46ce479 100644 --- a/aws-source/adapters/networkmanager-core-network-policy.go +++ b/aws-source/adapters/networkmanager-core-network-policy.go @@ -45,10 +45,6 @@ func coreNetworkPolicyItemMapper(_, scope string, cn *types.CoreNetworkPolicy) ( Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-core-network.go b/aws-source/adapters/networkmanager-core-network.go index accaadd8..8e9d8da1 100644 --- a/aws-source/adapters/networkmanager-core-network.go +++ b/aws-source/adapters/networkmanager-core-network.go @@ -44,10 +44,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -56,10 +52,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } @@ -72,10 +64,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: *cn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -88,10 +76,6 @@ func coreNetworkGetFunc(ctx context.Context, client NetworkManagerClient, scope Query: strconv.FormatInt(*edge.Asn, 10), Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-device.go b/aws-source/adapters/networkmanager-device.go index ecbf784e..6201423d 100644 --- a/aws-source/adapters/networkmanager-device.go +++ b/aws-source/adapters/networkmanager-device.go @@ -47,10 +47,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -59,10 +55,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithTypeAndGlobalNetwork(*s.GlobalNetworkId, "device", *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -71,10 +63,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -87,10 +75,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -102,10 +86,6 @@ func deviceOutputMapper(_ context.Context, _ *networkmanager.Client, scope strin Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceArn), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-global-network.go b/aws-source/adapters/networkmanager-global-network.go index 71d4d125..9c120ef5 100644 --- a/aws-source/adapters/networkmanager-global-network.go +++ b/aws-source/adapters/networkmanager-global-network.go @@ -41,10 +41,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -53,10 +49,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -65,10 +57,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -77,10 +65,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -89,10 +73,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -101,10 +81,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -113,10 +89,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -125,10 +97,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, { Query: &sdp.Query{ @@ -137,10 +105,6 @@ func globalNetworkOutputMapper(_ context.Context, client *networkmanager.Client, Query: *gn.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-link-association.go b/aws-source/adapters/networkmanager-link-association.go index 57c240e3..a4fc454f 100644 --- a/aws-source/adapters/networkmanager-link-association.go +++ b/aws-source/adapters/networkmanager-link-association.go @@ -48,10 +48,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -60,10 +56,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -72,10 +64,6 @@ func linkAssociationOutputMapper(_ context.Context, _ *networkmanager.Client, sc Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-link.go b/aws-source/adapters/networkmanager-link.go index 48b93e38..9858d8bd 100644 --- a/aws-source/adapters/networkmanager-link.go +++ b/aws-source/adapters/networkmanager-link.go @@ -43,10 +43,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -55,10 +51,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithTypeAndGlobalNetwork(*s.GlobalNetworkId, "link", *s.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } @@ -71,10 +63,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -86,10 +74,6 @@ func linkOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.LinkArn), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-network-resource-relationship.go b/aws-source/adapters/networkmanager-network-resource-relationship.go index 30adbd9f..cd6019cb 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship.go @@ -71,10 +71,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-device": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -84,10 +80,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-link": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -97,10 +89,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "networkmanager-site": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -110,10 +98,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: idWithGlobalNetwork(*input.GlobalNetworkId, toArn.ResourceID()), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-connection": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -123,10 +107,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-direct-connect-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -136,10 +116,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "directconnect-virtual-interface": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -149,10 +125,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-customer-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -162,10 +134,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -175,10 +143,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-attachment": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -188,10 +152,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-connect-peer": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -201,10 +161,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-transit-gateway-route-table": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -214,10 +170,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) case "ec2-vpn-connection": item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -227,10 +179,6 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage Query: toArn.ResourceID(), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) default: // skip unknown item types diff --git a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go index 9c0b0375..a0a6af96 100644 --- a/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go +++ b/aws-source/adapters/networkmanager-site-to-site-vpn-attachment.go @@ -49,10 +49,6 @@ func siteToSiteVpnAttachmentItemMapper(_, scope string, awsItem *types.SiteToSit Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -77,10 +73,6 @@ func siteToSiteVpnAttachmentItemMapper(_, scope string, awsItem *types.SiteToSit Query: *awsItem.VpnConnectionArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-site.go b/aws-source/adapters/networkmanager-site.go index 1459c70d..fa87210b 100644 --- a/aws-source/adapters/networkmanager-site.go +++ b/aws-source/adapters/networkmanager-site.go @@ -47,10 +47,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: *s.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, { Query: &sdp.Query{ @@ -59,10 +55,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, { Query: &sdp.Query{ @@ -71,10 +63,6 @@ func siteOutputMapper(_ context.Context, _ *networkmanager.Client, scope string, Query: idWithGlobalNetwork(*s.GlobalNetworkId, *s.SiteId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go index 7cee9d11..1211ceae 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go +++ b/aws-source/adapters/networkmanager-transit-gateway-connect-peer-association.go @@ -41,10 +41,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: *a.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -57,10 +53,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.DeviceId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -72,10 +64,6 @@ func transitGatewayConnectPeerAssociationsOutputMapper(_ context.Context, _ *net Query: idWithGlobalNetwork(*a.GlobalNetworkId, *a.LinkId), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering.go b/aws-source/adapters/networkmanager-transit-gateway-peering.go index 2c5b38d7..0f2ecdca 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering.go @@ -49,10 +49,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.Peering.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -75,10 +71,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.TransitGatewayPeeringAttachmentId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -92,10 +84,6 @@ func transitGatewayPeeringItemMapper(_, scope string, awsItem *types.TransitGate Query: *awsItem.TransitGatewayArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration.go b/aws-source/adapters/networkmanager-transit-gateway-registration.go index 1e81be97..f695c818 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration.go @@ -45,10 +45,6 @@ func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager Query: *r.GlobalNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }, }, } @@ -63,10 +59,6 @@ func transitGatewayRegistrationOutputMapper(_ context.Context, _ *networkmanager Query: *r.TransitGatewayArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go index 248aab5f..45667804 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go +++ b/aws-source/adapters/networkmanager-transit-gateway-route-table-attachment.go @@ -48,10 +48,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } @@ -63,10 +59,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.PeeringId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -80,10 +72,6 @@ func transitGatewayRouteTableAttachmentItemMapper(_, scope string, awsItem *type Query: *awsItem.TransitGatewayRouteTableArn, Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/aws-source/adapters/networkmanager-vpc-attachment.go b/aws-source/adapters/networkmanager-vpc-attachment.go index 764effec..1ef60ad9 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment.go +++ b/aws-source/adapters/networkmanager-vpc-attachment.go @@ -48,10 +48,6 @@ func vpcAttachmentItemMapper(_, scope string, awsItem *types.VpcAttachment) (*sd Query: *awsItem.Attachment.CoreNetworkId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/rds-db-cluster.go b/aws-source/adapters/rds-db-cluster.go index 418acd27..40a9adb0 100644 --- a/aws-source/adapters/rds-db-cluster.go +++ b/aws-source/adapters/rds-db-cluster.go @@ -50,11 +50,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.DBSubnetGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: false, - }, }) } @@ -67,11 +62,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } } @@ -85,11 +75,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: replica, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -103,11 +88,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *member.DBInstanceIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -121,12 +101,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *sg.VpcSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the security group can affect the cluster - In: true, - // The cluster won't affect the security group - Out: false, - }, }) } } @@ -139,12 +113,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the hosted zone can affect the cluster - In: true, - // The cluster won't affect the hosted zone - Out: false, - }, }) } @@ -157,12 +125,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -175,12 +137,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.ActivityStreamKinesisStreamName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the Kinesis stream can affect the cluster - In: true, - // Changes to the cluster can affect the Kinesis stream - Out: true, - }, }) } @@ -192,11 +148,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: endpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always linked - In: true, - Out: true, - }, }) } @@ -209,12 +160,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *optionGroup.DBClusterOptionGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the option group can affect the cluster - In: true, - // Changes to the cluster won't affect the option group - Out: false, - }, }) } } @@ -229,12 +174,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MasterUserSecret.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -248,12 +187,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MasterUserSecret.SecretArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the secret can affect the cluster - In: true, - // The cluster won't affect the secret - Out: false, - }, }) } } @@ -268,12 +201,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.MonitoringRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the IAM role can affect the cluster - In: true, - // The cluster won't affect the IAM role - Out: false, - }, }) } } @@ -288,12 +215,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.PerformanceInsightsKMSKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the KMS key can affect the cluster - In: true, - // The cluster won't affect the KMS key - Out: false, - }, }) } } @@ -307,11 +228,6 @@ func dBClusterOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *cluster.ReplicationSourceIdentifier, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/rds-db-instance.go b/aws-source/adapters/rds-db-instance.go index fb120f75..d0cfac1f 100644 --- a/aws-source/adapters/rds-db-instance.go +++ b/aws-source/adapters/rds-db-instance.go @@ -128,11 +128,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.Endpoint.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -144,12 +139,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.Endpoint.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the endpoint - In: true, - // The instance won't affect the hosted zone - Out: false, - }, }) } } @@ -163,12 +152,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *sg.VpcSecurityGroupId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the security group can affect the instance - In: true, - // The instance won't affect the security group - Out: false, - }, }) } } @@ -182,12 +165,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *paramGroup.DBParameterGroupName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the parameter group can affect the instance - In: true, - // The instance won't affect the parameter group - Out: false, - }, }) } } @@ -200,12 +177,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *dbSubnetGroup, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet group can affect the instance - In: true, - // The instance won't affect the subnet group - Out: false, - }, }) } @@ -217,11 +188,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.DBClusterIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -235,12 +201,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.KmsKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } } @@ -254,11 +214,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.EnhancedMonitoringResourceArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -272,12 +227,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MonitoringRoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role can affect the instance - In: true, - // The instance won't affect the role - Out: false, - }, }) } } @@ -292,12 +241,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.PerformanceInsightsKMSKeyId, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } } @@ -312,12 +255,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *role.RoleArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the role can affect the instance - In: true, - // The instance won't affect the role - Out: false, - }, }) } } @@ -331,11 +268,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ActivityStreamKinesisStreamName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } @@ -348,11 +280,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.AwsBackupRecoveryPointArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -367,12 +294,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.CustomIamInstanceProfile, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the instance profile can affect the instance - In: true, - // The instance won't affect the instance profile - Out: false, - }, }) } } @@ -387,11 +308,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *replication.DBInstanceAutomatedBackupsArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -406,11 +322,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ListenerEndpoint.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS always links - In: true, - Out: true, - }, }) } @@ -422,12 +333,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.ListenerEndpoint.HostedZoneId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the endpoint - In: true, - // The instance won't affect the hosted zone - Out: false, - }, }) } } @@ -441,12 +346,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MasterUserSecret.KmsKeyId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the KMS key can affect the instance - In: true, - // The instance won't affect the KMS key - Out: false, - }, }) } @@ -459,12 +358,6 @@ func dBInstanceOutputMapper(ctx context.Context, client rdsClient, scope string, Query: *instance.MasterUserSecret.SecretArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret can affect the instance - In: true, - // The instance won't affect the secret - Out: false, - }, }) } } diff --git a/aws-source/adapters/rds-db-subnet-group.go b/aws-source/adapters/rds-db-subnet-group.go index 9cc43031..5f999062 100644 --- a/aws-source/adapters/rds-db-subnet-group.go +++ b/aws-source/adapters/rds-db-subnet-group.go @@ -50,12 +50,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *sg.VpcId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the VPC can affect the subnet group - In: true, - // The subnet group won't affect the VPC - Out: false, - }, }) } @@ -68,12 +62,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *subnet.SubnetIdentifier, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the subnet can affect the subnet group - In: true, - // The subnet group won't affect the subnet - Out: false, - }, }) } @@ -87,12 +75,6 @@ func dBSubnetGroupOutputMapper(ctx context.Context, client rdsClient, scope stri Query: *subnet.SubnetOutpost.Arn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the outpost can affect the subnet group - In: true, - // The subnet group won't affect the outpost - Out: false, - }, }) } } diff --git a/aws-source/adapters/route53-health-check.go b/aws-source/adapters/route53-health-check.go index ea78fcad..93aaf6ec 100644 --- a/aws-source/adapters/route53-health-check.go +++ b/aws-source/adapters/route53-health-check.go @@ -103,11 +103,6 @@ func healthCheckItemMapper(_, scope string, awsItem *HealthCheck) (*sdp.Item, er Method: sdp.QueryMethod_SEARCH, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/route53-hosted-zone.go b/aws-source/adapters/route53-hosted-zone.go index 2af9708f..6fc5f882 100644 --- a/aws-source/adapters/route53-hosted-zone.go +++ b/aws-source/adapters/route53-hosted-zone.go @@ -59,12 +59,6 @@ func hostedZoneItemMapper(_, scope string, awsItem *types.HostedZone) (*sdp.Item Query: *awsItem.Id, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the hosted zone can affect the resource record set - Out: true, - // The resource record set won't affect the hosted zone - In: false, - }, }, }, } diff --git a/aws-source/adapters/route53-resource-record-set.go b/aws-source/adapters/route53-resource-record-set.go index 8fd99151..cb9ac5c0 100644 --- a/aws-source/adapters/route53-resource-record-set.go +++ b/aws-source/adapters/route53-resource-record-set.go @@ -125,10 +125,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: recordName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -142,11 +138,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *awsItem.AliasTarget.DNSName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS aliases links - In: true, - Out: true, - }, }) } } @@ -160,11 +151,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *record.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS aliases links - In: true, - Out: true, - }, }) } } @@ -177,11 +163,6 @@ func resourceRecordSetItemMapper(_, scope string, awsItem *types.ResourceRecordS Query: *awsItem.HealthCheckId, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Health check links tightly - In: true, - Out: true, - }, }) } diff --git a/aws-source/adapters/s3.go b/aws-source/adapters/s3.go index 7e0665ed..2e187d5e 100644 --- a/aws-source/adapters/s3.go +++ b/aws-source/adapters/s3.go @@ -441,11 +441,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: url, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // HTTP always linked - In: true, - Out: true, - }, }) } } @@ -462,11 +457,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *lambdaConfig.LambdaFunctionArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -482,11 +472,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *q.QueueArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -502,11 +487,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *topic.TopicArn, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -521,11 +501,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.LoggingEnabled.TargetBucket, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -542,11 +517,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.InventoryConfiguration.Destination.S3BucketDestination.Bucket, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } @@ -570,11 +540,6 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s Query: *bucket.AnalyticsConfiguration.StorageClassAnalysis.DataExport.Destination.S3BucketDestination.Bucket, Scope: FormatScope(a.AccountID, a.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // Tightly coupled - In: true, - Out: true, - }, }) } } diff --git a/aws-source/adapters/sns-data-protection-policy.go b/aws-source/adapters/sns-data-protection-policy.go index bc3fdae3..5d54efe7 100644 --- a/aws-source/adapters/sns-data-protection-policy.go +++ b/aws-source/adapters/sns-data-protection-policy.go @@ -51,14 +51,6 @@ func getDataProtectionPolicyFunc(ctx context.Context, client dataProtectionPolic Query: *input.ResourceArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Deleting the topic will delete the inline policy - In: true, - // Changing policy will affect the topic: - // a new statement denying credit card numbers will make the topic stop delivering messages - // containing credit card numbers - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/sns-platform-application.go b/aws-source/adapters/sns-platform-application.go index 89893c5a..cfd00ead 100644 --- a/aws-source/adapters/sns-platform-application.go +++ b/aws-source/adapters/sns-platform-application.go @@ -57,12 +57,6 @@ func getPlatformApplicationFunc(ctx context.Context, client platformApplicationC Query: *input.PlatformApplicationArn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // An unhealthy endpoint won't affect the platform application - In: false, - // If platform application is unhealthy, then endpoints won't get notifications - Out: true, - }, }) return item, nil diff --git a/aws-source/adapters/sns-subscription.go b/aws-source/adapters/sns-subscription.go index f134ffae..eeb67242 100644 --- a/aws-source/adapters/sns-subscription.go +++ b/aws-source/adapters/sns-subscription.go @@ -54,12 +54,6 @@ func getSubsFunc(ctx context.Context, client subsCli, scope string, input *sns.G Query: topicArn.(string), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If topic is not healthy, subscription will not work - In: true, - // Subscription won't affect the topic - Out: false, - }, }) } @@ -72,12 +66,6 @@ func getSubsFunc(ctx context.Context, client subsCli, scope string, input *sns.G Query: arn.ResourceID(), Scope: FormatScope(arn.AccountID, arn.Region), }, - BlastPropagation: &sdp.BlastPropagation{ - // If role is not healthy, subscription will not work - In: true, - // Subscription won't affect the role - Out: false, - }, }) } } diff --git a/aws-source/adapters/sns-topic.go b/aws-source/adapters/sns-topic.go index fac839d3..fd7d4368 100644 --- a/aws-source/adapters/sns-topic.go +++ b/aws-source/adapters/sns-topic.go @@ -54,12 +54,6 @@ func getTopicFunc(ctx context.Context, client topicClient, scope string, input * Query: fmt.Sprint(kmsMasterKeyID), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the key will affect the topic - In: true, - // Changing the topic won't affect the key - Out: false, - }, }) } diff --git a/aws-source/adapters/sqs-queue.go b/aws-source/adapters/sqs-queue.go index ee628ef4..7283bf7d 100644 --- a/aws-source/adapters/sqs-queue.go +++ b/aws-source/adapters/sqs-queue.go @@ -58,10 +58,6 @@ func getFunc(ctx context.Context, client sqsClient, scope string, input *sqs.Get Query: *input.QueueUrl, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, } @@ -74,12 +70,6 @@ func getFunc(ctx context.Context, client sqsClient, scope string, input *sqs.Get Query: arn, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // If event source mappings change, it doesn't affect the queue itself - In: false, - // If the SQS queue is updated, event source mappings will be affected - Out: true, - }, }) } diff --git a/aws-source/adapters/sqs-queue_test.go b/aws-source/adapters/sqs-queue_test.go index 1500b11e..11524d97 100644 --- a/aws-source/adapters/sqs-queue_test.go +++ b/aws-source/adapters/sqs-queue_test.go @@ -77,13 +77,6 @@ func TestGetFunc(t *testing.T) { if httpLink.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected HTTP link method to be SEARCH, got %v", httpLink.GetQuery().GetMethod()) } - // Test HTTP link blast propagation (bidirectional) - if httpLink.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected HTTP link blast propagation In to be true, got %v", httpLink.GetBlastPropagation().GetIn()) - } - if httpLink.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected HTTP link blast propagation Out to be true, got %v", httpLink.GetBlastPropagation().GetOut()) - } // Test Lambda Event Source Mapping link lambdaLink := item.GetLinkedItemQueries()[1] @@ -96,13 +89,6 @@ func TestGetFunc(t *testing.T) { if lambdaLink.GetQuery().GetQuery() != "arn:aws:sqs:us-west-2:123456789012:MyQueue" { t.Errorf("Expected Lambda link query to be the Queue ARN, got %s", lambdaLink.GetQuery().GetQuery()) } - // Test Lambda Event Source Mapping link blast propagation (outgoing only) - if lambdaLink.GetBlastPropagation().GetIn() != false { - t.Errorf("Expected Lambda link blast propagation In to be false, got %v", lambdaLink.GetBlastPropagation().GetIn()) - } - if lambdaLink.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected Lambda link blast propagation Out to be true, got %v", lambdaLink.GetBlastPropagation().GetOut()) - } } func TestSqsQueueSearchInputMapper(t *testing.T) { diff --git a/cmd/request_query.go b/cmd/request_query.go index cb7fa349..6c083703 100644 --- a/cmd/request_query.go +++ b/cmd/request_query.go @@ -171,8 +171,7 @@ func CreateQuery() (*sdp.Query, error) { Deadline: timestamppb.New(time.Now().Add(10 * time.Hour)), UUID: u[:], RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: viper.GetUint32("link-depth"), - FollowOnlyBlastPropagation: viper.GetBool("blast-radius"), + LinkDepth: viper.GetUint32("link-depth"), }, IgnoreCache: viper.GetBool("ignore-cache"), }, nil @@ -196,5 +195,4 @@ func init() { requestQueryCmd.PersistentFlags().String("snapshot-description", "none", "The snapshot description of the query results") requestQueryCmd.PersistentFlags().Uint32("link-depth", 0, "How deeply to link") - requestQueryCmd.PersistentFlags().Bool("blast-radius", false, "Whether to query using blast radius, note that if using this option, link-depth should be set to > 0") } diff --git a/go/discovery/enginerequests.go b/go/discovery/enginerequests.go index affb09ca..3b04bf82 100644 --- a/go/discovery/enginerequests.go +++ b/go/discovery/enginerequests.go @@ -113,7 +113,6 @@ func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { if query.GetRecursionBehaviour() != nil { span.SetAttributes( attribute.Int("ovm.sdp.linkDepth", int(query.GetRecursionBehaviour().GetLinkDepth())), - attribute.Bool("ovm.sdp.followOnlyBlastPropagation", query.GetRecursionBehaviour().GetFollowOnlyBlastPropagation()), ) } diff --git a/go/sdp-go/items.go b/go/sdp-go/items.go index 0f106653..ffee8cf1 100644 --- a/go/sdp-go/items.go +++ b/go/sdp-go/items.go @@ -74,12 +74,11 @@ func (i *Item) Hash() string { return HashSum(([]byte(fmt.Sprint(i.GloballyUniqueName())))) } -// IsEqual compares two Edges for equality by checking the From reference, -// To reference, and BlastPropagation settings. +// IsEqual compares two Edges for equality by checking the From reference +// and To reference. func (e *Edge) IsEqual(other *Edge) bool { return e.GetFrom().IsEqual(other.GetFrom()) && - e.GetTo().IsEqual(other.GetTo()) && - e.GetBlastPropagation().IsEqual(other.GetBlastPropagation()) + e.GetTo().IsEqual(other.GetTo()) } // Hash Returns a 12 character hash for the item. This is likely but not diff --git a/go/sdp-go/link_extract.go b/go/sdp-go/link_extract.go index d0e6c383..9f03b99f 100644 --- a/go/sdp-go/link_extract.go +++ b/go/sdp-go/link_extract.go @@ -96,10 +96,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: ip.String(), Scope: "global", }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: true, - }, }, } } @@ -118,15 +114,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: "global", }, - BlastPropagation: &BlastPropagation{ - // If we are referencing a HTTP URL, I think it's safe - // to assume that this is something that the current - // resource depends on and therefore that the blast - // radius should propagate inwards. This is a bit of a - // guess though... - In: true, - Out: false, - }, }, } } @@ -145,10 +132,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: "global", }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, }, } } @@ -214,10 +197,6 @@ func extractLinksFromStringValue(val string) []*LinkedItemQuery { Query: val, Scope: scope, }, - BlastPropagation: &BlastPropagation{ - In: true, - Out: false, - }, }, } } diff --git a/go/sdp-go/progress.go b/go/sdp-go/progress.go index fe1f40c6..f53c5094 100644 --- a/go/sdp-go/progress.go +++ b/go/sdp-go/progress.go @@ -664,17 +664,15 @@ func TranslateLinksToEdges(item *Item) (*Item, []*Edge) { for _, li := range lis { edges = append(edges, &Edge{ - From: item.Reference(), - To: li.GetItem(), - BlastPropagation: li.GetBlastPropagation(), + From: item.Reference(), + To: li.GetItem(), }) } for _, liq := range liqs { edges = append(edges, &Edge{ - From: item.Reference(), - To: liq.GetQuery().Reference(), - BlastPropagation: liq.GetBlastPropagation(), + From: item.Reference(), + To: liq.GetQuery().Reference(), }) } diff --git a/go/sdp-go/proto_clone_test.go b/go/sdp-go/proto_clone_test.go index 0a73c4eb..863c8f7d 100644 --- a/go/sdp-go/proto_clone_test.go +++ b/go/sdp-go/proto_clone_test.go @@ -48,8 +48,7 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { Scope: "scope", UUID: u[:], RecursionBehaviour: &Query_RecursionBehaviour{ - LinkDepth: 5, - FollowOnlyBlastPropagation: true, + LinkDepth: 5, }, IgnoreCache: true, Deadline: timestamppb.Now(), @@ -97,17 +96,9 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { }) t.Run("All other SDP types", func(t *testing.T) { - // BlastPropagation - bp := &BlastPropagation{In: true, Out: false} - bpClone := proto.Clone(bp).(*BlastPropagation) - if !proto.Equal(bp, bpClone) { - t.Errorf("proto.Clone failed for BlastPropagation") - } - // LinkedItemQuery liq := &LinkedItemQuery{ - Query: &Query{Type: "test", Method: QueryMethod_LIST}, - BlastPropagation: bp, + Query: &Query{Type: "test", Method: QueryMethod_LIST}, } liqClone := proto.Clone(liq).(*LinkedItemQuery) if !proto.Equal(liq, liqClone) { @@ -116,8 +107,7 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { // LinkedItem li := &LinkedItem{ - Item: &Reference{Type: "test", Scope: "scope"}, - BlastPropagation: bp, + Item: &Reference{Type: "test", Scope: "scope"}, } liClone := proto.Clone(li).(*LinkedItem) if !proto.Equal(li, liClone) { diff --git a/go/sdpcache/item_generator_test.go b/go/sdpcache/item_generator_test.go index ebe9bc91..602a5c8b 100644 --- a/go/sdpcache/item_generator_test.go +++ b/go/sdpcache/item_generator_test.go @@ -82,8 +82,7 @@ func GenerateRandomItem() *sdp.Item { Method: sdp.QueryMethod(rand.Intn(3)), Query: randSeq(rand.Intn(MaxAttributeValueLength)), RecursionBehaviour: &sdp.Query_RecursionBehaviour{ - LinkDepth: rand.Uint32(), - FollowOnlyBlastPropagation: rand.Intn(2) == 0, + LinkDepth: rand.Uint32(), }, Scope: randSeq(rand.Intn(MaxAttributeKeyLength)), }} diff --git a/k8s-source/adapters/clusterrolebinding.go b/k8s-source/adapters/clusterrolebinding.go index af61c45a..27d09861 100644 --- a/k8s-source/adapters/clusterrolebinding.go +++ b/k8s-source/adapters/clusterrolebinding.go @@ -19,13 +19,6 @@ func clusterRoleBindingExtractor(resource *v1.ClusterRoleBinding, scope string) Query: resource.RoleRef.Name, Type: resource.RoleRef.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect the things bound to it since the - // role contains the permissions - In: true, - // Changes to the binding won't affect the role - Out: false, - }, }) for _, subject := range resource.Subjects { @@ -44,12 +37,6 @@ func clusterRoleBindingExtractor(resource *v1.ClusterRoleBinding, scope string) Query: subject.Name, Type: subject.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the group won't affect the binding itself - In: false, - // Changes to the binding will affect the group it's bound to - Out: true, - }, }) } diff --git a/k8s-source/adapters/deployment.go b/k8s-source/adapters/deployment.go index ddfd02e7..b4d64368 100644 --- a/k8s-source/adapters/deployment.go +++ b/k8s-source/adapters/deployment.go @@ -55,11 +55,6 @@ func newDeploymentAdapter(cs *kubernetes.Clientset, cluster string, namespaces [ Query: matches[1], Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // These are tightly bound - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index a1facc2f..ef90ec11 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -27,11 +27,6 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Query: address.Hostname, Type: "dns", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } @@ -43,12 +38,6 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Method: sdp.QueryMethod_GET, Query: *address.NodeName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node can affect the endpoint - In: true, - // Changes to the endpoint cannot affect the node - Out: false, - }, }) } @@ -60,20 +49,11 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem Query: address.IP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) } if address.TargetRef != nil { - queries = append(queries, ObjectReferenceToQuery(address.TargetRef, sd, &sdp.BlastPropagation{ - // These are tightly coupled - In: true, - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(address.TargetRef, sd)) } } } diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index 38d32be8..b2fc027e 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -29,11 +29,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: *endpoint.Hostname, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } @@ -45,21 +40,11 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: *endpoint.NodeName, Scope: sd.ClusterName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node can affect the endpoint - In: true, - // Changes to the endpoint cannot affect the node - Out: false, - }, }) } if endpoint.TargetRef != nil { - queries = append(queries, ObjectReferenceToQuery(endpoint.TargetRef, sd, &sdp.BlastPropagation{ - // Changes to the pod could affect the endpoint and vice versa - In: true, - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(endpoint.TargetRef, sd)) } for _, address := range endpoint.Addresses { @@ -72,11 +57,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) case v1.AddressTypeFQDN: queries = append(queries, &sdp.LinkedItemQuery{ @@ -86,11 +66,6 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li Query: address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index 5d284787..6f331f59 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -393,17 +393,6 @@ func (s *KubeTypeAdapter[Resource, ResourceList]) resourceToItem(resource Resour Query: ref.Name, Scope: sd.String(), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the owner will definitely affect the owned e.g. - // changes to a deployment will affect the pods in that - // deployment - In: true, - // Changes to the owned may affect the owner e.g. changing a - // secret could affect a pod, but if all pods used that secret - // then the change should propagate from the pods to the - // deployment too - Out: true, - }, }) } @@ -434,7 +423,7 @@ func (s *KubeTypeAdapter[Resource, ResourceList]) resourceToItem(resource Resour // request. Note that you must provide the parent scope since the reference // could be an object in a different namespace, if it is we need to re-use the // cluster name from the parent scope -func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetails, blastProp *sdp.BlastPropagation) *sdp.LinkedItemQuery { +func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetails) *sdp.LinkedItemQuery { if ref == nil { return nil } @@ -449,7 +438,6 @@ func ObjectReferenceToQuery(ref *corev1.ObjectReference, parentScope ScopeDetail Query: ref.Name, Scope: parentScope.String(), }, - BlastPropagation: blastProp, } } diff --git a/k8s-source/adapters/generic_source_test.go b/k8s-source/adapters/generic_source_test.go index a688efc9..2635e80a 100644 --- a/k8s-source/adapters/generic_source_test.go +++ b/k8s-source/adapters/generic_source_test.go @@ -727,12 +727,10 @@ func TestObjectReferenceToQuery(t *testing.T) { Name: "foo", } - b := &sdp.BlastPropagation{} - query := ObjectReferenceToQuery(ref, ScopeDetails{ ClusterName: "test-cluster", Namespace: "default", - }, b) + }) if query.GetQuery().GetType() != "Pod" { t.Errorf("expected type Pod, got %s", query.GetQuery().GetType()) @@ -745,14 +743,10 @@ func TestObjectReferenceToQuery(t *testing.T) { if query.GetQuery().GetScope() != "test-cluster.default" { t.Errorf("expected scope to be test-cluster.default, got %s", query.GetQuery().GetScope()) } - - if query.GetBlastPropagation() != b { - t.Errorf("expected blast propagation to be %v, got %v", b, query.GetBlastPropagation()) - } }) t.Run("with a nil object reference", func(t *testing.T) { - query := ObjectReferenceToQuery(nil, ScopeDetails{}, nil) + query := ObjectReferenceToQuery(nil, ScopeDetails{}) if query != nil { t.Errorf("expected nil query, got %v", query) diff --git a/k8s-source/adapters/horizontalpodautoscaler.go b/k8s-source/adapters/horizontalpodautoscaler.go index 654b9541..b2c63125 100644 --- a/k8s-source/adapters/horizontalpodautoscaler.go +++ b/k8s-source/adapters/horizontalpodautoscaler.go @@ -19,12 +19,6 @@ func horizontalPodAutoscalerExtractor(resource *v2.HorizontalPodAutoscaler, scop Query: resource.Spec.ScaleTargetRef.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the target won't affect the hpa - In: false, - // Changes to the hpa can affect the target i.e. by scaling the pods - Out: true, - }, }) return queries, nil diff --git a/k8s-source/adapters/ingress.go b/k8s-source/adapters/ingress.go index c0cc4a7d..d400ecb5 100644 --- a/k8s-source/adapters/ingress.go +++ b/k8s-source/adapters/ingress.go @@ -20,13 +20,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: *resource.Spec.IngressClassName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the ingress (e.g. nginx) class can affect the - // ingresses that use it - In: true, - // Changes to an ingress wont' affect the class - Out: false, - }, }) } @@ -39,12 +32,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: resource.Spec.DefaultBackend.Service.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service affects the ingress' endpoints - In: true, - // Changing an ingress does not affect the service - Out: false, - }, }) } @@ -56,13 +43,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: linkRes.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the default backend won't affect the ingress - // itself - In: false, - // Changes to the ingress could affect the default backend - Out: true, - }, }) } } @@ -76,11 +56,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: rule.Host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate through rules - In: true, - Out: true, - }, }) } @@ -94,12 +69,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: path.Backend.Service.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service affects the ingress' endpoints - In: true, - // Changing an ingress does not affect the service - Out: false, - }, }) } @@ -111,14 +80,6 @@ func ingressExtractor(resource *v1.Ingress, scope string) ([]*sdp.LinkedItemQuer Query: path.Backend.Resource.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes can go in both directions here. An - // backend change can affect the ingress by rendering - // backend change can affect the ingress by rending - // it broken - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/job.go b/k8s-source/adapters/job.go index 1dab45e4..fa81d0d4 100644 --- a/k8s-source/adapters/job.go +++ b/k8s-source/adapters/job.go @@ -20,12 +20,6 @@ func jobExtractor(resource *v1.Job, scope string) ([]*sdp.LinkedItemQuery, error Query: LabelSelectorToQuery(resource.Spec.Selector), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to a job will replace the pods, changes to the pods - // could break the job - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/networkpolicy.go b/k8s-source/adapters/networkpolicy.go index 85de6a22..5af261a8 100644 --- a/k8s-source/adapters/networkpolicy.go +++ b/k8s-source/adapters/networkpolicy.go @@ -19,13 +19,6 @@ func NetworkPolicyExtractor(resource *v1.NetworkPolicy, scope string) ([]*sdp.Li Query: LabelSelectorToQuery(&resource.Spec.PodSelector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the network policy or anything else - // that shares it - In: false, - // Changes to the network policy will affect pods - Out: true, - }, }) var peers []v1.NetworkPolicyPeer @@ -53,13 +46,6 @@ func NetworkPolicyExtractor(resource *v1.NetworkPolicy, scope string) ([]*sdp.Li Query: LabelSelectorToQuery(ps), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the network policy or anything else - // that shares it - In: false, - // Changes to the network policy will affect pods - Out: true, - }, }) } } diff --git a/k8s-source/adapters/node.go b/k8s-source/adapters/node.go index 7186496f..6156c207 100644 --- a/k8s-source/adapters/node.go +++ b/k8s-source/adapters/node.go @@ -24,11 +24,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: addr.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over DNS - In: true, - Out: true, - }, }) case v1.NodeExternalIP, v1.NodeInternalIP: @@ -39,11 +34,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: addr.Address, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Always propagate over IP - In: true, - Out: true, - }, }) } } @@ -62,12 +52,6 @@ func linkedItemExtractor(resource *v1.Node, scope string) ([]*sdp.LinkedItemQuer Query: sections[1], Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume can affect the node - In: true, - // Changes to the node cannot affect the volume - Out: true, - }, }) } } diff --git a/k8s-source/adapters/persistentvolume.go b/k8s-source/adapters/persistentvolume.go index 561f4025..c94a1c68 100644 --- a/k8s-source/adapters/persistentvolume.go +++ b/k8s-source/adapters/persistentvolume.go @@ -28,12 +28,6 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: resource.Spec.PersistentVolumeSource.AWSElasticBlockStore.VolumeID, Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the EBS volume can affect the PV - In: true, - // Changes to the PV might affect the EBS volume - Out: true, - }, }) } @@ -52,24 +46,13 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: matches[1], Scope: "*", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the EFS access point can affect the PV - In: true, - // Changes to the PV won't affect the EFS access point - Out: false, - }, }) } } } if resource.Spec.ClaimRef != nil { - queries = append(queries, ObjectReferenceToQuery(resource.Spec.ClaimRef, sd, &sdp.BlastPropagation{ - // Changing claim might not affect the PV - In: false, - // Changing the PV will definitely affect the claim - Out: true, - })) + queries = append(queries, ObjectReferenceToQuery(resource.Spec.ClaimRef, sd)) } if resource.Spec.StorageClassName != "" { @@ -80,12 +63,6 @@ func PersistentVolumeExtractor(resource *v1.PersistentVolume, scope string) ([]* Query: resource.Spec.StorageClassName, Scope: sd.ClusterName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the storage class can affect the PV - In: true, - // Changes to the PV cannot affect the storage class - Out: false, - }, }) } diff --git a/k8s-source/adapters/persistentvolumeclaim.go b/k8s-source/adapters/persistentvolumeclaim.go index 65d655ae..c739c837 100644 --- a/k8s-source/adapters/persistentvolumeclaim.go +++ b/k8s-source/adapters/persistentvolumeclaim.go @@ -25,13 +25,6 @@ func PersistentVolumeClaimExtractor(resource *v1.PersistentVolumeClaim, scope st Query: resource.Spec.VolumeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume could affect the claim - In: true, - // Changes to the claim could affect the volume if there are - // other claims - Out: true, - }, }) } diff --git a/k8s-source/adapters/poddisruptionbudget.go b/k8s-source/adapters/poddisruptionbudget.go index 2ec34256..2a43e902 100644 --- a/k8s-source/adapters/poddisruptionbudget.go +++ b/k8s-source/adapters/poddisruptionbudget.go @@ -19,12 +19,6 @@ func podDisruptionBudgetExtractor(resource *v1.PodDisruptionBudget, scope string Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to pods won't affect the disruption budget - In: false, - // Changes to the disruption budget will affect pods - Out: true, - }, }) } diff --git a/k8s-source/adapters/pods.go b/k8s-source/adapters/pods.go index 45896aa4..3e9a6b01 100644 --- a/k8s-source/adapters/pods.go +++ b/k8s-source/adapters/pods.go @@ -30,12 +30,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Method: sdp.QueryMethod_GET, Query: resource.Spec.ServiceAccountName, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the service account can affect the pod - In: true, - // Changes to the pod cannot affect the service account - Out: false, - }, }) } @@ -50,13 +44,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.PersistentVolumeClaim.ClaimName, Type: "PersistentVolumeClaim", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the PVC will affect the pod - In: true, - // The pod can definitely affect the PVC, by filling it up - // for example - Out: true, - }, }) } @@ -69,12 +56,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.AWSElasticBlockStore.VolumeID, Type: "ec2-volume", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the volume will affect the pod - In: true, - // The pod can definitely affect the volume - Out: true, - }, }) } @@ -87,12 +68,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.Secret.SecretName, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -108,12 +83,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.NFS.Server, Type: "ip", }, - BlastPropagation: &sdp.BlastPropagation{ - // NFS server can affect the pod - In: true, - // Pod can't affect the NFS server - Out: false, - }, }) } else { queries = append(queries, &sdp.LinkedItemQuery{ @@ -123,12 +92,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Type: "dns", Query: vol.NFS.Server, }, - BlastPropagation: &sdp.BlastPropagation{ - // NFS server can affect the pod - In: true, - // Pod can't affect the NFS server - Out: false, - }, }) } } @@ -142,12 +105,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: vol.ConfigMap.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } @@ -162,12 +119,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: source.ConfigMap.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } @@ -179,12 +130,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: source.Secret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } } @@ -205,12 +150,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: env.ValueFrom.SecretKeyRef.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -222,12 +161,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: env.ValueFrom.ConfigMapKeyRef.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } } @@ -243,12 +176,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: envFrom.SecretRef.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret could easily break the pod - In: true, - // The pod however isn't going to affect a secret - Out: false, - }, }) } @@ -260,12 +187,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: envFrom.ConfigMapRef.Name, Type: "ConfigMap", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the config map could easily break the pod - In: true, - // The pod however isn't going to affect a config map - Out: false, - }, }) } } @@ -279,14 +200,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: resource.Spec.PriorityClassName, Type: "PriorityClass", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the priority class could break a pod by meaning that - // it would now be scheduled with a lower priority and could - // therefore end up pending for ages - In: true, - // The pod however isn't going to affect a priority class - Out: false, - }, }) } @@ -299,11 +212,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: ip.IP, Type: "ip", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs go in both directions - In: true, - Out: true, - }, }) } } else if resource.Status.PodIP != "" { @@ -314,11 +222,6 @@ func PodExtractor(resource *v1.Pod, scope string) ([]*sdp.LinkedItemQuery, error Query: resource.Status.PodIP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs go in both directions - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/replicaset.go b/k8s-source/adapters/replicaset.go index a4f96ad4..b38554c2 100644 --- a/k8s-source/adapters/replicaset.go +++ b/k8s-source/adapters/replicaset.go @@ -20,12 +20,6 @@ func replicaSetExtractor(resource *v1.ReplicaSet, scope string) ([]*sdp.LinkedIt Query: LabelSelectorToQuery(resource.Spec.Selector), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/replicationcontroller.go b/k8s-source/adapters/replicationcontroller.go index d30aa2c4..e4d6531b 100644 --- a/k8s-source/adapters/replicationcontroller.go +++ b/k8s-source/adapters/replicationcontroller.go @@ -22,12 +22,6 @@ func replicationControllerExtractor(resource *v1.ReplicationController, scope st }), Type: "Pod", }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } diff --git a/k8s-source/adapters/rolebinding.go b/k8s-source/adapters/rolebinding.go index a030abdf..bad7bdb8 100644 --- a/k8s-source/adapters/rolebinding.go +++ b/k8s-source/adapters/rolebinding.go @@ -29,13 +29,6 @@ func roleBindingExtractor(resource *v1.RoleBinding, scope string) ([]*sdp.Linked Namespace: subject.Namespace, }.String(), }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the subject (the group we're applying permissions - // to) won't affect the role or the binding - In: false, - // Changes to the binding will affect the subject - Out: true, - }, }) } @@ -61,13 +54,6 @@ func roleBindingExtractor(resource *v1.RoleBinding, scope string) ([]*sdp.Linked Query: resource.RoleRef.Name, Type: resource.RoleRef.Kind, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the role will affect the things bound to it since the - // role contains the permissions - In: true, - // Changes to the binding won't affect the role - Out: false, - }, }) return queries, nil diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index 8d24f928..6c49ed96 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -22,12 +22,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer }), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the service - In: true, - Out: true, - }, }) } @@ -51,11 +45,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ip, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } } @@ -68,11 +57,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: resource.Spec.ExternalName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always bidirectional - In: true, - Out: true, - }, }) } @@ -84,12 +68,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: resource.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // The service causes the endpoint to be created, so changes to the - // service can affect the endpoint and vice versa - In: true, - Out: true, - }, }) for _, ingress := range resource.Status.LoadBalancer.Ingress { @@ -101,11 +79,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ingress.IP, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always bidirectional - In: true, - Out: true, - }, }) } @@ -117,11 +90,6 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer Query: ingress.Hostname, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS is always bidirectional - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/serviceaccount.go b/k8s-source/adapters/serviceaccount.go index 5d202075..c9b6b586 100644 --- a/k8s-source/adapters/serviceaccount.go +++ b/k8s-source/adapters/serviceaccount.go @@ -19,13 +19,6 @@ func serviceAccountExtractor(resource *v1.ServiceAccount, scope string) ([]*sdp. Query: secret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret will affect the service account and the - // things that use it - In: true, - // The service account cannot affect the secret - Out: false, - }, }) } @@ -37,13 +30,6 @@ func serviceAccountExtractor(resource *v1.ServiceAccount, scope string) ([]*sdp. Query: ipSecret.Name, Type: "Secret", }, - BlastPropagation: &sdp.BlastPropagation{ - // Changing the secret will affect the service account and the - // things that use it - In: true, - // The service account cannot affect the secret - Out: false, - }, }) } diff --git a/k8s-source/adapters/statefulset.go b/k8s-source/adapters/statefulset.go index a11fd48e..e54e21d7 100644 --- a/k8s-source/adapters/statefulset.go +++ b/k8s-source/adapters/statefulset.go @@ -22,12 +22,6 @@ func statefulSetExtractor(resource *v1.StatefulSet, scope string) ([]*sdp.Linked Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the stateful set - In: true, - Out: true, - }, }) if len(resource.Spec.VolumeClaimTemplates) > 0 { @@ -38,12 +32,6 @@ func statefulSetExtractor(resource *v1.StatefulSet, scope string) ([]*sdp.Linked Query: LabelSelectorToQuery(resource.Spec.Selector), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Bidirectional propagation since we control the pods, and the - // pods host the stateful set - In: true, - Out: true, - }, }) } } diff --git a/k8s-source/adapters/volumeattachment.go b/k8s-source/adapters/volumeattachment.go index 7b8e9625..d7bca3c6 100644 --- a/k8s-source/adapters/volumeattachment.go +++ b/k8s-source/adapters/volumeattachment.go @@ -19,11 +19,6 @@ func volumeAttachmentExtractor(resource *v1.VolumeAttachment, scope string) ([]* Query: *resource.Spec.Source.PersistentVolumeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the PV could affect the attachment and vice versa - In: true, - Out: true, - }, }) } @@ -35,12 +30,6 @@ func volumeAttachmentExtractor(resource *v1.VolumeAttachment, scope string) ([]* Query: resource.Spec.NodeName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Changes to the node could affect the attachment and vice - // versa - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/authorization-role-assignment.go b/sources/azure/manual/authorization-role-assignment.go index f06584e2..48d0f07a 100644 --- a/sources/azure/manual/authorization-role-assignment.go +++ b/sources/azure/manual/authorization-role-assignment.go @@ -167,12 +167,6 @@ func (a authorizationRoleAssignmentWrapper) azureRoleAssignmentToSDPItem(roleAss Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Role assignment depends on managed identity for delegated access - // If identity is deleted/modified, role assignment operations may fail - In: true, - Out: false, - }, }) } } @@ -205,12 +199,6 @@ func (a authorizationRoleAssignmentWrapper) azureRoleAssignmentToSDPItem(roleAss Query: roleDefinitionGUID, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Role assignment depends on role definition for permissions - // If role definition is deleted/modified, role assignment becomes invalid - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index 8f46c631..5dda3207 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -128,12 +128,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: storageAccountName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on storage account for auto-storage functionality - // If storage account is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -156,12 +150,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: keyVaultName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on Key Vault for certificate management and encryption - // If Key Vault is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -188,12 +176,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified or key is rotated, batch account encryption may be affected - In: true, - Out: false, - }, }) } } @@ -219,12 +201,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connection is tightly coupled with the batch account - // Changes to either affect the other - In: true, - Out: true, - }, }) } } @@ -249,12 +225,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account depends on managed identity for authentication - // If identity is deleted/modified, batch account operations may fail - In: true, - Out: false, - }, }) } } @@ -278,12 +248,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: nodeIdentityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Batch account compute nodes depend on managed identity for auto-storage access - // If identity is deleted/modified, compute nodes may fail to access storage - In: true, - Out: false, - }, }) } } @@ -298,12 +262,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Applications are child resources of the batch account - // Changes to batch account affect applications, and vice versa - In: true, - Out: true, - }, }) // Link to Pools (child resource) @@ -316,12 +274,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Pools are child resources of the batch account - // Changes to batch account affect pools, and vice versa - In: true, - Out: true, - }, }) // Link to Certificates (child resource) @@ -334,12 +286,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificates are child resources of the batch account - // Changes to batch account affect certificates, and vice versa - In: true, - Out: true, - }, }) // Link to Private Endpoint Connections (child resource) @@ -352,12 +298,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connections are child resources of the batch account - // Changes to batch account affect connections, and vice versa - In: true, - Out: true, - }, }) // Link to Private Link Resources (child resource) @@ -370,12 +310,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private link resources are child resources of the batch account - // Changes to batch account affect resources, and vice versa - In: true, - Out: true, - }, }) // Link to Detectors (child resource) @@ -388,12 +322,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Detectors are child resources of the batch account - // Changes to batch account affect detectors, and vice versa - In: true, - Out: true, - }, }) // Link to DNS name (standard library) if AccountEndpoint is configured @@ -405,11 +333,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *account.Properties.AccountEndpoint, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } @@ -425,11 +348,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } } @@ -448,11 +366,6 @@ func (b batchAccountWrapper) azureBatchAccountToSDPItem(account *armbatch.Accoun Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are shared resources; changes can affect connectivity both ways - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-availability-set.go b/sources/azure/manual/compute-availability-set.go index f9d36dee..97126244 100644 --- a/sources/azure/manual/compute-availability-set.go +++ b/sources/azure/manual/compute-availability-set.go @@ -149,10 +149,6 @@ func (c computeAvailabilitySetWrapper) azureAvailabilitySetToSDPItem(availabilit Query: ppgName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If PPG changes → Availability Set placement changes (In: true) - Out: false, // If Availability Set is deleted → PPG remains (Out: false) - }, }) } } @@ -176,10 +172,6 @@ func (c computeAvailabilitySetWrapper) azureAvailabilitySetToSDPItem(availabilit Query: vmName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM changes → Availability Set membership changes (In: true) - Out: false, // If Availability Set is deleted → VMs remain but lose availability set association (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-capacity-reservation-group.go b/sources/azure/manual/compute-capacity-reservation-group.go index 21e221a8..25caaa53 100644 --- a/sources/azure/manual/compute-capacity-reservation-group.go +++ b/sources/azure/manual/compute-capacity-reservation-group.go @@ -154,11 +154,6 @@ func (c *computeCapacityReservationGroupWrapper) azureCapacityReservationGroupTo Query: shared.CompositeLookupKey(groupName, reservationName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Reservations are children of the group; group view depends on them (In). Group deletion removes reservations (Out). - In: true, - Out: true, - }, }) } } @@ -184,11 +179,6 @@ func (c *computeCapacityReservationGroupWrapper) azureCapacityReservationGroupTo Query: vmName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Group view lists associated VMs (In). VM deletion affects group's association list (Out). - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-dedicated-host-group.go b/sources/azure/manual/compute-dedicated-host-group.go index 4e8f1b6d..2dc9d4ed 100644 --- a/sources/azure/manual/compute-dedicated-host-group.go +++ b/sources/azure/manual/compute-dedicated-host-group.go @@ -132,11 +132,6 @@ func (c *computeDedicatedHostGroupWrapper) azureDedicatedHostGroupToSDPItem(dedi Query: shared.CompositeLookupKey(hostGroupName, hostName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Hosts are children of the group; host failure affects group view (In). Group deletion removes hosts (Out). - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-disk-access.go b/sources/azure/manual/compute-disk-access.go index f11f811e..15b821cd 100644 --- a/sources/azure/manual/compute-disk-access.go +++ b/sources/azure/manual/compute-disk-access.go @@ -132,10 +132,6 @@ func (c *computeDiskAccessWrapper) azureDiskAccessToSDPItem(diskAccess *armcompu Query: *diskAccess.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Disk access changes affect private endpoint connections - Out: true, // Private endpoint connection state affects disk access connectivity - }, }) // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections @@ -157,10 +153,6 @@ func (c *computeDiskAccessWrapper) azureDiskAccessToSDPItem(diskAccess *armcompu Query: privateEndpointName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes affect disk access private connectivity - Out: true, // Disk access deletion or config changes may affect the private endpoint connection state - }, }) } } diff --git a/sources/azure/manual/compute-disk-encryption-set.go b/sources/azure/manual/compute-disk-encryption-set.go index 5beb9a7b..08c6c811 100644 --- a/sources/azure/manual/compute-disk-encryption-set.go +++ b/sources/azure/manual/compute-disk-encryption-set.go @@ -158,10 +158,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/locked down → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key Vault remains (Out: false) - }, }) } } @@ -192,10 +188,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/locked down → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key Vault remains (Out: false) - }, }) } } @@ -218,10 +210,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: keyQuery, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key remains (Out: false) - }, }) } } @@ -235,11 +223,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -266,10 +249,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: keyQuery, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → DES can't access key material (In: true) - Out: false, // If DES is deleted/modified → Key remains (Out: false) - }, }) } } @@ -284,11 +263,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -315,10 +289,6 @@ func (c computeDiskEncryptionSetWrapper) azureDiskEncryptionSetToSDPItem(diskEnc Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If identity is deleted/permissions change → DES can't authenticate to Key Vault (In: true) - Out: false, // If DES is deleted/modified → identity remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-disk.go b/sources/azure/manual/compute-disk.go index 3b793324..ec8add3c 100644 --- a/sources/azure/manual/compute-disk.go +++ b/sources/azure/manual/compute-disk.go @@ -138,10 +138,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -164,10 +160,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -192,10 +184,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → disk becomes detached (In: true) - Out: false, // If disk is deleted → VM remains but loses disk (Out: false) - }, }) } } @@ -218,10 +206,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: diskAccessName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Access is deleted/modified → disk private endpoint access is affected (In: true) - Out: false, // If disk is deleted → Disk Access remains (Out: false) - }, }) } } @@ -242,10 +226,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → disk encryption is affected (In: true) - Out: false, // If disk is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -266,10 +246,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → disk encryption is affected (In: true) - Out: false, // If disk is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -298,10 +274,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → this disk may be affected (In: true) - Out: false, // If this disk is deleted → source disk remains (Out: false) - }, }) } } else if strings.Contains(sourceResourceID, "/snapshots/") { @@ -318,10 +290,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → this disk may be affected (In: true) - Out: false, // If this disk is deleted → source snapshot remains (Out: false) - }, }) } } @@ -343,10 +311,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: storageAccountName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → disk import may fail (In: true) - Out: false, // If disk is deleted → Storage Account remains (Out: false) - }, }) } } @@ -370,10 +334,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: imageName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Image remains (Out: false) - }, }) } } @@ -402,10 +362,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -430,10 +386,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -468,10 +420,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Community Gallery Image is deleted/modified → disk created from image may be affected (In: true) - Out: false, // If disk is deleted → Community Gallery Image remains (Out: false) - }, }) } } @@ -501,10 +449,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(elasticSanName, volumeGroupName, snapshotName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Elastic SAN snapshot is deleted/modified → disk created from snapshot may be affected (In: true) - Out: false, // If disk is deleted → Elastic SAN snapshot remains (Out: false) - }, }) } } @@ -535,10 +479,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → disk encryption key access is affected (In: true) - Out: false, // If disk is deleted → Key Vault remains (Out: false) - }, }) } } @@ -557,10 +497,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(vaultName, secretName), Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → disk encryption key is affected (In: true) - Out: false, // If disk is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -574,11 +510,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -598,10 +529,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → key encryption key access is affected (In: true) - Out: false, // If disk is deleted → Key Vault remains (Out: false) - }, }) } } @@ -620,10 +547,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: shared.CompositeLookupKey(vaultName, keyName), Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → key encryption key is affected (In: true) - Out: false, // If disk is deleted → Key Vault Key remains (Out: false) - }, }) } @@ -637,11 +560,6 @@ func (c computeDiskWrapper) azureDiskToSDPItem(disk *armcompute.Disk, scope stri Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index e4666974..8dd2c7f9 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -228,12 +228,6 @@ func TestComputeDisk(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.ComputeSnapshot.String() && linkedQuery.GetQuery().GetQuery() == "test-snapshot" { foundSnapshotLink = true - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected BlastPropagation.In to be true for snapshot link") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected BlastPropagation.Out to be false for snapshot link") - } break } } diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index c16a62fb..ebf07824 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -136,10 +136,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: galleryName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If gallery is deleted → version is deleted (In: true) - Out: false, // If version is deleted → gallery remains (Out: false) - }, }) // Parent Gallery Application: version depends on application; deleting version does not delete application @@ -150,10 +146,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: shared.CompositeLookupKey(galleryName, galleryApplicationName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If application is deleted → version is deleted (In: true) - Out: false, // If version is deleted → application remains (Out: false) - }, }) // MediaLink and DefaultConfigurationLink: add stdlib.NetworkHTTP, stdlib.NetworkDNS (hostname), stdlib.NetworkIP (when host is IP), azureshared.StorageAccount and azureshared.StorageBlobContainer (when Azure Blob) links. @@ -179,10 +171,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If storage account is unavailable → version artifact cannot be accessed (In: true) - Out: false, // If version is deleted → storage account remains (Out: false) - }, }) } containerName := azureshared.ExtractContainerNameFromBlobURI(link) @@ -197,10 +185,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: containerKey, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If blob container is unavailable → version artifact cannot be accessed (In: true) - Out: false, // If version is deleted → blob container remains (Out: false) - }, }) } } @@ -244,10 +228,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: name, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption set changes → replication encryption affected (In: true) - Out: false, // If version is deleted → encryption set remains (Out: false) - }, }) } } @@ -269,10 +249,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: name, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -296,10 +272,6 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT Query: name, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/compute-gallery-image.go b/sources/azure/manual/compute-gallery-image.go index dade2662..b903588e 100644 --- a/sources/azure/manual/compute-gallery-image.go +++ b/sources/azure/manual/compute-gallery-image.go @@ -124,10 +124,6 @@ func (c computeGalleryImageWrapper) azureGalleryImageToSDPItem( Query: galleryName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If gallery is deleted → image definition is deleted - Out: false, // If image definition is deleted → gallery remains - }, }) // URI-based links: Eula, PrivacyStatementURI, ReleaseNoteURI diff --git a/sources/azure/manual/compute-image.go b/sources/azure/manual/compute-image.go index d9102966..dc3975dd 100644 --- a/sources/azure/manual/compute-image.go +++ b/sources/azure/manual/compute-image.go @@ -143,10 +143,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source disk remains (Out: false) - }, }) } } @@ -167,10 +163,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source snapshot remains (Out: false) - }, }) } } @@ -190,10 +182,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → image blob becomes inaccessible (In: true) - Out: false, // If image is deleted → Storage Account remains (Out: false) - }, }) } @@ -206,10 +194,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: blobURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → image cannot be accessed (In: true) - Out: true, // If image is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, }) } @@ -223,11 +207,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -248,10 +227,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → image encryption is affected (In: true) - Out: false, // If image is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -280,10 +255,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source disk remains (Out: false) - }, }) } } @@ -304,10 +275,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → image becomes invalid (In: true) - Out: false, // If image is deleted → source snapshot remains (Out: false) - }, }) } } @@ -325,10 +292,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → image blob becomes inaccessible (In: true) - Out: false, // If image is deleted → Storage Account remains (Out: false) - }, }) } @@ -341,10 +304,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: blobURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → image cannot be accessed (In: true) - Out: true, // If image is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, }) } @@ -358,11 +317,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -383,10 +337,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → image encryption is affected (In: true) - Out: false, // If image is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -410,10 +360,6 @@ func (c computeImageWrapper) azureImageToSDPItem(image *armcompute.Image, scope Query: vmName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source VM is deleted/modified → image source becomes invalid (In: true) - Out: false, // If image is deleted → source VM remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-proximity-placement-group.go b/sources/azure/manual/compute-proximity-placement-group.go index e6531884..c6684149 100644 --- a/sources/azure/manual/compute-proximity-placement-group.go +++ b/sources/azure/manual/compute-proximity-placement-group.go @@ -138,10 +138,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: vmName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects VM placement - Out: true, // VM add/remove changes PPG membership - }, }) } } @@ -166,10 +162,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: avSetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects Availability Set placement - Out: true, // Availability Set add/remove changes PPG membership - }, }) } } @@ -194,10 +186,6 @@ func (c computeProximityPlacementGroupWrapper) azureProximityPlacementGroupToSDP Query: vmssName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // PPG change affects VMSS placement - Out: true, // VMSS add/remove changes PPG membership - }, }) } } diff --git a/sources/azure/manual/compute-shared-gallery-image.go b/sources/azure/manual/compute-shared-gallery-image.go index 9b1a6445..97d09b6c 100644 --- a/sources/azure/manual/compute-shared-gallery-image.go +++ b/sources/azure/manual/compute-shared-gallery-image.go @@ -127,10 +127,6 @@ func (c computeSharedGalleryImageWrapper) azureSharedGalleryImageToSDPItem( Query: shared.CompositeLookupKey(location, galleryUniqueName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If shared gallery is removed → image is no longer visible - Out: false, // If image definition is deleted → shared gallery remains - }, }) // URI-based links. Note: armcompute.SharedGalleryImageProperties has no ReleaseNoteURI field (unlike GalleryImage). diff --git a/sources/azure/manual/compute-snapshot.go b/sources/azure/manual/compute-snapshot.go index a5f3397f..80626272 100644 --- a/sources/azure/manual/compute-snapshot.go +++ b/sources/azure/manual/compute-snapshot.go @@ -160,10 +160,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: diskAccessName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Access is deleted/modified → snapshot private endpoint access is affected - Out: false, // If snapshot is deleted → Disk Access remains - }, }) } } @@ -184,10 +180,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → snapshot encryption is affected - Out: false, // If snapshot is deleted → Disk Encryption Set remains - }, }) } } @@ -208,10 +200,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set is deleted/modified → snapshot encryption is affected - Out: false, // If snapshot is deleted → Disk Encryption Set remains - }, }) } } @@ -240,10 +228,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: diskName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source disk is deleted/modified → snapshot may be affected - Out: false, // If snapshot is deleted → source disk remains - }, }) } } else if strings.Contains(sourceResourceIDLower, "/snapshots/") { @@ -260,10 +244,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: snapshotName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If source snapshot is deleted/modified → this snapshot may be affected - Out: false, // If this snapshot is deleted → source snapshot remains - }, }) } } @@ -285,10 +265,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: storageAccountName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → snapshot import may fail - Out: false, // If snapshot is deleted → Storage Account remains - }, }) } } @@ -306,10 +282,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → snapshot import blob becomes inaccessible - Out: false, // If snapshot is deleted → Storage Account remains - }, }) containerName := azureshared.ExtractContainerNameFromBlobURI(sourceURI) @@ -321,10 +293,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(storageAccountName, containerName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If blob container is deleted/modified → snapshot import source is lost - Out: false, // If snapshot is deleted → blob container remains - }, }) } } @@ -337,10 +305,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: sourceURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → snapshot import source is lost - Out: true, // Bidirectional: changes to either side may affect the other - }, }) } @@ -354,10 +318,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -367,10 +327,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -395,10 +351,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: imageName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Image is deleted/modified → snapshot created from image may be affected - Out: false, // If snapshot is deleted → Image remains - }, }) } } @@ -425,10 +377,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → snapshot created from image may be affected - Out: false, // If snapshot is deleted → Gallery Image remains - }, }) } } @@ -451,10 +399,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image is deleted/modified → snapshot created from image may be affected - Out: false, // If snapshot is deleted → Gallery Image remains - }, }) } } @@ -485,10 +429,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Community Gallery Image is deleted/modified → snapshot may be affected - Out: false, // If snapshot is deleted → Community Gallery Image remains - }, }) } } @@ -515,10 +455,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(elasticSanName, volumeGroupName, esSnapshotName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Elastic SAN snapshot is deleted/modified → this snapshot may be affected - Out: false, // If this snapshot is deleted → Elastic SAN snapshot remains - }, }) } } @@ -547,10 +483,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → snapshot encryption key access is affected - Out: false, // If snapshot is deleted → Key Vault remains - }, }) } } @@ -575,10 +507,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(vaultName, secretName), Scope: secretScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → snapshot encryption key is affected - Out: false, // If snapshot is deleted → Key Vault Secret remains - }, }) } @@ -592,10 +520,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: secretHost, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -605,10 +529,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: secretHost, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -629,10 +549,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → key encryption key access is affected - Out: false, // If snapshot is deleted → Key Vault remains - }, }) } } @@ -657,10 +573,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: shared.CompositeLookupKey(vaultName, keyName), Scope: keyScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Key is deleted/modified → key encryption key is affected - Out: false, // If snapshot is deleted → Key Vault Key remains - }, }) } @@ -674,10 +586,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: keyHost, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } else { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -687,10 +595,6 @@ func (c computeSnapshotWrapper) azureSnapshotToSDPItem(snapshot *armcompute.Snap Query: keyHost, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/compute-virtual-machine-extension.go b/sources/azure/manual/compute-virtual-machine-extension.go index 645a31e8..468c6115 100644 --- a/sources/azure/manual/compute-virtual-machine-extension.go +++ b/sources/azure/manual/compute-virtual-machine-extension.go @@ -88,10 +88,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: virtualMachineName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted → Extension becomes invalid/unusable (In: true) - Out: false, // If Extension is deleted → VM remains functional (Out: false) - }, // Extension is a child resource of VM }) } @@ -115,10 +111,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → Extension settings access changes (In: true) - Out: false, // If Extension is deleted → Key Vault remains (Out: false) - }, // Extension depends on Key Vault for protected settings }) } } @@ -139,11 +131,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -166,10 +153,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Extension cannot resolve endpoint (In: true) - Out: true, // If Extension is deleted → DNS name may still be used by other resources (Out: true) - }, // Extension depends on DNS name for endpoint resolution }) } } @@ -195,10 +178,6 @@ func (c computeVirtualMachineExtensionWrapper) azureVirtualMachineExtensionToSDP Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Extension cannot resolve endpoint (In: true) - Out: true, // If Extension is deleted → DNS name may still be used by other resources (Out: true) - }, // Extension depends on DNS name for endpoint resolution }) } } diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index 6b2dbf3c..e64c4a17 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -116,12 +116,6 @@ func TestComputeVirtualMachineExtension(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected scope %s, got %s", scope, liq.GetQuery().GetScope()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected blast propagation In=true for Key Vault") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected blast propagation Out=false for Key Vault") - } case azureshared.ComputeVirtualMachine.String(): hasVMLink = true } @@ -183,9 +177,6 @@ func TestComputeVirtualMachineExtension(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected method SEARCH for DNS link, got %v", liq.GetQuery().GetMethod()) } - if liq.GetBlastPropagation().GetIn() != true || liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected blast propagation In: true, Out: true for DNS link, got In: %v, Out: %v", liq.GetBlastPropagation().GetIn(), liq.GetBlastPropagation().GetOut()) - } } } } diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index 517b5664..904d62a8 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -104,10 +104,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: virtualMachineName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VM is deleted/modified → Run Command becomes invalid (In: true) - Out: false, // If Run Command is deleted → VM remains functional (Out: false) - }, // Run Command is a child resource of VM }) } @@ -139,10 +135,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: identityQuery, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Managed Identity is deleted/modified → Run Command cannot access blob/script (In: true) - Out: false, // If Run Command is deleted → Managed Identity remains (Out: false) - }, // Run Command depends on Managed Identity for blob/script access }) } @@ -165,10 +157,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Storage Account remains (Out: false) - }, // Run Command depends on Storage Account for blob access }) // Extract container name and link to Blob Container @@ -184,10 +172,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: shared.CompositeLookupKey(storageAccountName, containerName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Blob Container is deleted/modified → blob becomes inaccessible (In: true) - Out: false, // If Run Command is deleted → Blob Container remains (Out: false) - }, // Run Command depends on Blob Container for blob access }) } } @@ -202,10 +186,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: uri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → Run Command cannot access script/blob (In: true) - Out: true, // If Run Command is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, // Run Command depends on HTTP endpoint for script/blob access }) // Link to DNS name (standard library) from URI @@ -218,10 +198,6 @@ func (s computeVirtualMachineRunCommandWrapper) azureVirtualMachineRunCommandToS Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → Run Command cannot resolve endpoint (In: true) - Out: true, // If Run Command is deleted → DNS name may still be used by other resources (Out: true) - }, // Run Command depends on DNS name for endpoint resolution }) } } diff --git a/sources/azure/manual/compute-virtual-machine-run-command_test.go b/sources/azure/manual/compute-virtual-machine-run-command_test.go index 83b2c6af..fa0ddc92 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -198,12 +198,6 @@ func TestComputeVirtualMachineRunCommand(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected method GET, got %s", liq.GetQuery().GetMethod()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected blast propagation In=true for Storage Account") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected blast propagation Out=false for Storage Account") - } case azureshared.StorageBlobContainer.String(): blobContainerLinks++ expectedQuery := shared.CompositeLookupKey("mystorageaccount", "outputcontainer") diff --git a/sources/azure/manual/compute-virtual-machine-scale-set.go b/sources/azure/manual/compute-virtual-machine-scale-set.go index 3f6f003a..056294bd 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set.go @@ -150,10 +150,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(scaleSetName, *extension.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Extensions are deleted → VMSS remains functional (In: false) - Out: true, // If VMSS is deleted → Extensions become invalid/unusable (Out: true) - }, }) } } @@ -170,10 +166,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: scaleSetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If VM instances are deleted → VMSS remains functional (In: false) - Out: true, // If VMSS is deleted → VM instances become invalid/unusable (Out: true) - }, }) } @@ -200,10 +192,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: nsgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NSG changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → NSG remains (Out: false) - }, }) } } @@ -236,10 +224,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vnetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Virtual Network changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → Virtual Network remains (Out: false) - }, }) // Link to Subnet sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -249,10 +233,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Subnet changes → VMSS network behavior changes (In: true) - Out: false, // If VMSS is deleted → Subnet remains (Out: false) - }, }) } } @@ -277,10 +257,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: publicIPPrefixName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Public IP Prefix changes → VMSS public IP allocation changes (In: true) - Out: false, // If VMSS is deleted → Public IP Prefix remains (Out: false) - }, }) } } @@ -311,10 +287,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Backend Address Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -324,10 +296,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Backend Pool changes → VMSS load balancing changes (In: true) - Out: true, // If VMSS is deleted → Backend Pool loses members (Out: true) - }, }) } } @@ -360,10 +328,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Inbound NAT Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -373,10 +337,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Pool changes → VMSS NAT behavior changes (In: true) - Out: true, // If VMSS is deleted → NAT Pool loses members (Out: true) - }, }) } } @@ -409,10 +369,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: agName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Application Gateway changes → VMSS routing changes (In: true) - Out: false, // If VMSS is deleted → Application Gateway remains (Out: false) - }, }) // Link to Backend Address Pool sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -422,10 +378,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(agName, poolName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Backend Pool changes → VMSS routing changes (In: true) - Out: true, // If VMSS is deleted → Backend Pool loses members (Out: true) - }, }) } } @@ -451,10 +403,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: asgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If ASG changes → VMSS network rules change (In: true) - Out: false, // If VMSS is deleted → ASG remains (Out: false) - }, }) } } @@ -495,10 +443,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: lbName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Load Balancer changes → VMSS load balancing changes (In: true) - Out: false, // If VMSS is deleted → Load Balancer remains (Out: false) - }, }) // Link to Health Probe sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -508,10 +452,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(lbName, probeName), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Health Probe changes → VMSS health checks change (In: true) - Out: false, // If VMSS is deleted → Health Probe remains (Out: false) - }, }) } } @@ -539,10 +479,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set changes → VMSS disk encryption changes (In: true) - Out: false, // If VMSS is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -566,10 +502,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: encryptionSetName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Disk Encryption Set changes → VMSS disk encryption changes (In: true) - Out: false, // If VMSS is deleted → Disk Encryption Set remains (Out: false) - }, }) } } @@ -603,10 +535,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: imageName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Image remains (Out: false) - }, }) } } @@ -635,10 +563,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(galleryName, imageName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -664,10 +588,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(communityGalleryName, imageName, version), Scope: scope, // Community galleries are subscription-level }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Image changes → VMSS VM configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Image remains (Out: false) - }, }) } } @@ -703,10 +623,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: shared.CompositeLookupKey(galleryName, applicationName, version), Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Gallery Application Version changes → VMSS application configuration changes (In: true) - Out: false, // If VMSS is deleted → Gallery Application Version remains (Out: false) - }, }) } } @@ -733,10 +649,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: ppgName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If PPG changes → VMSS placement changes (In: true) - Out: false, // If VMSS is deleted → PPG remains (Out: false) - }, }) } } @@ -758,10 +670,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: hostGroupName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Host Group changes → VMSS host placement changes (In: true) - Out: false, // If VMSS is deleted → Host Group remains (Out: false) - }, }) } } @@ -787,10 +695,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: capacityReservationGroupName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Capacity Reservation Group changes → VMSS capacity reservation changes (In: true) - Out: false, // If VMSS is deleted → Capacity Reservation Group remains (Out: false) - }, }) } } @@ -814,10 +718,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Identity changes → VMSS access changes (In: true) - Out: false, // If VMSS is deleted → Identity remains (Out: false) - }, }) } } @@ -846,11 +746,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) // Extract account name (everything before the first dot) @@ -871,10 +766,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Storage Account changes → VMSS boot diagnostics affected (In: true) - Out: false, // If VMSS is deleted → Storage Account remains (Out: false) - }, }) } } @@ -902,10 +793,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VMSS secrets access changes (In: true) - Out: false, // If VMSS is deleted → Key Vault remains (Out: false) - }, }) } } @@ -934,10 +821,6 @@ func (c computeVirtualMachineScaleSetWrapper) azureVirtualMachineScaleSetToSDPIt Query: vaultName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VMSS extension settings access changes (In: true) - Out: false, // If VMSS is deleted → Key Vault remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/compute-virtual-machine.go b/sources/azure/manual/compute-virtual-machine.go index b3907780..03350d33 100644 --- a/sources/azure/manual/compute-virtual-machine.go +++ b/sources/azure/manual/compute-virtual-machine.go @@ -222,10 +222,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If disk changes → VM affected (In: true) - Out: true, // If VM is deleted → disk may be deleted depending on delete option (Out: true) - }, }) } // Link to disk encryption set for OS disk @@ -244,10 +240,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskEncryptionSetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption set changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → encryption set remains (Out: false) - }, }) } } @@ -272,10 +264,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If disk changes → VM affected (In: true) - Out: true, // If VM is deleted → disk may be deleted depending on delete option (Out: true) - }, }) } // Link to disk encryption set for data disk @@ -294,10 +282,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: diskEncryptionSetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption set changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → encryption set remains (Out: false) - }, }) } } @@ -324,10 +308,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: nicName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NIC changes → VM network connectivity affected (In: true) - Out: false, // If VM is deleted → NIC remains (Out: false) - }, }) } } @@ -351,10 +331,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: availabilitySetName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If availability set changes → VM placement affected (In: true) - Out: false, // If VM is deleted → availability set remains (Out: false) - }, }) } } @@ -375,10 +351,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: proximityPlacementGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If proximity placement group changes → VM placement affected (In: true) - Out: false, // If VM is deleted → proximity placement group remains (Out: false) - }, }) } } @@ -399,10 +371,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: hostGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If host group changes → VM host placement affected (In: true) - Out: false, // If VM is deleted → host group remains (Out: false) - }, }) } } @@ -423,10 +391,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: capacityReservationGroupName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If capacity reservation group changes → VM capacity reservation affected (In: true) - Out: false, // If VM is deleted → capacity reservation group remains (Out: false) - }, }) } } @@ -447,10 +411,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vmssName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VMSS changes → VM configuration affected (In: true) - Out: false, // If VM is deleted → VMSS remains (Out: false) - }, }) } } @@ -473,10 +433,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vmssName[0], Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If VMSS changes → VM configuration affected (In: true) - Out: false, // If VM is deleted → VMSS remains (Out: false) - }, }) } } @@ -507,10 +463,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: shared.CompositeLookupKey(galleryName, imageName, versionName), Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If image version changes → VM image affected (In: true) - Out: false, // If VM is deleted → image version remains (Out: false) - }, }) } } else if strings.Contains(imageID, "/images/") { @@ -528,10 +480,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: imageName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If image changes → VM image affected (In: true) - Out: false, // If VM is deleted → image remains (Out: false) - }, }) } } @@ -555,10 +503,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: identityName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If identity changes → VM identity access affected (In: true) - Out: false, // If VM is deleted → identity remains (Out: false) - }, }) } } @@ -582,10 +526,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → VM secrets access affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -612,10 +552,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -634,10 +570,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: vaultName, Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault changes → disk encryption affected (In: true) - Out: false, // If VM is deleted → Key Vault remains (Out: false) - }, }) } } @@ -667,10 +599,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: shared.CompositeLookupKey(galleryName, appName, versionName), Scope: linkScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If application version changes → VM application affected (In: true) - Out: false, // If VM is deleted → application version remains (Out: false) - }, }) } } @@ -690,10 +618,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: storageURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If storage URI changes → boot diagnostics affected (In: true) - Out: false, // If VM is deleted → storage URI remains (Out: false) - }, }) // Extract DNS name from URL and create DNS link // Reference: Any attribute containing a DNS name must create a LinkedItemQuery for dns type @@ -706,10 +630,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS changes → boot diagnostics affected (In: true) - Out: false, // If VM is deleted → DNS remains (Out: false) - }, }) } } @@ -727,10 +647,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: shared.CompositeLookupKey(*vm.Name, *extension.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Extensions are deleted → VM remains functional (In: false) - Out: true, // If VM is deleted → Extensions become invalid/unusable (Out: true) - }, }) } } @@ -747,10 +663,6 @@ func (c computeVirtualMachineWrapper) azureVirtualMachineToSDPItem(vm *armcomput Query: *vm.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // If Run Commands are deleted → VM remains functional (In: false) - Out: true, // If VM is deleted → Run Commands become invalid/unusable (Out: true) - }, }) } diff --git a/sources/azure/manual/dbforpostgresql-database.go b/sources/azure/manual/dbforpostgresql-database.go index 66962894..384de872 100644 --- a/sources/azure/manual/dbforpostgresql-database.go +++ b/sources/azure/manual/dbforpostgresql-database.go @@ -91,10 +91,6 @@ func (s dbforPostgreSQLDatabaseWrapper) azureDBforPostgreSQLDatabaseToSDPItem(da Query: serverName, Scope: scope, // Server is in the same resource group as the database }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration, maintenance) directly affect database availability and functionality - Out: false, // Database changes (schema, data) don't directly affect the server's configuration or operation - }, // Database depends on server - server is the parent resource that hosts the database }) return sdpItem, nil diff --git a/sources/azure/manual/dbforpostgresql-flexible-server.go b/sources/azure/manual/dbforpostgresql-flexible-server.go index a74541e3..51867295 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server.go @@ -133,12 +133,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, // Use the subnet's scope, not the server's scope }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on subnet for network connectivity - // If subnet is deleted/modified, server network access may be affected - In: true, - Out: false, - }, // Subnet is an external resource that the server depends on }) // Link to Virtual Network (parent of subnet) @@ -151,12 +145,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vnetName, Scope: scope, // Use the same scope as the subnet }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on virtual network for network connectivity - // If virtual network is deleted/modified, server network access may be affected - In: true, - Out: false, - }, // Virtual Network is an external resource that the server depends on }) } } @@ -171,10 +159,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration, maintenance) directly affect database availability - Out: false, // Database changes (schema, data) don't directly affect the server's configuration - }, // Databases are child resources that depend on their parent server }) // Link to Firewall Rules (child resource) @@ -187,10 +171,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect firewall rules - Out: true, // Firewall rule changes affect server connectivity - }, // Firewall Rules are child resources that control server access }) // Link to Configurations (child resource) @@ -203,10 +183,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect configurations - Out: true, // Configuration changes affect server behavior and performance - }, // Configurations are child resources that control server settings }) // Link to Fully Qualified Domain Name (DNS) @@ -219,10 +195,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: *server.Properties.FullyQualifiedDomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS changes affect server connectivity - Out: true, // Server changes may affect DNS resolution - }, // DNS names are shared resources that affect connectivity }) } @@ -244,12 +216,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for authentication - // If identity is deleted/modified, server operations may fail - In: true, - Out: false, - }, }) } } @@ -274,12 +240,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: privateDNSZoneName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on private DNS zone for DNS resolution - // If DNS zone is deleted/modified, server DNS resolution may fail - In: true, - Out: false, - }, }) } } @@ -294,10 +254,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect administrators - Out: true, // Administrator changes affect server access and authentication - }, // Administrators are child resources that control server access }) // Link to Private Endpoint Connections (child resource) @@ -310,10 +266,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect private endpoint connections - Out: true, // Private endpoint connection changes affect server network connectivity - }, // Private Endpoint Connections are child resources that manage private network access }) // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections @@ -337,12 +289,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint changes (deletion, network configuration) affect the PostgreSQL Flexible Server's private connectivity - // Server deletion or configuration changes may affect the private endpoint's connection state - In: true, - Out: true, - }, // Private endpoints are tightly coupled to the server - changes affect connectivity }) } } @@ -359,10 +305,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect private link resources - Out: true, // Private link resource changes affect server private connectivity - }, // Private Link Resources are child resources that define available private link services }) // Link to Replicas (child resource) @@ -375,10 +317,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect replica availability - Out: false, // Replica changes don't directly affect the primary server's configuration - }, // Replicas are child resources that depend on their parent server }) // Link to Migrations (child resource) @@ -391,10 +329,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes affect migration operations - Out: false, // Migration changes don't directly affect the server's configuration - }, // Migrations are child resources that represent data migration operations }) // Link to Backups (child resource) @@ -407,10 +341,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect backup availability - Out: false, // Backup changes don't directly affect the server's configuration - }, // Backups are child resources that depend on their parent server }) // Link to Virtual Endpoints (child resource) @@ -423,10 +353,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Server changes (deletion, configuration) directly affect virtual endpoint availability - Out: true, // Virtual endpoint changes affect server connectivity and routing - }, // Virtual Endpoints are child resources that control server network access }) // Link to Key Vault Vault (external resource) from Data Encryption @@ -446,12 +372,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vaultName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified, server encryption may fail - In: true, - Out: false, - }, }) } } @@ -477,12 +397,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault key for customer-managed encryption - // If key is deleted/modified, server encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -505,12 +419,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for accessing encryption keys - // If identity is deleted/modified, server encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -532,12 +440,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: vaultName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault for geo-redundant backup encryption keys - // If Key Vault is deleted/modified, server geo-backup encryption may fail - In: true, - Out: false, - }, }) } } @@ -563,12 +465,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: query, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on Key Vault key for geo-redundant backup encryption - // If key is deleted/modified, server geo-backup encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -591,12 +487,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // PostgreSQL Flexible Server depends on managed identity for accessing geo-backup encryption keys - // If identity is deleted/modified, server geo-backup encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -620,12 +510,6 @@ func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServer Query: sourceServerName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Replica server depends on source server for replication - // If source server is deleted/modified, replica operations may fail - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index 2850a92d..b6278f77 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -199,12 +199,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.vnet-rg" { t.Errorf("Expected subnet link scope to be 'sub-id.vnet-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected subnet link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected subnet link to have Out=false") - } } if linkedQuery.GetQuery().GetType() == azureshared.NetworkVirtualNetwork.String() { foundVNetLink = true @@ -214,12 +208,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.vnet-rg" { t.Errorf("Expected virtual network link scope to be 'sub-id.vnet-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected virtual network link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected virtual network link to have Out=false") - } } } @@ -264,12 +252,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "global" { t.Errorf("Expected DNS link scope to be 'global', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected DNS link to have In=true") - } - if !linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected DNS link to have Out=true") - } } } @@ -476,9 +458,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected primary identity link to use GET method, got %v", linkedQuery.GetQuery().GetMethod()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected primary identity link to have In=true") - } } // Primary Key Vault Vault if linkedQuery.GetQuery().GetType() == azureshared.KeyVaultVault.String() && @@ -519,9 +498,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected geo backup identity link to use GET method, got %v", linkedQuery.GetQuery().GetMethod()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected geo backup identity link to have In=true") - } } } @@ -580,12 +556,6 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != "sub-id.source-rg" { t.Errorf("Expected source server link scope to be 'sub-id.source-rg', got %s", linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected source server link to have In=true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected source server link to have Out=false") - } } } diff --git a/sources/azure/manual/dns_links.go b/sources/azure/manual/dns_links.go index 44824c1a..bda702f5 100644 --- a/sources/azure/manual/dns_links.go +++ b/sources/azure/manual/dns_links.go @@ -22,7 +22,6 @@ func appendDNSServerLinkIfValid(queries *[]*sdp.LinkedItemQuery, server string, Query: s, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, } }) } diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 874d1818..950f5822 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -116,10 +116,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: *account.Name, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint connection changes (deletion, status changes) affect the Database Account's network connectivity and accessibility - Out: true, // Database Account deletion makes the private endpoint connection invalid, and account configuration changes may affect connection status - }, // Private endpoint connections are tightly coupled to the Database Account - changes on either side affect connectivity and validity }) // Link to Private Endpoint resources @@ -149,10 +145,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: privateEndpointName, Scope: scope, // Use the private endpoint's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Database Account's private connectivity - Out: true, // Database Account deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the Database Account - changes affect connectivity }) } } @@ -193,10 +185,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: query, Scope: scope, // Use the subnet's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Database Account's network accessibility - Out: false, // Database Account changes don't directly affect the subnet configuration - }, // Database Account depends on subnet for network access - subnet changes impact connectivity }) } } @@ -224,10 +212,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault changes (key deletion, rotation, access policy) affect the Database Account's encryption - Out: false, // Database Account changes don't directly affect the Key Vault - }, // Database Account depends on Key Vault for encryption keys - key changes impact encryption/decryption }) } } @@ -271,10 +255,6 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt Query: resourceGroupName, Scope: scope, // Use the identity's scope, not the database account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Identity changes (deletion, role assignments) affect the Database Account's authentication and authorization - Out: false, // Database Account changes don't directly affect the managed identity - }, // Database Account depends on managed identity for authentication - identity changes impact access }) } } diff --git a/sources/azure/manual/keyvault-managed-hsm.go b/sources/azure/manual/keyvault-managed-hsm.go index 05308a3d..5c5daad6 100644 --- a/sources/azure/manual/keyvault-managed-hsm.go +++ b/sources/azure/manual/keyvault-managed-hsm.go @@ -129,10 +129,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: shared.CompositeLookupKey(*hsm.Name, connectionName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Connection state changes affect the Managed HSM's private connectivity - Out: true, // Managed HSM deletion removes the connection - }, }) } } @@ -167,10 +163,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: privateEndpointName, Scope: peScope, // Use the private endpoint's scope, not the Managed HSM's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Managed HSM's private connectivity - Out: true, // Managed HSM deletion or configuration changes may affect the private endpoint's connection state - }, }) } } @@ -211,10 +203,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: query, Scope: scope, // Use the subnet's scope, not the Managed HSM's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Managed HSM's network accessibility - Out: false, // Managed HSM changes don't directly affect the subnet configuration - }, // Managed HSM depends on subnet for network access - subnet changes impact connectivity }) } } @@ -233,11 +221,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -265,12 +248,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Managed HSM depends on managed identity for authentication and access control - // If identity is deleted/modified, Managed HSM operations may fail - In: true, - Out: false, - }, }) } } @@ -290,11 +267,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } // Link to HTTP/HTTPS endpoint (standard library) from HsmURI @@ -305,11 +277,6 @@ func (k keyvaultManagedHSMsWrapper) azureManagedHSMToSDPItem(hsm *armkeyvault.Ma Query: hsmURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Endpoint connectivity affects HSM access and vice versa - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index b9ef5f2d..fa25686d 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -161,10 +161,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: vaultName, Scope: linkedScope, // Use the vault's scope from the resource ID }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault is deleted/modified → secret access and configuration are affected (In: true) - Out: false, // If secret is deleted → Key Vault remains (Out: false) - }, // Secret depends on Key Vault - vault changes impact secret availability and access }) } } @@ -185,10 +181,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -198,10 +190,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: secretURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -217,10 +205,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -230,10 +214,6 @@ func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, Query: secretURIWithVersion, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index c4fbaa93..cbb8f037 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -163,10 +163,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: privateEndpointName, Scope: scope, // Use the private endpoint's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the Key Vault's private connectivity - Out: true, // Key Vault deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the Key Vault - changes affect connectivity }) } } @@ -207,10 +203,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: query, Scope: scope, // Use the subnet's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (deletion, network configuration) affect the Key Vault's network accessibility - Out: false, // Key Vault changes don't directly affect the subnet configuration - }, // Key Vault depends on subnet for network access - subnet changes impact connectivity }) } } @@ -229,11 +221,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: *ipRule.Value, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - IP rule changes affect Key Vault network accessibility - In: true, - Out: true, - }, }) } } @@ -249,11 +236,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: vaultURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Vault endpoint connectivity affects Key Vault operations; Key Vault changes may affect endpoint - In: true, - Out: true, - }, }) } @@ -283,10 +265,6 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Query: hsmName, Scope: scope, // Use the Managed HSM's scope, not the vault's scope }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed HSM changes (deletion, configuration) affect the Key Vault's functionality and availability - Out: false, // Key Vault changes don't directly affect the Managed HSM itself - }, // Key Vault depends on Managed HSM for hardware-backed security - HSM changes impact vault operations }) } } diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index 98fa99f8..46302efc 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -51,7 +51,6 @@ func AppendURILinks( Query: uri, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, }) hostFromURL := azureshared.ExtractDNSFromURL(uri) if hostFromURL != "" { @@ -69,7 +68,6 @@ func AppendURILinks( Query: hostOnly, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, }) } } else { @@ -82,7 +80,6 @@ func AppendURILinks( Query: hostOnly, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: blastIn, Out: blastOut}, }) } } @@ -98,6 +95,5 @@ func networkIPQuery(query string) *sdp.LinkedItemQuery { Query: query, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{In: true, Out: true}, } } diff --git a/sources/azure/manual/managedidentity-user-assigned-identity.go b/sources/azure/manual/managedidentity-user-assigned-identity.go index 11713958..1873629b 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity.go @@ -117,12 +117,6 @@ func (m managedIdentityUserAssignedIdentityWrapper) azureManagedIdentityUserAssi Query: *identity.Name, // Identity name is sufficient since resource group is available to the adapter Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Federated credentials are tightly coupled to the identity - // Changes to the identity affect credentials, and credential changes affect identity usage - In: true, - Out: true, - }, }) return sdpItem, nil diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index 04d3fa0c..a4511f87 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -125,10 +125,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *gatewayIPConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // GatewayIPConfiguration changes affect the Application Gateway's network configuration - Out: true, // Application Gateway changes (like deletion) affect the gateway IP configuration - }, }) // Link to Subnet from GatewayIPConfiguration @@ -150,10 +146,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes affect the Application Gateway's network configuration - Out: false, // Application Gateway changes don't affect the subnet itself - }, }) // Link to VirtualNetwork (extracted from subnet ID) @@ -168,10 +160,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: vnetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // VirtualNetwork changes affect the Application Gateway's network configuration - Out: false, // Application Gateway changes don't affect the virtual network itself - }, }) } } @@ -191,10 +179,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *frontendIPConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // FrontendIPConfiguration changes affect the Application Gateway's frontend configuration - Out: true, // Application Gateway changes (like deletion) affect the frontend IP configuration - }, }) } @@ -215,10 +199,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: publicIPName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes affect the Application Gateway's frontend configuration - Out: false, // Application Gateway changes don't affect the public IP address itself - }, }) } } @@ -242,10 +222,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes affect the Application Gateway's frontend configuration - Out: false, // Application Gateway changes don't affect the subnet itself - }, }) } } @@ -259,10 +235,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *frontendIPConfig.Properties.PrivateIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // IPs are always linked bidirectionally - Out: true, - }, }) } } @@ -281,10 +253,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *backendPool.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendAddressPool changes affect which backends receive traffic - Out: true, // Application Gateway changes (like deletion) affect the backend address pool - }, }) } @@ -299,10 +267,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendAddress.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // IPs are always linked bidirectionally - Out: true, - }, }) } @@ -315,10 +279,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendAddress.Fqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS names are always linked bidirectionally - Out: true, - }, }) } } @@ -338,10 +298,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *httpListener.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // HTTPListener changes affect how the Application Gateway receives traffic - Out: true, // Application Gateway changes (like deletion) affect the HTTP listener - }, }) } @@ -357,10 +313,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *httpListener.Properties.HostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect how the Application Gateway receives traffic - Out: true, // DNS names are always linked bidirectionally - }, }) } @@ -375,10 +327,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *hostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect how the Application Gateway receives traffic - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -399,10 +347,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *backendHTTPSettings.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendHTTPSettings changes affect how the Application Gateway communicates with backends - Out: true, // Application Gateway changes (like deletion) affect the backend HTTP settings - }, }) } @@ -416,10 +360,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *backendHTTPSettings.Properties.HostName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect backend communication - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -437,10 +377,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *rule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RequestRoutingRule changes affect how traffic is routed - Out: true, // Application Gateway changes (like deletion) affect the routing rule - }, }) } } @@ -458,10 +394,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *probe.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Probe changes affect backend health monitoring - Out: true, // Application Gateway changes (like deletion) affect the probe - }, }) } @@ -475,10 +407,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: *probe.Properties.Host, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect health probe targets - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -496,10 +424,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *sslCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SSLCertificate changes affect HTTPS listeners - Out: true, // Application Gateway changes (like deletion) affect the SSL certificate - }, }) } @@ -518,10 +442,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vaultName, secretName), Scope: n.DefaultScope(), // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → SSL certificate access is affected (In: true) - Out: false, // If Application Gateway is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -535,11 +455,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked bidirectionally - In: true, - Out: true, - }, }) } } @@ -558,10 +473,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *urlPathMap.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // URLPathMap changes affect path-based routing - Out: true, // Application Gateway changes (like deletion) affect the URL path map - }, }) } } @@ -579,10 +490,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *authCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // AuthenticationCertificate changes affect backend authentication - Out: true, // Application Gateway changes (like deletion) affect the authentication certificate - }, }) } } @@ -600,10 +507,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *trustedRootCert.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // TrustedRootCertificate changes affect backend server validation - Out: true, // Application Gateway changes (like deletion) affect the trusted root certificate - }, }) } @@ -622,10 +525,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(vaultName, secretName), Scope: n.DefaultScope(), // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Key Vault Secret is deleted/modified → TrustedRootCertificate access is affected (In: true) - Out: false, // If Application Gateway is deleted → Key Vault Secret remains (Out: false) - }, }) } @@ -639,11 +538,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked bidirectionally - In: true, - Out: true, - }, }) } } @@ -662,10 +556,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *rewriteRuleSet.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RewriteRuleSet changes affect request/response modification - Out: true, // Application Gateway changes (like deletion) affect the rewrite rule set - }, }) } } @@ -683,10 +573,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: shared.CompositeLookupKey(applicationGatewayName, *redirectConfig.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // RedirectConfiguration changes affect URL redirection behavior - Out: true, // Application Gateway changes (like deletion) affect the redirect configuration - }, }) } @@ -702,10 +588,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect redirect target availability - Out: true, // DNS names are always linked bidirectionally - }, }) } } @@ -728,10 +610,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: firewallPolicyName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // WAF Policy changes affect the Application Gateway's security configuration - Out: false, // Application Gateway changes don't affect the WAF policy itself - }, }) } } @@ -754,12 +632,6 @@ func (n networkApplicationGatewayWrapper) azureApplicationGatewayToSDPItem(appli Query: identityName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Application Gateway depends on managed identity for authentication (e.g., Key Vault integration for SSL certificates) - // If identity is deleted/modified, Application Gateway operations may fail - In: true, - Out: false, - }, }) } } diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index d5ba165d..4369d479 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -154,10 +154,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *frontendIPConfig.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // FrontendIPConfiguration changes affect the load balancer's frontend configuration - Out: true, // Load balancer changes (like deletion) affect the frontend IP configuration - }, // FrontendIPConfiguration is a child resource of the Load Balancer; bidirectional dependency }) } @@ -184,10 +180,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: publicIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes (like deletion or reassignment) affect the load balancer's frontend - Out: false, // Load balancer changes don't affect the public IP address itself - }, // Public IP provides the frontend IP for the load balancer }) } } @@ -210,10 +202,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like address space modifications) affect the load balancer's network configuration - Out: false, // Load balancer changes don't affect the subnet itself - }, }) } } @@ -234,10 +222,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Gateway LB frontend changes affect this load balancer's chained configuration - Out: false, // This LB changes don't affect the gateway LB frontend - }, }) } } @@ -258,10 +242,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: publicIPPrefixName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP prefix changes affect the load balancer's frontend allocation - Out: false, // Load balancer changes don't affect the public IP prefix - }, }) } } @@ -275,11 +255,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: *frontendIPConfig.Properties.PrivateIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -299,10 +274,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *backendPool.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // BackendAddressPool changes affect which backends receive traffic - Out: true, // Load balancer changes (like deletion) affect the backend address pool - }, }) } @@ -322,10 +293,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // VNet changes affect backend pool scope/connectivity - Out: false, // Load balancer changes don't affect the virtual network - }, }) } } @@ -351,10 +318,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -373,10 +336,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(params[0], params[1]), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -395,10 +354,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -411,10 +366,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: *addr.Properties.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } @@ -435,10 +386,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *natRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // InboundNatRule changes affect the load balancer's NAT configuration - Out: true, // Load balancer changes (like deletion) affect the NAT rules - }, // InboundNatRule is a child resource of the Load Balancer; bidirectional dependency }) } @@ -461,10 +408,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: nicName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Network interface changes affect NAT rule routing - Out: true, // NAT rule changes affect which network interface receives inbound traffic - }, // Inbound NAT rules map traffic to specific network interfaces; bidirectional operational dependency }) } } @@ -484,10 +427,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *lbRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // LoadBalancingRule changes affect how traffic is distributed - Out: true, // Load balancer changes (like deletion) affect the load balancing rules - }, // LoadBalancingRule is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -506,10 +445,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *probe.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Probe changes affect health monitoring of backend instances - Out: true, // Load balancer changes (like deletion) affect the probes - }, // Probe is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -528,10 +463,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *outboundRule.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // OutboundRule changes affect outbound connectivity configuration - Out: true, // Load balancer changes (like deletion) affect the outbound rules - }, // OutboundRule is a child resource of the Load Balancer; bidirectional dependency }) } } @@ -550,10 +481,6 @@ func (n networkLoadBalancerWrapper) azureLoadBalancerToSDPItem(loadBalancer *arm Query: shared.CompositeLookupKey(loadBalancerName, *natPool.Name), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // InboundNatPool changes affect NAT pool configuration - Out: true, // Load balancer changes (like deletion) affect the NAT pools - }, // InboundNatPool is a child resource of the Load Balancer; bidirectional dependency }) } } diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index de75607c..7c7d75e9 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -115,10 +115,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *networkInterface.Name, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // IP configuration changes don't affect the network interface itself - Out: true, // Network interface changes (especially deletion) affect IP configurations - }, // IP configurations are child resources of the network interface }) if networkInterface.Properties != nil && networkInterface.Properties.VirtualMachine != nil { @@ -132,10 +128,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: vmName, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // VM changes (like deletion) may detach the interface but don't delete it - Out: true, // Network interface changes/deletion directly affect VM network connectivity - }, // Network interface provides connectivity to the VM; bidirectional operational dependency }) } } @@ -152,10 +144,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: nsgName, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NSG rule changes affect the network interface's security and traffic flow - Out: false, // Network interface changes don't affect the NSG itself - }, // NSG controls security rules applied to the network interface }) } } @@ -177,10 +165,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: peName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes affect the NIC's role - Out: true, // NIC changes affect the private endpoint's connectivity - }, }) } } @@ -201,10 +185,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: plsName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private link service changes affect the NIC - Out: true, // NIC changes affect the private link service - }, }) } } @@ -225,10 +205,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: dscpName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DSCP config changes affect NIC QoS - Out: false, // NIC changes don't affect the DSCP configuration resource - }, }) } } @@ -241,10 +217,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *networkInterface.Name, Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Tap config changes don't affect the NIC itself - Out: true, // NIC changes (e.g. deletion) affect tap configurations - }, }) // IP configuration references: subnet, public IP, private IP (stdlib), ASGs, LB pools/rules, App Gateway pools, gateway LB, VNet taps @@ -271,10 +243,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (e.g. address space) affect the NIC's IP config - Out: false, // NIC changes don't affect the subnet resource - }, }) } } @@ -294,10 +262,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: pipName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP changes affect the NIC's connectivity - Out: true, // NIC detachment affects the public IP's association - }, }) } } @@ -312,10 +276,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: addr, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } @@ -336,10 +296,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG rule changes affect the NIC's effective rules - Out: false, // NIC changes don't affect the ASG - }, }) } } @@ -363,10 +319,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Pool config changes affect which backends receive traffic - Out: true, // NIC removal affects the pool's members - }, }) } } @@ -390,10 +342,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NAT rule changes affect the NIC - Out: true, // NIC removal affects the NAT rule's target - }, }) } } @@ -417,10 +365,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // App GW pool changes affect backend targets - Out: true, // NIC removal affects the pool's members - }, }) } } @@ -442,10 +386,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: shared.CompositeLookupKey(params[0], params[1]), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Gateway LB frontend changes affect traffic path - Out: true, // NIC changes affect the gateway LB association - }, }) } } @@ -467,10 +407,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: tapName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Tap config changes affect what is mirrored - Out: true, // NIC removal affects the tap's sources - }, }) } } @@ -498,10 +434,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *dns.InternalDNSNameLabel, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } if dns.InternalFqdn != nil && *dns.InternalFqdn != "" { @@ -512,10 +444,6 @@ func (n networkNetworkInterfaceWrapper) azureNetworkInterfaceToSDPItem(networkIn Query: *dns.InternalFqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index 949e87d4..5e072ab1 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -152,10 +152,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(nsgName, *securityRule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Security rule changes affect the NSG's behavior - Out: false, // NSG changes don't affect individual rules (rules are part of NSG) - }, }) } } @@ -173,10 +169,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(nsgName, *defaultSecurityRule.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Default security rule changes affect the NSG's behavior - Out: false, // NSG changes don't affect individual default rules (rules are part of NSG) - }, }) } } @@ -213,10 +205,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like deletion) affect the NSG association - Out: true, // NSG rule changes affect traffic in the subnet - }, }) } } @@ -243,10 +231,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: nicName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Network interface changes don't affect the NSG - Out: true, // NSG rule changes affect traffic on the network interface - }, }) } } @@ -277,10 +261,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: shared.CompositeLookupKey(networkWatcherName, flowLogName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Flow log config changes affect the NSG's observability - Out: false, // NSG changes don't affect the flow log resource - }, }) } } @@ -313,10 +293,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the security rule's source criteria - Out: false, // Security rule changes don't affect the ASG - }, }) } } @@ -341,10 +317,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the security rule's destination criteria - Out: false, // Security rule changes don't affect the ASG - }, }) } } @@ -394,10 +366,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the default security rule's source criteria - Out: false, // Default security rule changes don't affect the ASG - }, }) } } @@ -422,10 +390,6 @@ func (n networkNetworkSecurityGroupWrapper) azureNetworkSecurityGroupToSDPItem(n Query: asgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // ASG changes affect the default security rule's destination criteria - Out: false, // Default security rule changes don't affect the ASG - }, }) } } diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index 85028dd1..eecf5e3e 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -143,11 +143,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: *publicIPAddress.Properties.IPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } @@ -160,11 +155,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: *publicIPAddress.Properties.DNSSettings.Fqdn, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -189,10 +179,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: nicName[0], Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Network interface IP configuration changes affect the public IP address - Out: false, // Public IP address changes don't affect the network interface itself - }, // Public IP address is associated with the network interface's IP configuration }) } } @@ -217,10 +203,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: linkedIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Linked public IP address changes can affect this public IP address - Out: true, // This public IP address changes can affect the linked public IP address - }, // Linked public IP addresses are tightly coupled and affect each other }) } } @@ -244,10 +226,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: serviceIPName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Service public IP address changes can affect this public IP address - Out: false, // This public IP address changes don't affect the service public IP address - }, // Service public IP address is the underlying resource for this public IP address }) } } @@ -271,10 +249,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: prefixName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Public IP prefix changes can affect this public IP address - Out: false, // This public IP address changes don't affect the public IP prefix - }, // Public IP address is allocated from the public IP prefix }) } } @@ -298,10 +272,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: natGatewayName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // NAT gateway changes can affect this public IP address - Out: false, // This public IP address changes don't affect the NAT gateway - }, // Public IP address is associated with the NAT gateway for outbound connectivity }) } } @@ -326,10 +296,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: ddosPlanName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DDoS protection plan changes can affect this public IP address protection - Out: false, // This public IP address changes don't affect the DDoS protection plan - }, // Public IP address is protected by the DDoS protection plan }) } } @@ -357,10 +323,6 @@ func (n networkPublicIPAddressWrapper) azurePublicIPAddressToSDPItem(publicIPAdd Query: lbName[0], Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Load balancer frontend IP configuration changes affect the public IP address - Out: false, // Public IP address changes don't affect the load balancer itself - }, // Public IP address is associated with the load balancer's frontend IP configuration }) } } diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index f99f6bdb..f8971218 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -159,9 +159,6 @@ func TestNetworkPublicIPAddress(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.NetworkPublicIPAddress.String() && linkedQuery.GetQuery().GetQuery() == linkedIPName { foundLinkedIP = true - if linkedQuery.GetBlastPropagation().GetIn() != true || linkedQuery.GetBlastPropagation().GetOut() != true { - t.Error("Expected linked public IP to have In: true, Out: true") - } break } } @@ -195,9 +192,6 @@ func TestNetworkPublicIPAddress(t *testing.T) { if linkedQuery.GetQuery().GetType() == azureshared.NetworkPublicIPAddress.String() && linkedQuery.GetQuery().GetQuery() == serviceIPName { foundServiceIP = true - if linkedQuery.GetBlastPropagation().GetIn() != true || linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected service public IP to have In: true, Out: false") - } break } } diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index 2ec4d7bd..e0c1e839 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -121,10 +121,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: shared.CompositeLookupKey(routeTableName, *route.Name), Scope: n.DefaultScope(), }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Route changes affect the route table's routing behavior - Out: false, // Route table changes don't affect individual routes (routes are part of the table) - }, }) // Link to NextHopIPAddress (IP address to stdlib) @@ -137,11 +133,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: *route.Properties.NextHopIPAddress, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked bidirectionally - In: true, - Out: true, - }, }) } @@ -179,10 +170,6 @@ func (n networkRouteTableWrapper) azureRouteTableToSDPItem(routeTable *armnetwor Query: shared.CompositeLookupKey(vnetName, subnetName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Subnet changes (like route table association) affect the route table's usage - Out: true, // Route table changes affect traffic routing in the subnet - }, }) } } diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index 169f838a..1d24fe8b 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -140,10 +140,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Scope: scope, Query: *network.Name, // List subnets in the virtual network }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, - Out: true, - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -153,10 +149,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Scope: scope, Query: *network.Name, // List virtual network peerings in the virtual network }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Peering changes don't affect the Virtual Network itself - Out: true, // Virtual Network changes (especially deletion) affect peerings - }, }) // Link to DDoS protection plan @@ -177,10 +169,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: ddosPlanName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DDoS protection plan changes → Virtual Network protection affected (In: true) - Out: false, // If Virtual Network is deleted → DDoS protection plan remains (Out: false) - }, }) } } @@ -210,10 +198,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: nsgName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NSG changes → Subnet security rules affected (In: true) - Out: false, // If Virtual Network is deleted → NSG remains (Out: false) - }, }) } } @@ -236,10 +220,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: routeTableName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Route Table changes → Subnet routing affected (In: true) - Out: false, // If Virtual Network is deleted → Route Table remains (Out: false) - }, }) } } @@ -262,10 +242,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: natGatewayName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Gateway changes → Subnet outbound connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → NAT Gateway remains (Out: false) - }, }) } } @@ -290,10 +266,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: privateEndpointName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If Private Endpoint changes → Subnet connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → Private Endpoint may become invalid (Out: false, but could be true) - }, }) } } @@ -322,10 +294,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: remoteVNetName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If remote VNet changes → Peering connectivity affected (In: true) - Out: true, // If this VNet changes → Remote VNet peering affected (Out: true) - }, }) } } @@ -349,10 +317,6 @@ func (n networkVirtualNetworkWrapper) azureVirtualNetworkToSDPItem(network *armn Query: natGatewayName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If NAT Gateway changes → VNet outbound connectivity affected (In: true) - Out: false, // If Virtual Network is deleted → NAT Gateway remains (Out: false) - }, }) } } diff --git a/sources/azure/manual/network-zone.go b/sources/azure/manual/network-zone.go index a5d904ff..8bfea04e 100644 --- a/sources/azure/manual/network-zone.go +++ b/sources/azure/manual/network-zone.go @@ -122,11 +122,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: zoneName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } @@ -150,12 +145,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS zone depends on virtual network for registration - // If virtual network is deleted/modified, DNS zone registration may fail - In: true, - Out: false, - }, // Virtual network provides registration capability for the DNS zone }) } } @@ -182,12 +171,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: vnetName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS zone depends on virtual network for resolution - // If virtual network is deleted/modified, DNS zone resolution may fail - In: true, - Out: false, - }, // Virtual network provides resolution capability for the DNS zone }) } } @@ -205,12 +188,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: zoneName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Record sets are child resources of the DNS zone - // Changes to record sets affect DNS resolution for the zone - In: true, - Out: true, - }, // Record sets are tightly coupled with the DNS zone; bidirectional dependency }) // Link to DNS names (standard library) from NameServers @@ -225,11 +202,6 @@ func (n networkZoneWrapper) azureZoneToSDPItem(zone *armdns.Zone, scope string) Query: *nameServer, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index 054523de..f22feca3 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -85,10 +85,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: extractedServerName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes (especially deletion) affect the database's availability and configuration - Out: false, // Database changes don't affect the SQL Server itself - }, // SQL Database is a child resource that depends on its parent SQL Server }) } } @@ -103,10 +99,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: elasticPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Elastic pool changes (especially deletion or resource configuration changes) affect the database's performance and availability - Out: false, // Database changes don't affect the elastic pool itself (though they may affect pool resource usage) - }, // SQL Database depends on its Elastic Pool for resource allocation and management }) } } @@ -124,10 +116,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(recoverableServerName, recoverableDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Recoverable database deletion or unavailability affects the SQL Database's ability to restore from that point - Out: false, // SQL Database changes don't affect the recoverable database itself (it's a point-in-time snapshot) - }, // SQL Database depends on its recoverable database for disaster recovery and restore capabilities }) } } @@ -145,10 +133,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(restorableDroppedServerName, restorableDroppedDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Restorable dropped database deletion/purge affects the SQL Database's ability to restore from that dropped database - Out: false, // SQL Database changes don't affect the restorable dropped database itself (it's already dropped) - }, // SQL Database depends on its restorable dropped database for restore capabilities after accidental deletion }) } } @@ -161,10 +145,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Recovery point deletion affects the SQL Database's ability to restore from that specific backup point - Out: false, // SQL Database changes don't affect the recovery point itself (it's a point-in-time snapshot) - }, // SQL Database depends on Recovery Services recovery points for backup and restore capabilities }) } @@ -180,10 +160,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(sourceServerName, sourceDatabaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Source database changes don't affect the copy (copy is independent after creation) - Out: false, // Copy database changes don't affect the source database - }, // Database copy is independent from its source after the copy operation completes }) } } @@ -205,10 +181,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(serverName, databaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Source database changes (especially deletion) affect the database's ability to restore - Out: false, // Database changes don't affect the source database itself - }, // SQL Database depends on the source SQL database for restore/recovery operations }) case azureshared.SourceResourceTypeSQLElasticPool: @@ -220,10 +192,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: elasticPoolName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Source elastic pool changes (especially deletion) affect the database's ability to restore - Out: false, // Database changes don't affect the source elastic pool itself - }, // SQL Database depends on the source SQL elastic pool for restore/recovery operations }) case azureshared.SourceResourceTypeUnknown: @@ -250,10 +218,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(failoverServerName, failoverGroupName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Failover group deletion or failover affects the database's availability and replication - Out: false, // Database membership in the group doesn't change the failover group configuration - }, // SQL Database belongs to a Failover Group for high availability }) } } @@ -272,10 +236,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(locationName, ltrServerName, ltrDatabaseName, backupName), Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // LTR backup deletion affects the database's ability to restore from that backup - Out: false, // SQL Database changes don't affect the LTR backup itself - }, // SQL Database depends on LTR backup for long-term retention restore }) } } @@ -297,10 +257,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: configName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Maintenance config changes affect when maintenance updates occur for the database - Out: false, // Database changes don't affect the maintenance configuration itself - }, // SQL Database uses Maintenance Configuration for update scheduling }) } } @@ -323,10 +279,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(vaultName, keyName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault Key deletion/rotation affects database encryption - Out: false, // Database changes don't affect the Key Vault Key - }, // SQL Database uses Key Vault Key for per-database CMK and encryption at rest }) } if database.Properties != nil && database.Properties.EncryptionProtector != nil && *database.Properties.EncryptionProtector != "" { @@ -359,10 +311,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // User Assigned Identity deletion affects database identity and CMK access - Out: false, // Database changes don't affect the User Assigned Identity - }, // SQL Database uses User Assigned Identity for Azure AD auth and CMK }) } } @@ -377,10 +325,6 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, Query: shared.CompositeLookupKey(serverName, databaseName), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Schema changes don't affect the parent database resource - Out: true, // Database deletion removes all schemas - }, // Database Schemas are child resources of the SQL Database }) return sdpItem, nil diff --git a/sources/azure/manual/sql-server.go b/sources/azure/manual/sql-server.go index 8639384b..75027a92 100644 --- a/sources/azure/manual/sql-server.go +++ b/sources/azure/manual/sql-server.go @@ -144,10 +144,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes (especially deletion) affect databases - Out: false, // Database changes don't affect the SQL Server itself - }, // SQL Databases are child resources that depend on their parent SQL Server }) // Link to Elastic Pools (child resource) @@ -160,10 +156,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect elastic pools - Out: false, // Elastic pool changes don't affect the SQL Server itself - }, // SQL Elastic Pools are child resources that depend on their parent SQL Server }) // Link to Firewall Rules (child resource) @@ -176,10 +168,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect firewall rules - Out: true, // Firewall rule changes affect server connectivity - }, // SQL Server Firewall Rules are child resources that control server access }) // Link to Virtual Network Rules (child resource) @@ -192,10 +180,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect virtual network rules - Out: true, // Virtual network rule changes affect server connectivity - }, // SQL Server Virtual Network Rules are child resources that control server access }) // Link to Server Keys (child resource) @@ -208,10 +192,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect server keys - Out: true, // Server key changes (especially deletion) affect server encryption and availability - }, // SQL Server Keys are child resources used for encryption }) // Link to Failover Groups (child resource) @@ -224,10 +204,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect failover groups - Out: true, // Failover group changes affect server availability and failover behavior - }, // SQL Server Failover Groups are child resources that manage high availability }) // Link to Administrators (child resource) @@ -240,10 +216,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect administrators - Out: true, // Administrator changes affect server authentication and access control - }, // SQL Server Administrators are child resources that control authentication }) // Link to Sync Groups (child resource) @@ -256,10 +228,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect sync groups - Out: false, // Sync group changes don't affect the SQL Server itself - }, // SQL Server Sync Groups are child resources for data synchronization }) // Link to Sync Agents (child resource) @@ -272,10 +240,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect sync agents - Out: false, // Sync agent changes don't affect the SQL Server itself - }, // SQL Server Sync Agents are child resources for data synchronization }) // Link to Private Endpoint Connections (child resource) @@ -288,10 +252,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect private endpoint connections - Out: true, // Private endpoint connection changes affect server network connectivity - }, // SQL Server Private Endpoint Connections are child resources that manage private network access }) // Link to Network Private Endpoints (external resources) from PrivateEndpointConnections @@ -315,10 +275,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: privateEndpointName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Private endpoint changes (deletion, network configuration) affect the SQL Server's private connectivity - Out: true, // SQL Server deletion or configuration changes may affect the private endpoint's connection state - }, // Private endpoints are tightly coupled to the SQL Server - changes affect connectivity }) } } @@ -335,10 +291,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect auditing settings - Out: true, // Auditing setting changes affect server security and compliance - }, // SQL Server Auditing Settings are child resources that control audit logging }) // Link to Security Alert Policies (child resource) @@ -351,10 +303,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect security alert policies - Out: true, // Security alert policy changes affect server security monitoring - }, // SQL Server Security Alert Policies are child resources that control threat detection }) // Link to Vulnerability Assessments (child resource) @@ -367,10 +315,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect vulnerability assessments - Out: true, // Vulnerability assessment changes affect server security scanning - }, // SQL Server Vulnerability Assessments are child resources that control security scanning }) // Link to Encryption Protector (child resource) @@ -383,10 +327,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect encryption protector - Out: true, // Encryption protector changes affect server encryption and data access - }, // SQL Server Encryption Protector is a child resource that controls encryption }) // Link to Blob Auditing Policies (child resource) @@ -399,10 +339,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect blob auditing policies - Out: true, // Blob auditing policy changes affect server audit logging - }, // SQL Server Blob Auditing Policies are child resources that control blob audit logging }) // Link to Automatic Tuning (child resource) @@ -415,10 +351,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect automatic tuning - Out: true, // Automatic tuning changes affect server performance optimization - }, // SQL Server Automatic Tuning is a child resource that controls performance optimization }) // Link to Advanced Threat Protection Settings (child resource) @@ -431,10 +363,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect advanced threat protection settings - Out: true, // Advanced threat protection setting changes affect server security - }, // SQL Server Advanced Threat Protection Settings are child resources that control threat protection }) // Link to DNS Aliases (child resource) @@ -447,10 +375,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect DNS aliases - Out: true, // DNS alias changes affect server connectivity and routing - }, // SQL Server DNS Aliases are child resources that provide alternate DNS names }) // Link to Server Usages (child resource) @@ -463,10 +387,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect usage metrics - Out: false, // Usage metrics are read-only and don't affect the server - }, // SQL Server Usages are child resources that provide usage metrics }) // Link to Server Operations (child resource) @@ -479,10 +399,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect operations history - Out: false, // Operations history is read-only and doesn't affect the server - }, // SQL Server Operations are child resources that provide operation history }) // Link to Server Advisors (child resource) @@ -495,10 +411,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect advisors - Out: false, // Advisor recommendations don't affect the server until applied - }, // SQL Server Advisors are child resources that provide optimization recommendations }) // Link to Backup Long-Term Retention Policies (child resource) @@ -511,10 +423,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect backup retention policies - Out: true, // Backup retention policy changes affect backup management and storage - }, // SQL Server Backup Long-Term Retention Policies are child resources that control backup retention }) // Link to DevOps Audit Settings (child resource) @@ -527,10 +435,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect DevOps audit settings - Out: true, // DevOps audit setting changes affect server audit logging - }, // SQL Server DevOps Audit Settings are child resources that control DevOps audit logging }) // Link to Server Trust Groups (child resource) @@ -543,10 +447,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect trust groups - Out: true, // Trust group changes affect server trust relationships - }, // SQL Server Trust Groups are child resources that manage trust relationships }) // Link to Outbound Firewall Rules (child resource) @@ -559,10 +459,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect outbound firewall rules - Out: true, // Outbound firewall rule changes affect server outbound connectivity - }, // SQL Server Outbound Firewall Rules are child resources that control outbound access }) // Link to Private Link Resources (child resource) @@ -575,10 +471,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // SQL Server changes affect private link resources - Out: false, // Private link resources are metadata about available private endpoints - }, // SQL Server Private Link Resources are child resources that provide private link metadata }) // Link to Long Term Retention Backups (child resource) @@ -591,10 +483,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: serverName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // LTR backup changes don't affect the SQL Server itself - Out: true, // SQL Server changes (deletion) affect LTR backups - }, // SQL Server Long Term Retention Backups are child resources that can be listed by server }) } @@ -620,10 +508,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed identity deletion/modification affects server authentication and operations - Out: false, // Server changes don't affect the managed identity itself - }, // SQL Server depends on managed identity for authentication }) processedIdentityIDs[*server.Properties.PrimaryUserAssignedIdentityID] = true } @@ -651,10 +535,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: identityName, Scope: extractedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Managed identity deletion/modification affects server authentication and operations - Out: false, // Server changes don't affect the managed identity itself - }, // SQL Server depends on managed identity for authentication }) processedIdentityIDs[identityResourceID] = true } @@ -682,10 +562,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Key Vault changes (key deletion, rotation, access policy) affect the SQL Server's encryption - Out: false, // SQL Server changes don't directly affect the Key Vault - }, // SQL Server depends on Key Vault for customer-managed encryption keys - key changes impact encryption/decryption }) } } @@ -700,10 +576,6 @@ func (s sqlServerWrapper) azureSqlServerToSDPItem(server *armsql.Server, scope s Query: *server.Properties.FullyQualifiedDomainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // DNS name changes affect server connectivity - Out: true, // DNS names are always linked bidirectionally - }, // DNS names are shared resources that affect multiple entities }) } } diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index 3f069802..d2165407 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -422,10 +422,6 @@ func TestSqlServer(t *testing.T) { if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected NetworkPrivateEndpoint method GET, got %v", link.GetQuery().GetMethod()) } - if link.GetBlastPropagation().GetIn() != true || link.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected NetworkPrivateEndpoint BlastPropagation In=true, Out=true, got In=%v, Out=%v", - link.GetBlastPropagation().GetIn(), link.GetBlastPropagation().GetOut()) - } } } @@ -485,10 +481,6 @@ func TestSqlServer(t *testing.T) { if link.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected KeyVault method GET, got %v", link.GetQuery().GetMethod()) } - if link.GetBlastPropagation().GetIn() != true || link.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected KeyVault BlastPropagation In=true, Out=false, got In=%v, Out=%v", - link.GetBlastPropagation().GetIn(), link.GetBlastPropagation().GetOut()) - } } } diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index 8430bfa0..5ab37a6e 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -143,10 +143,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if blob containers change - Out: true, // Blob containers ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -156,10 +152,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if file shares change - Out: true, // File shares ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -169,10 +161,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if tables change - Out: true, // Tables ARE affected if storage account changes/deletes - }, }) sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ @@ -182,10 +170,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: false, // Storage account is NOT affected if queues change - Out: true, // Queues ARE affected if storage account changes/deletes - }, }) // Link to Private Endpoint Connections (child resource) @@ -198,12 +182,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: accountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connections are child resources of the storage account - // Changes to storage account affect connections, and connection state affects storage access - In: true, - Out: true, - }, }) // Link to User Assigned Managed Identities (external resources) @@ -224,12 +202,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on managed identity for authentication - // If identity is deleted/modified, storage account operations may fail - In: true, - Out: false, - }, }) } } @@ -257,12 +229,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: vaultName, Scope: scope, // Limitation: Key Vault URI doesn't contain resource group info }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on Key Vault for customer-managed encryption keys - // If Key Vault is deleted/modified or key is rotated, storage account encryption may be affected - In: true, - Out: false, - }, }) } } @@ -287,12 +253,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: identityName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on managed identity for encryption key access - // If identity is deleted/modified, storage account encryption operations may fail - In: true, - Out: false, - }, }) } } @@ -326,12 +286,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: query, Scope: scope, // Use the subnet's scope, not the storage account's scope }, - BlastPropagation: &sdp.BlastPropagation{ - // Storage account depends on subnet for network access control - // If subnet is deleted/modified, storage account network access may be affected - In: true, - Out: false, - }, }) } } @@ -349,11 +303,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *ipRule.IPAddressOrRange, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -370,11 +319,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *ipRule.IPAddressOrRange, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // IPs are always linked - In: true, - Out: true, - }, }) } } @@ -400,12 +344,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: privateEndpointName, Scope: linkedScope, }, - BlastPropagation: &sdp.BlastPropagation{ - // Private endpoint connection is tightly coupled with the storage account - // Changes to either affect the other - In: true, - Out: true, - }, }) } } @@ -438,11 +376,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -475,11 +408,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } } @@ -495,11 +423,6 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. Query: *account.Properties.CustomDomain.Name, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // DNS names are always linked - In: true, - Out: true, - }, }) } diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index ec50794d..532c32f5 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -157,10 +157,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) // Link to DNS name (standard library) from blob container URI @@ -176,10 +172,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If DNS name is unavailable → blob container becomes inaccessible (In: true) - Out: true, // If blob container is deleted → DNS name may still be used by other resources (Out: true) - }, // Blob container depends on DNS name for endpoint resolution }) } @@ -192,10 +184,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: blobContainerURI, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If HTTP endpoint is unavailable → blob container becomes inaccessible (In: true) - Out: true, // If blob container is deleted → HTTP endpoint may still be used by other resources (Out: true) - }, // Blob container depends on HTTP endpoint for access }) } @@ -208,10 +196,6 @@ func (s storageBlobContainerWrapper) azureBlobContainerToSDPItem(container *arms Query: shared.CompositeLookupKey(storageAccountName, *container.ContainerProperties.DefaultEncryptionScope), Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // If encryption scope is removed or changed → container's default encryption is affected - Out: false, // Container deletion does not affect the encryption scope - }, }) } diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index c4b1358c..86a391be 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -130,12 +130,6 @@ func TestStorageBlobContainer(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected StorageAccount linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected StorageAccount BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected StorageAccount BlastPropagation.Out to be false") - } case "dns": hasDNSLink = true if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { @@ -213,12 +207,6 @@ func TestStorageBlobContainer(t *testing.T) { if linkedQuery.GetQuery().GetScope() != subscriptionID+"."+resourceGroup { t.Errorf("Expected StorageEncryptionScope scope %s, got %s", subscriptionID+"."+resourceGroup, linkedQuery.GetQuery().GetScope()) } - if !linkedQuery.GetBlastPropagation().GetIn() { - t.Error("Expected StorageEncryptionScope BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() { - t.Error("Expected StorageEncryptionScope BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index 2ab24c9d..354702db 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -154,12 +154,6 @@ func (s storageFileShareWrapper) azureFileShareToSDPItem(fileShare *armstorage.F Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - // File Share depends on Storage Account (parent); deletion/change of Storage Account affects File Share. - // Storage Account is not affected when a child File Share is deleted. - In: true, - Out: false, - }, }) return sdpItem, nil diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index a9ec705a..a29503b0 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -128,12 +128,6 @@ func TestStorageFileShare(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index 47d673ee..3af9df7f 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -80,10 +80,6 @@ func (s storageQueuesWrapper) azureQueueToSDPItem(queue *armstorage.Queue, stora Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Queue depends on storage account; account deletion/change affects the queue. - Out: false, // Storage account is not affected by queue create/update/delete. - }, }) return sdpItem, nil diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index ca61a425..ad631a15 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -129,12 +129,6 @@ func TestStorageQueues(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index 921e63db..7ee50f70 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -86,10 +86,6 @@ func (s storageTablesWrapper) azureTableToSDPItem(table *armstorage.Table, stora Query: storageAccountName, Scope: scope, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, // Table is affected if parent storage account changes or is deleted - Out: false, // Table changes/deletes do not affect the storage account - }, }) return sdpItem, nil diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index 177a8ace..86d1b2ad 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -129,12 +129,6 @@ func TestStorageTables(t *testing.T) { if linkedQuery.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } }) }) diff --git a/sources/example/custom_searchable_listable.go b/sources/example/custom_searchable_listable.go index bb118a7e..94ad2997 100644 --- a/sources/example/custom_searchable_listable.go +++ b/sources/example/custom_searchable_listable.go @@ -142,10 +142,6 @@ func (d *customComputeInstanceWrapper) externalTypeToSDPItem(external *ExternalT Query: external.LinkedItemID, Scope: d.Scopes()[0], }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/sources/example/standard_searchable_listable.go b/sources/example/standard_searchable_listable.go index 9eaa051f..1d6097f5 100644 --- a/sources/example/standard_searchable_listable.go +++ b/sources/example/standard_searchable_listable.go @@ -163,10 +163,6 @@ func (d *computeInstanceWrapper) externalTypeToSDPItem(external *ExternalType) ( Query: external.LinkedItemID, Scope: d.Scopes()[0], }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }, }, } diff --git a/sources/gcp/manual/certificate-manager-certificate.go b/sources/gcp/manual/certificate-manager-certificate.go index 973ddc42..43bb47b2 100644 --- a/sources/gcp/manual/certificate-manager-certificate.go +++ b/sources/gcp/manual/certificate-manager-certificate.go @@ -234,12 +234,6 @@ func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificat Query: dnsName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificate depends on DNS resolution - // DNS changes affect certificate validity - In: true, - Out: true, - }, }) } } @@ -256,12 +250,6 @@ func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificat Query: domain, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificate depends on DNS resolution for domain validation - // DNS changes affect certificate provisioning - In: true, - Out: true, - }, }) } } @@ -277,13 +265,6 @@ func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificat Query: shared.CompositeLookupKey(values[0], values[1]), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificate depends on DNS authorization for domain validation - // If DNS authorization is deleted, certificate provisioning fails - // Deleting certificate doesn't affect the DNS authorization - In: true, - Out: false, - }, }) } } @@ -301,13 +282,6 @@ func (c certificateManagerCertificateWrapper) gcpCertificateToSDPItem(certificat Query: shared.CompositeLookupKey(values[0], values[1]), Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - // Certificate depends on issuance config for private PKI - // If issuance config is deleted, certificate provisioning fails - // Deleting certificate doesn't affect the issuance config - In: true, - Out: false, - }, }) } } diff --git a/sources/gcp/manual/storage-bucket-iam-policy.go b/sources/gcp/manual/storage-bucket-iam-policy.go index 7c470a3f..3ce013d0 100644 --- a/sources/gcp/manual/storage-bucket-iam-policy.go +++ b/sources/gcp/manual/storage-bucket-iam-policy.go @@ -184,10 +184,6 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location Query: bucketName, Scope: location.ProjectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: true, - }, }) // Collect unique linked SAs, projects, domains, and custom IAM roles across all bindings. @@ -231,10 +227,6 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location Query: saEmail, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } for projectID, roleIDs := range linkedRoles { @@ -246,10 +238,6 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location Query: roleID, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } } @@ -261,10 +249,6 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location Query: projectID, Scope: projectID, }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } for domainName := range linkedDomains { @@ -275,10 +259,6 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location Query: domainName, Scope: "global", }, - BlastPropagation: &sdp.BlastPropagation{ - In: true, - Out: false, - }, }) } From 1a176abc028b920208b632160ff64de595c799e1 Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Mon, 23 Feb 2026 16:30:26 +0100 Subject: [PATCH 47/51] Hypothesis status skipped state (#3973) Add `SKIPPED` to the `HypothesisStatus` enum to unblock follow-up work for handling skipped hypotheses. --- Linear Issue: [ENG-2717](https://linear.app/overmind/issue/ENG-2717/add-skipped-to-hypothesisstatus-enum)

Open in Web Open in Cursor 

--- > [!NOTE] > **Medium Risk** > Proto schema changes require coordinated updates across services/clients and can break consumers that assume the previous enum/field set. The server-side logic change is small but affects API output for change timelines. > > **Overview** > Adds a new `HypothesisStatus` enum value `INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED` and extends `InvestigateHypothesesTimelineEntry` with `numSkipped` in `changes.proto` (and regenerated Go/TS protobuf outputs). > > Updates `GetInvestigateHypothesesTimelineEntry` to include skipped hypotheses in the returned summaries and to populate the new `NumSkipped` counter; tests are expanded to cover the new status and adjusted totals. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6f3f9b72a9070913e2bc67bd883175f1d973fd1e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: cd8facfe9540e0d10f13a636af0479a6733a1f85 --- go/sdp-go/changes.pb.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/go/sdp-go/changes.pb.go b/go/sdp-go/changes.pb.go index a728ee87..9a4130f2 100644 --- a/go/sdp-go/changes.pb.go +++ b/go/sdp-go/changes.pb.go @@ -148,6 +148,8 @@ const ( HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_PROVEN HypothesisStatus = 3 // The hypothesis has been disproven, no risk has been found HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN HypothesisStatus = 4 + // The hypothesis was skipped and not investigated + HypothesisStatus_INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED HypothesisStatus = 5 ) // Enum value maps for HypothesisStatus. @@ -158,6 +160,7 @@ var ( 2: "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING", 3: "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN", 4: "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN", + 5: "INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED", } HypothesisStatus_value = map[string]int32{ "INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED": 0, @@ -165,6 +168,7 @@ var ( "INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING": 2, "INVESTIGATED_HYPOTHESIS_STATUS_PROVEN": 3, "INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN": 4, + "INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED": 5, } ) @@ -2536,7 +2540,9 @@ type InvestigateHypothesesTimelineEntry struct { // Number of hypotheses that are still being investigated NumInvestigating uint32 `protobuf:"varint,3,opt,name=numInvestigating,proto3" json:"numInvestigating,omitempty"` // The current state of the hypotheses under investigation - Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + Hypotheses []*HypothesisSummary `protobuf:"bytes,4,rep,name=hypotheses,proto3" json:"hypotheses,omitempty"` + // Number of hypotheses that were skipped + NumSkipped uint32 `protobuf:"varint,5,opt,name=numSkipped,proto3" json:"numSkipped,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2599,6 +2605,13 @@ func (x *InvestigateHypothesesTimelineEntry) GetHypotheses() []*HypothesisSummar return nil } +func (x *InvestigateHypothesesTimelineEntry) GetNumSkipped() uint32 { + if x != nil { + return x.NumSkipped + } + return 0 +} + type HypothesisSummary struct { state protoimpl.MessageState `protogen:"open.v1"` // The status of the investigation @@ -6632,14 +6645,17 @@ const file_changes_proto_rawDesc = "" + "\rnumHypotheses\x18\x01 \x01(\rR\rnumHypotheses\x12:\n" + "\n" + "hypotheses\x18\x02 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"\xce\x01\n" + + "hypotheses\"\xee\x01\n" + "\"InvestigateHypothesesTimelineEntry\x12\x1c\n" + "\tnumProven\x18\x01 \x01(\rR\tnumProven\x12\"\n" + "\fnumDisproven\x18\x02 \x01(\rR\fnumDisproven\x12*\n" + "\x10numInvestigating\x18\x03 \x01(\rR\x10numInvestigating\x12:\n" + "\n" + "hypotheses\x18\x04 \x03(\v2\x1a.changes.HypothesisSummaryR\n" + - "hypotheses\"t\n" + + "hypotheses\x12\x1e\n" + + "\n" + + "numSkipped\x18\x05 \x01(\rR\n" + + "numSkipped\"t\n" + "\x11HypothesisSummary\x121\n" + "\x06status\x18\x01 \x01(\x0e2\x19.changes.HypothesisStatusR\x06status\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + @@ -6971,13 +6987,14 @@ const file_changes_proto_rawDesc = "" + "\"MAPPED_ITEM_MAPPING_STATUS_SUCCESS\x10\x01\x12*\n" + "&MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED\x10\x02\x12/\n" + "+MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION\x10\x03\x12$\n" + - " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xf9\x01\n" + + " MAPPED_ITEM_MAPPING_STATUS_ERROR\x10\x04*\xa5\x02\n" + "\x10HypothesisStatus\x12.\n" + "*INVESTIGATED_HYPOTHESIS_STATUS_UNSPECIFIED\x10\x00\x12*\n" + "&INVESTIGATED_HYPOTHESIS_STATUS_FORMING\x10\x01\x120\n" + ",INVESTIGATED_HYPOTHESIS_STATUS_INVESTIGATING\x10\x02\x12)\n" + "%INVESTIGATED_HYPOTHESIS_STATUS_PROVEN\x10\x03\x12,\n" + - "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04*_\n" + + "(INVESTIGATED_HYPOTHESIS_STATUS_DISPROVEN\x10\x04\x12*\n" + + "&INVESTIGATED_HYPOTHESIS_STATUS_SKIPPED\x10\x05*_\n" + "\x19ChangeTimelineEntryStatus\x12\x0f\n" + "\vUNSPECIFIED\x10\x00\x12\v\n" + "\aPENDING\x10\x01\x12\x0f\n" + From 87cbb3e5913a5758ec2447d8041414aff31a53fd Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Mon, 23 Feb 2026 18:47:50 +0100 Subject: [PATCH 48/51] Blast propagation proto removal (#3971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reserve `BlastPropagation` and `followOnlyBlastPropagation` fields from SDP protos to maintain wire-format compatibility and update documentation and tests to reflect their deprecation. This PR completes Phase 3 of the "Remove Blast Propagation Information" project (ENG-2404), following the prior code removal (ENG-2647). It ensures that old messages can still be parsed safely and prevents accidental reuse of field numbers. --- Linear Issue: [ENG-2404](https://linear.app/overmind/issue/ENG-2404/sdp-reserve-blastpropagation-and-followonlyblastpropagation-from)

Open in Web Open in Cursor 

--- > [!NOTE] > **Medium Risk** > Touches core protobuf contracts and regenerated client code; downstream services/clients relying on `blastPropagation` fields or reverse-edge filtering may break if not updated in lockstep. > > **Overview** > This PR **deprecates and effectively removes blast-propagation metadata from the SDP surface** by reserving the `BlastPropagation` fields in `sdp/items.proto` and `sdp/revlink.proto` (and regenerating Go/TS protobuf outputs) so old messages can still be parsed without allowing field-number reuse. > > It updates sources and tooling to stop setting/expecting `BlastPropagation` on links (e.g., AWS CloudWatch metric suggested queries, EC2 address links, snapshot edge→linked-item conversion), and strips Azure integration tests and docs/prompting guidance that referenced propagation semantics, reflecting the move to AI-driven blast radius calculation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 58035be2747c0f212919a366e08717efd786b30f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: ecdcb08090547689567725f2c37d33617b0456aa --- .../adapters/cloudwatch_metric_links.go | 9 - aws-source/adapters/ec2-address.go | 14 -- go/sdp-go/items.go | 6 - go/sdp-go/items.pb.go | 194 ++++++------------ go/sdp-go/revlink.pb.go | 20 +- .../authorization-role-assignment_test.go | 6 - .../batch-batch-accounts_test.go | 24 --- .../compute-availability-set_test.go | 14 -- ...compute-capacity-reservation-group_test.go | 11 - .../compute-dedicated-host-group_test.go | 11 - .../compute-disk-access_test.go | 11 - .../compute-disk-encryption-set_test.go | 40 ---- .../integration-tests/compute-disk_test.go | 15 +- ...ompute-gallery-application-version_test.go | 9 - .../integration-tests/compute-image_test.go | 23 +-- .../compute-proximity-placement-group_test.go | 9 - .../compute-snapshot_test.go | 15 +- .../compute-virtual-machine-extension_test.go | 35 ---- ...ompute-virtual-machine-run-command_test.go | 35 ---- .../compute-virtual-machine-scale-set_test.go | 21 -- .../compute-virtual-machine_test.go | 14 -- .../dbforpostgresql-database_test.go | 12 -- .../dbforpostgresql-flexible-server_test.go | 65 +----- .../keyvault-managed-hsm_test.go | 10 - .../integration-tests/keyvault-secret_test.go | 6 - ...gedidentity-user-assigned-identity_test.go | 7 - .../network-application-gateway_test.go | 10 - .../network-load-balancer_test.go | 42 ---- .../network-network-interface_test.go | 21 -- .../network-network-security-group_test.go | 31 +-- .../network-public-ip-address_test.go | 12 -- .../network-route-table_test.go | 26 +-- .../network-virtual-network_test.go | 12 -- .../integration-tests/network-zone_test.go | 39 ---- .../integration-tests/sql-database_test.go | 12 -- .../integration-tests/sql-server_test.go | 86 +------- .../integration-tests/storage-account_test.go | 12 -- .../integration-tests/storage-queues_test.go | 6 - .../integration-tests/storage-table_test.go | 6 - sources/snapshot/adapters/index.go | 3 +- 40 files changed, 82 insertions(+), 872 deletions(-) diff --git a/aws-source/adapters/cloudwatch_metric_links.go b/aws-source/adapters/cloudwatch_metric_links.go index 4cf460a0..b3816024 100644 --- a/aws-source/adapters/cloudwatch_metric_links.go +++ b/aws-source/adapters/cloudwatch_metric_links.go @@ -24,14 +24,6 @@ func SuggestedQuery(namespace string, scope string, dimensions []types.Dimension var query *sdp.Query var err error - bp := &sdp.BlastPropagation{ - // These links are the metrics that feed the alarms. If the thing that - // we're measuring changes, we definitely want the alarm to be in the - // blast radius. But an alarm on its own doesn't affect these things - In: false, - Out: true, - } - accountID, _, err := ParseScope(scope) if err != nil { @@ -244,7 +236,6 @@ func SuggestedQuery(namespace string, scope string, dimensions []types.Dimension return &sdp.LinkedItemQuery{ Query: query, - BlastPropagation: bp, }, err } diff --git a/aws-source/adapters/ec2-address.go b/aws-source/adapters/ec2-address.go index 473b0422..d96b444e 100644 --- a/aws-source/adapters/ec2-address.go +++ b/aws-source/adapters/ec2-address.go @@ -34,14 +34,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. var err error var attrs *sdp.ItemAttributes - // An EC2-address, along with an IP is an item that inherently links things - // and therefore should propagate blast radius in both directions on all - // links - bp := &sdp.BlastPropagation{ - In: true, - Out: true, - } - for _, address := range output.Addresses { attrs, err = ToAttributesWithExclude(address, "tags") @@ -62,7 +54,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.PublicIp, Scope: "global", }, - BlastPropagation: bp, }, }, Tags: ec2TagsToMap(address.Tags), @@ -76,7 +67,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.InstanceId, Scope: scope, }, - BlastPropagation: bp, }) } @@ -88,7 +78,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.CarrierIp, Scope: "global", }, - BlastPropagation: bp, }) } @@ -100,7 +89,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.CustomerOwnedIp, Scope: "global", }, - BlastPropagation: bp, }) } @@ -112,7 +100,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.NetworkInterfaceId, Scope: scope, }, - BlastPropagation: bp, }) } @@ -124,7 +111,6 @@ func addressOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ *ec2. Query: *address.PrivateIpAddress, Scope: "global", }, - BlastPropagation: bp, }) } diff --git a/go/sdp-go/items.go b/go/sdp-go/items.go index ffee8cf1..49f7f59b 100644 --- a/go/sdp-go/items.go +++ b/go/sdp-go/items.go @@ -20,12 +20,6 @@ import ( const WILDCARD = "*" -// IsEqual compares two BlastPropagation settings for equality by checking -// both the In and Out propagation directions. -func (bp *BlastPropagation) IsEqual(other *BlastPropagation) bool { - return bp.GetIn() == other.GetIn() && bp.GetOut() == other.GetOut() -} - // UniqueAttributeValue returns the value of whatever the Unique Attribute is // for this item. This will then be converted to a string and returned func (i *Item) UniqueAttributeValue() string { diff --git a/go/sdp-go/items.pb.go b/go/sdp-go/items.pb.go index 2f7bf864..f05460e2 100644 --- a/go/sdp-go/items.pb.go +++ b/go/sdp-go/items.pb.go @@ -278,27 +278,13 @@ func (QueryError_ErrorType) EnumDescriptor() ([]byte, []int) { return file_items_proto_rawDescGZIP(), []int{11, 0} } -// This message stores additional information on Edges (and edge-like constructs) to determine how configuration changes can impact -// the linked items. +// DEPRECATED: BlastPropagation was previously used to determine how configuration +// changes propagate over links. It has been replaced with an AI-driven approach +// for blast radius calculation and is no longer used. // -// Blast Propagation options: -// -// |-------|-------|---------------------- -// | in | out | result -// |-------|-------|---------------------- -// | false | false | no change in any item can affect the other -// | false | true | a change to this item can affect its linked items -// | | | example: a change to an EC2 instance can affect its DNS name (in the sense that other items depending on that DNS name will see the impact) -// | true | false | a change to linked items can affect this item -// | | | example: changing the KMS key used by a DynamoDB table can impact the table, but no change to the table can impact the key -// | true | true | changes on both sides of the link can affect the other -// | | | example: changes to both EC2 Instances and their volumes can affect the other side of the relation. +// Reserved to prevent field number reuse and maintain wire-format compatibility. type BlastPropagation struct { - state protoimpl.MessageState `protogen:"open.v1"` - // is true if changes on linked items can affect this item - In bool `protobuf:"varint,1,opt,name=in,proto3" json:"in,omitempty"` - // is true if changes on this item can affect linked items - Out bool `protobuf:"varint,2,opt,name=out,proto3" json:"out,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -333,29 +319,13 @@ func (*BlastPropagation) Descriptor() ([]byte, []int) { return file_items_proto_rawDescGZIP(), []int{0} } -func (x *BlastPropagation) GetIn() bool { - if x != nil { - return x.In - } - return false -} - -func (x *BlastPropagation) GetOut() bool { - if x != nil { - return x.Out - } - return false -} - // An annotated query to indicate potential linked items. type LinkedItemQuery struct { state protoimpl.MessageState `protogen:"open.v1"` // the query that would find linked items - Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LinkedItemQuery) Reset() { @@ -395,22 +365,13 @@ func (x *LinkedItemQuery) GetQuery() *Query { return nil } -func (x *LinkedItemQuery) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // An annotated reference to list linked items. type LinkedItem struct { state protoimpl.MessageState `protogen:"open.v1"` // the linked item - Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - // how configuration changes (i.e. the "blast") propagates over this link - BlastPropagation *BlastPropagation `protobuf:"bytes,2,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Item *Reference `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LinkedItem) Reset() { @@ -450,13 +411,6 @@ func (x *LinkedItem) GetItem() *Reference { return nil } -func (x *LinkedItem) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // This is the same as Item within the package with a couple of exceptions, no // real reason why this whole thing couldn't be modelled in protobuf though if // required. Just need to decide what if anything should remain private @@ -1477,12 +1431,11 @@ func (x *Reference) GetMethod() QueryMethod { // that will be unrolled by the gateway during query processing. Clients are // guaranteed that edges are only sent after the referenced items. type Edge struct { - state protoimpl.MessageState `protogen:"open.v1"` - From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` - To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` - BlastPropagation *BlastPropagation `protobuf:"bytes,3,opt,name=blastPropagation,proto3" json:"blastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + From *Reference `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To *Reference `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Edge) Reset() { @@ -1529,13 +1482,6 @@ func (x *Edge) GetTo() *Reference { return nil } -func (x *Edge) GetBlastPropagation() *BlastPropagation { - if x != nil { - return x.BlastPropagation - } - return nil -} - // Defines how this query should behave when finding new items type Query_RecursionBehaviour struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1543,11 +1489,9 @@ type Query_RecursionBehaviour struct { // To resolve linked items "infinitely" simply set this to a high number, with // the highest being 4,294,967,295. While this isn't truly *infinite*, chances // are that it is effectively the same, think six degrees of separation etc. - LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` - // set to true to only follow links that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,2,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + LinkDepth uint32 `protobuf:"varint,1,opt,name=linkDepth,proto3" json:"linkDepth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Query_RecursionBehaviour) Reset() { @@ -1587,29 +1531,18 @@ func (x *Query_RecursionBehaviour) GetLinkDepth() uint32 { return 0 } -func (x *Query_RecursionBehaviour) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - var File_items_proto protoreflect.FileDescriptor const file_items_proto_rawDesc = "" + "\n" + - "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"4\n" + - "\x10BlastPropagation\x12\x0e\n" + - "\x02in\x18\x01 \x01(\bR\x02in\x12\x10\n" + - "\x03out\x18\x02 \x01(\bR\x03out\"n\n" + + "\vitems.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fresponses.proto\"'\n" + + "\x10BlastPropagationJ\x04\b\x01\x10\x02J\x04\b\x02\x10\x03R\x02inR\x03out\"G\n" + "\x0fLinkedItemQuery\x12\x1c\n" + - "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05query\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"k\n" + + "\x05query\x18\x01 \x01(\v2\x06.QueryR\x05queryJ\x04\b\x02\x10\x03R\x10blastPropagation\"D\n" + "\n" + "LinkedItem\x12\x1e\n" + "\x04item\x18\x01 \x01(\v2\n" + - ".ReferenceR\x04item\x12=\n" + - "\x10blastPropagation\x18\x02 \x01(\v2\x11.BlastPropagationR\x10blastPropagation\"\xe3\x03\n" + + ".ReferenceR\x04itemJ\x04\b\x02\x10\x03R\x10blastPropagation\"\xe3\x03\n" + "\x04Item\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12(\n" + "\x0funiqueAttribute\x18\x02 \x01(\tR\x0funiqueAttribute\x12/\n" + @@ -1647,7 +1580,7 @@ const file_items_proto_rawDesc = "" + "\x10LogStreamDetails\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05scope\x18\x02 \x01(\tR\x05scope\x12\x14\n" + - "\x05query\x18\x03 \x01(\tR\x05query\"\xa0\x03\n" + + "\x05query\x18\x03 \x01(\tR\x05query\"\x82\x03\n" + "\x05Query\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12$\n" + "\x06method\x18\x02 \x01(\x0e2\f.QueryMethodR\x06method\x12\x14\n" + @@ -1656,10 +1589,9 @@ const file_items_proto_rawDesc = "" + "\x05scope\x18\x05 \x01(\tR\x05scope\x12 \n" + "\vignoreCache\x18\x06 \x01(\bR\vignoreCache\x12\x12\n" + "\x04UUID\x18\a \x01(\fR\x04UUID\x126\n" + - "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1ar\n" + + "\bdeadline\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\bdeadline\x1aT\n" + "\x12RecursionBehaviour\x12\x1c\n" + - "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepth\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x02 \x01(\bR\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + + "\tlinkDepth\x18\x01 \x01(\rR\tlinkDepthJ\x04\b\x02\x10\x03R\x1afollowOnlyBlastPropagationJ\x04\b\b\x10\t\"\xae\x01\n" + "\rQueryResponse\x12!\n" + "\anewItem\x18\x02 \x01(\v2\x05.ItemH\x00R\anewItem\x12'\n" + "\bresponse\x18\x03 \x01(\v2\t.ResponseH\x00R\bresponse\x12#\n" + @@ -1705,13 +1637,12 @@ const file_items_proto_rawDesc = "" + "\x05scope\x18\x03 \x01(\tR\x05scope\x12\x18\n" + "\aisQuery\x18\x04 \x01(\bR\aisQuery\x12\x14\n" + "\x05query\x18\x05 \x01(\tR\x05query\x12$\n" + - "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"\x81\x01\n" + + "\x06method\x18\x06 \x01(\x0e2\f.QueryMethodR\x06method\"Z\n" + "\x04Edge\x12\x1e\n" + "\x04from\x18\x01 \x01(\v2\n" + ".ReferenceR\x04from\x12\x1a\n" + "\x02to\x18\x02 \x01(\v2\n" + - ".ReferenceR\x02to\x12=\n" + - "\x10blastPropagation\x18\x03 \x01(\v2\x11.BlastPropagationR\x10blastPropagation*e\n" + + ".ReferenceR\x02toJ\x04\b\x03\x10\x04R\x10blastPropagation*e\n" + "\x06Health\x12\x12\n" + "\x0eHEALTH_UNKNOWN\x10\x00\x12\r\n" + "\tHEALTH_OK\x10\x01\x12\x12\n" + @@ -1768,42 +1699,39 @@ var file_items_proto_goTypes = []any{ } var file_items_proto_depIdxs = []int32{ 12, // 0: LinkedItemQuery.query:type_name -> Query - 4, // 1: LinkedItemQuery.blastPropagation:type_name -> BlastPropagation - 18, // 2: LinkedItem.item:type_name -> Reference - 4, // 3: LinkedItem.blastPropagation:type_name -> BlastPropagation - 8, // 4: Item.attributes:type_name -> ItemAttributes - 9, // 5: Item.metadata:type_name -> Metadata - 5, // 6: Item.linkedItemQueries:type_name -> LinkedItemQuery - 6, // 7: Item.linkedItems:type_name -> LinkedItem - 0, // 8: Item.health:type_name -> Health - 20, // 9: Item.tags:type_name -> Item.TagsEntry - 11, // 10: Item.logStreams:type_name -> LogStreamDetails - 22, // 11: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct - 12, // 12: Metadata.sourceQuery:type_name -> Query - 23, // 13: Metadata.timestamp:type_name -> google.protobuf.Timestamp - 24, // 14: Metadata.sourceDuration:type_name -> google.protobuf.Duration - 24, // 15: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration - 7, // 16: Items.items:type_name -> Item - 1, // 17: Query.method:type_name -> QueryMethod - 21, // 18: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour - 23, // 19: Query.deadline:type_name -> google.protobuf.Timestamp - 7, // 20: QueryResponse.newItem:type_name -> Item - 25, // 21: QueryResponse.response:type_name -> Response - 15, // 22: QueryResponse.error:type_name -> QueryError - 19, // 23: QueryResponse.edge:type_name -> Edge - 2, // 24: QueryStatus.status:type_name -> QueryStatus.Status - 3, // 25: QueryError.errorType:type_name -> QueryError.ErrorType - 18, // 26: Expand.item:type_name -> Reference - 23, // 27: Expand.deadline:type_name -> google.protobuf.Timestamp - 1, // 28: Reference.method:type_name -> QueryMethod - 18, // 29: Edge.from:type_name -> Reference - 18, // 30: Edge.to:type_name -> Reference - 4, // 31: Edge.blastPropagation:type_name -> BlastPropagation - 32, // [32:32] is the sub-list for method output_type - 32, // [32:32] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 18, // 1: LinkedItem.item:type_name -> Reference + 8, // 2: Item.attributes:type_name -> ItemAttributes + 9, // 3: Item.metadata:type_name -> Metadata + 5, // 4: Item.linkedItemQueries:type_name -> LinkedItemQuery + 6, // 5: Item.linkedItems:type_name -> LinkedItem + 0, // 6: Item.health:type_name -> Health + 20, // 7: Item.tags:type_name -> Item.TagsEntry + 11, // 8: Item.logStreams:type_name -> LogStreamDetails + 22, // 9: ItemAttributes.attrStruct:type_name -> google.protobuf.Struct + 12, // 10: Metadata.sourceQuery:type_name -> Query + 23, // 11: Metadata.timestamp:type_name -> google.protobuf.Timestamp + 24, // 12: Metadata.sourceDuration:type_name -> google.protobuf.Duration + 24, // 13: Metadata.sourceDurationPerItem:type_name -> google.protobuf.Duration + 7, // 14: Items.items:type_name -> Item + 1, // 15: Query.method:type_name -> QueryMethod + 21, // 16: Query.recursionBehaviour:type_name -> Query.RecursionBehaviour + 23, // 17: Query.deadline:type_name -> google.protobuf.Timestamp + 7, // 18: QueryResponse.newItem:type_name -> Item + 25, // 19: QueryResponse.response:type_name -> Response + 15, // 20: QueryResponse.error:type_name -> QueryError + 19, // 21: QueryResponse.edge:type_name -> Edge + 2, // 22: QueryStatus.status:type_name -> QueryStatus.Status + 3, // 23: QueryError.errorType:type_name -> QueryError.ErrorType + 18, // 24: Expand.item:type_name -> Reference + 23, // 25: Expand.deadline:type_name -> google.protobuf.Timestamp + 1, // 26: Reference.method:type_name -> QueryMethod + 18, // 27: Edge.from:type_name -> Reference + 18, // 28: Edge.to:type_name -> Reference + 29, // [29:29] is the sub-list for method output_type + 29, // [29:29] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name } func init() { file_items_proto_init() } diff --git a/go/sdp-go/revlink.pb.go b/go/sdp-go/revlink.pb.go index a1d2fa2b..6ede8fd4 100644 --- a/go/sdp-go/revlink.pb.go +++ b/go/sdp-go/revlink.pb.go @@ -27,11 +27,9 @@ type GetReverseEdgesRequest struct { // The account that the item belongs to Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` // The item that you would like to find reverse edges for - Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` - // set to true to only return edges that propagate configuration change impact - FollowOnlyBlastPropagation bool `protobuf:"varint,3,opt,name=followOnlyBlastPropagation,proto3" json:"followOnlyBlastPropagation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Item *Reference `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetReverseEdgesRequest) Reset() { @@ -78,13 +76,6 @@ func (x *GetReverseEdgesRequest) GetItem() *Reference { return nil } -func (x *GetReverseEdgesRequest) GetFollowOnlyBlastPropagation() bool { - if x != nil { - return x.FollowOnlyBlastPropagation - } - return false -} - type GetReverseEdgesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The edges to the requested item @@ -351,12 +342,11 @@ var File_revlink_proto protoreflect.FileDescriptor const file_revlink_proto_rawDesc = "" + "\n" + - "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x92\x01\n" + + "\rrevlink.proto\x12\arevlink\x1a\vitems.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"t\n" + "\x16GetReverseEdgesRequest\x12\x18\n" + "\aaccount\x18\x01 \x01(\tR\aaccount\x12\x1e\n" + "\x04item\x18\x02 \x01(\v2\n" + - ".ReferenceR\x04item\x12>\n" + - "\x1afollowOnlyBlastPropagation\x18\x03 \x01(\bR\x1afollowOnlyBlastPropagation\"6\n" + + ".ReferenceR\x04itemJ\x04\b\x03\x10\x04R\x1afollowOnlyBlastPropagation\"6\n" + "\x17GetReverseEdgesResponse\x12\x1b\n" + "\x05edges\x18\x01 \x03(\v2\x05.EdgeR\x05edges\"\x8f\x01\n" + "\x1cIngestGatewayResponseRequest\x12\x18\n" + diff --git a/sources/azure/integration-tests/authorization-role-assignment_test.go b/sources/azure/integration-tests/authorization-role-assignment_test.go index 60ea32d6..b4548094 100644 --- a/sources/azure/integration-tests/authorization-role-assignment_test.go +++ b/sources/azure/integration-tests/authorization-role-assignment_test.go @@ -272,12 +272,6 @@ func TestAuthorizationRoleAssignmentIntegration(t *testing.T) { if linkedQuery.GetQuery().GetScope() != subscriptionID { t.Errorf("Expected role definition link scope to be subscription ID %s, got %s", subscriptionID, linkedQuery.GetQuery().GetScope()) } - if linkedQuery.GetBlastPropagation().GetIn() != true { - t.Error("Expected role definition link BlastPropagation.In to be true") - } - if linkedQuery.GetBlastPropagation().GetOut() != false { - t.Error("Expected role definition link BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index 7e62209d..4aea0a93 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -246,30 +246,6 @@ func TestBatchAccountIntegration(t *testing.T) { t.Errorf("Expected linked query method to be SEARCH for %s, got %s", linkedType, queryMethod) } } - - // Verify blast propagation - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - bp := liq.GetBlastPropagation() - if linkedType == azureshared.StorageAccount.String() { - // Storage account: In=true, Out=false (batch depends on storage) - if bp.GetIn() != true { - t.Errorf("Expected blast propagation In=true for storage account, got false") - } - if bp.GetOut() != false { - t.Errorf("Expected blast propagation Out=false for storage account, got true") - } - } else { - // Child resources: In=true, Out=true (tightly coupled) - if bp.GetIn() != true { - t.Errorf("Expected blast propagation In=true for %s, got false", linkedType) - } - if bp.GetOut() != true { - t.Errorf("Expected blast propagation Out=true for %s, got false", linkedType) - } - } - } } } diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index 5049a01c..ffc52e92 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -272,26 +272,12 @@ func TestComputeAvailabilitySetIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected VM link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.ComputeProximityPlacementGroup.String(): // PPG may or may not be present depending on availability set setup // Verify PPG link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected PPG link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected PPG blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected PPG blast propagation Out=false, got true") - } } } diff --git a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go index 7b4e76c7..9509e715 100644 --- a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go +++ b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go @@ -219,17 +219,6 @@ func TestComputeCapacityReservationGroupIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) - } else { - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) - } } }) }) diff --git a/sources/azure/integration-tests/compute-dedicated-host-group_test.go b/sources/azure/integration-tests/compute-dedicated-host-group_test.go index adb0edfa..89e1d475 100644 --- a/sources/azure/integration-tests/compute-dedicated-host-group_test.go +++ b/sources/azure/integration-tests/compute-dedicated-host-group_test.go @@ -219,17 +219,6 @@ func TestComputeDedicatedHostGroupIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) - } else { - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) - } } }) }) diff --git a/sources/azure/integration-tests/compute-disk-access_test.go b/sources/azure/integration-tests/compute-disk-access_test.go index 3f245afd..bea69ff7 100644 --- a/sources/azure/integration-tests/compute-disk-access_test.go +++ b/sources/azure/integration-tests/compute-disk-access_test.go @@ -229,17 +229,6 @@ func TestComputeDiskAccessIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s (BlastPropagation nil)", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) - } else { - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) - } } }) }) 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 5b104edb..4278e7b6 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -281,16 +281,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected Key Vault link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("Key Vault linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault BlastPropagation.Out to be false") - } - } case azureshared.KeyVaultKey.String(): hasKeyVaultKeyLink = true if query.GetMethod() != sdp.QueryMethod_GET { @@ -303,16 +293,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected Key Vault Key link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("Key Vault Key linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault Key BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault Key BlastPropagation.Out to be false") - } - } case azureshared.ManagedIdentityUserAssignedIdentity.String(): hasUserAssignedIdentityLink = true if query.GetMethod() != sdp.QueryMethod_GET { @@ -324,16 +304,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != scope { t.Errorf("Expected User Assigned Identity link scope %s, got %s", scope, query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("User Assigned Identity linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected User Assigned Identity BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected User Assigned Identity BlastPropagation.Out to be false") - } - } case "dns": hasDNSLink = true if query.GetMethod() != sdp.QueryMethod_SEARCH { @@ -346,16 +316,6 @@ func TestComputeDiskEncryptionSetIntegration(t *testing.T) { if query.GetScope() != "global" { t.Errorf("Expected DNS link scope global, got %s", query.GetScope()) } - if liq.GetBlastPropagation() == nil { - t.Error("DNS linked item query has nil BlastPropagation") - } else { - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS BlastPropagation.Out to be true") - } - } default: t.Errorf("Unexpected linked item type: %s", query.GetType()) } diff --git a/sources/azure/integration-tests/compute-disk_test.go b/sources/azure/integration-tests/compute-disk_test.go index 6c7cbb5b..bd47dad0 100644 --- a/sources/azure/integration-tests/compute-disk_test.go +++ b/sources/azure/integration-tests/compute-disk_test.go @@ -242,19 +242,8 @@ func TestComputeDiskIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } }) }) diff --git a/sources/azure/integration-tests/compute-gallery-application-version_test.go b/sources/azure/integration-tests/compute-gallery-application-version_test.go index eeb4b84b..28959583 100644 --- a/sources/azure/integration-tests/compute-gallery-application-version_test.go +++ b/sources/azure/integration-tests/compute-gallery-application-version_test.go @@ -244,15 +244,6 @@ func TestComputeGalleryApplicationVersionIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) - } } }) }) diff --git a/sources/azure/integration-tests/compute-image_test.go b/sources/azure/integration-tests/compute-image_test.go index 71af70f1..78ceef2c 100644 --- a/sources/azure/integration-tests/compute-image_test.go +++ b/sources/azure/integration-tests/compute-image_test.go @@ -274,32 +274,13 @@ func TestComputeImageIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - continue - } - - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - // Check if this is a link to the source disk if query.GetType() == azureshared.ComputeDisk.String() && query.GetQuery() == integrationTestImageDiskName { foundDiskLink = true - // Verify blast propagation for disk link - if !bp.GetIn() { - t.Error("Expected In=true for disk link (if source disk is deleted/modified, image becomes invalid)") - } - if bp.GetOut() { - t.Error("Expected Out=false for disk link (if image is deleted, source disk remains)") - } } - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify we found the expected disk link diff --git a/sources/azure/integration-tests/compute-proximity-placement-group_test.go b/sources/azure/integration-tests/compute-proximity-placement-group_test.go index 81606299..7280cf75 100644 --- a/sources/azure/integration-tests/compute-proximity-placement-group_test.go +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -202,15 +202,6 @@ func TestComputeProximityPlacementGroupIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // PPG links use In: true, Out: true per adapter - if !bp.GetIn() || !bp.GetOut() { - t.Errorf("Expected BlastPropagation In=true Out=true for PPG links, got In=%v Out=%v", bp.GetIn(), bp.GetOut()) - } - } } }) diff --git a/sources/azure/integration-tests/compute-snapshot_test.go b/sources/azure/integration-tests/compute-snapshot_test.go index 3ad4d733..c6388e86 100644 --- a/sources/azure/integration-tests/compute-snapshot_test.go +++ b/sources/azure/integration-tests/compute-snapshot_test.go @@ -271,11 +271,6 @@ func TestComputeSnapshotIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } - if query.GetType() == azureshared.ComputeDisk.String() { hasDiskLink = true if query.GetMethod() != sdp.QueryMethod_GET { @@ -284,14 +279,6 @@ func TestComputeSnapshotIntegration(t *testing.T) { if query.GetQuery() != integrationTestDiskForSnapName { t.Errorf("Expected disk link query to be %s, got %s", integrationTestDiskForSnapName, query.GetQuery()) } - if bp != nil { - if bp.GetIn() != true { - t.Error("Expected disk blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected disk blast propagation Out=false, got true") - } - } } log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", @@ -344,7 +331,7 @@ func createSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, res Location: ptr.To(location), Properties: &armcompute.SnapshotProperties{ CreationData: &armcompute.CreationData{ - CreateOption: ptr.To(armcompute.DiskCreateOptionCopy), + CreateOption: ptr.To(armcompute.DiskCreateOptionCopy), SourceResourceID: ptr.To(sourceDiskID), }, }, 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 bc4809cd..1c89a6df 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -269,49 +269,14 @@ func TestComputeVirtualMachineExtensionIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestExtensionVMName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestExtensionVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.KeyVaultVault.String(): // Key Vault links may be present if ProtectedSettingsFromKeyVault is set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Key Vault blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Key Vault blast propagation Out=false, got true") - } case stdlib.NetworkHTTP.String(): // HTTP links may be present if settings contain URLs - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected HTTP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected HTTP blast propagation Out=true, got false") - } case stdlib.NetworkDNS.String(): // DNS links may be present if settings contain DNS names - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS blast propagation Out=true, got false") - } case stdlib.NetworkIP.String(): // IP links may be present if settings contain IP addresses - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected IP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected IP blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go index edbed38d..e887ac32 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go @@ -269,49 +269,14 @@ func TestComputeVirtualMachineRunCommandIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestRunCommandVMName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestRunCommandVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected VM blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected VM blast propagation Out=false, got true") - } case azureshared.StorageAccount.String(): // Storage account links may be present if outputBlobUri, errorBlobUri, or scriptUri are set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Storage Account blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Storage Account blast propagation Out=false, got true") - } case azureshared.StorageBlobContainer.String(): // Blob container links may be present if outputBlobUri, errorBlobUri, or scriptUri are set - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected Blob Container blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected Blob Container blast propagation Out=false, got true") - } case stdlib.NetworkHTTP.String(): // HTTP links may be present if scriptUri is HTTP/HTTPS - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected HTTP blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected HTTP blast propagation Out=true, got false") - } case stdlib.NetworkDNS.String(): // DNS links may be present if scriptUri contains a DNS name - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected DNS blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected DNS blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go index b1c309e8..f71515fc 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go @@ -229,13 +229,6 @@ func TestComputeVirtualMachineScaleSetIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected subnet link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected subnet blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected subnet blast propagation Out=false, got true") - } case azureshared.ComputeVirtualMachine.String(): hasVMLink = true // Verify VM link properties (VM instances are linked via SEARCH) @@ -245,26 +238,12 @@ func TestComputeVirtualMachineScaleSetIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVMSSName { t.Errorf("Expected VM link query to be %s, got %s", integrationTestVMSSName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected VM blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected VM blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachineExtension.String(): // Extensions may or may not be present depending on VMSS setup // Verify extension link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected extension link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected extension blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected extension blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index d1707ff9..2ef716e8 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -239,26 +239,12 @@ func TestComputeVirtualMachineIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVMName { t.Errorf("Expected run command link query to be %s, got %s", integrationTestVMName, liq.GetQuery().GetQuery()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected run command blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected run command blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachineExtension.String(): // Extensions may or may not be present depending on VM setup // Verify extension link properties if present if liq.GetQuery().GetMethod() != sdp.QueryMethod_GET { t.Errorf("Expected extension link method to be GET, got %s", liq.GetQuery().GetMethod()) } - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected extension blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected extension blast propagation Out=true, got false") - } } } diff --git a/sources/azure/integration-tests/dbforpostgresql-database_test.go b/sources/azure/integration-tests/dbforpostgresql-database_test.go index 5cb4065e..e2efc7ed 100644 --- a/sources/azure/integration-tests/dbforpostgresql-database_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-database_test.go @@ -236,18 +236,6 @@ func TestDBforPostgreSQLDatabaseIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for PostgreSQL server link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for PostgreSQL server link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for PostgreSQL server link") - } - } break } } diff --git a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go index fd7ff62c..d0a39a7f 100644 --- a/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-flexible-server_test.go @@ -226,12 +226,6 @@ func TestDBforPostgreSQLFlexibleServerIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } } // Check for conditional links @@ -253,63 +247,10 @@ func TestDBforPostgreSQLFlexibleServerIntegration(t *testing.T) { } } - log.Printf("Verified %d linked item queries for PostgreSQL Flexible Server %s (hasSubnet: %v, hasVNet: %v, hasDNS: %v)", - len(linkedQueries), postgreSQLServerName, hasSubnetLink, hasVirtualNetworkLink, hasDNSLink) - }) - - t.Run("VerifyChildResourceBlastPropagation", func(t *testing.T) { - ctx := t.Context() - - log.Printf("Verifying blast propagation for child resources of PostgreSQL Flexible Server %s", postgreSQLServerName) - - pgServerWrapper := manual.NewDBforPostgreSQLFlexibleServer( - clients.NewPostgreSQLFlexibleServersClient(postgreSQLServerClient), - []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, - ) - scope := pgServerWrapper.Scopes()[0] - - pgServerAdapter := sources.WrapperToAdapter(pgServerWrapper, sdpcache.NewNoOpCache()) - sdpItem, qErr := pgServerAdapter.Get(ctx, scope, postgreSQLServerName, true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - linkedQueries := sdpItem.GetLinkedItemQueries() - - // Verify specific blast propagation patterns - blastPropagationTests := map[string]struct { - in bool - out bool - }{ - // Child resources that depend on server (In: true, Out: false) - azureshared.DBforPostgreSQLDatabase.String(): {in: true, out: false}, - // Child resources that affect server connectivity/configuration (In: true, Out: true) - azureshared.DBforPostgreSQLFlexibleServerFirewallRule.String(): {in: true, out: true}, - azureshared.DBforPostgreSQLFlexibleServerConfiguration.String(): {in: true, out: true}, - } - - for _, liq := range linkedQueries { - linkedType := liq.GetQuery().GetType() - if expected, ok := blastPropagationTests[linkedType]; ok { - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - continue - } - - if bp.GetIn() != expected.in { - t.Errorf("Expected BlastPropagation.In=%v for %s, got %v", expected.in, linkedType, bp.GetIn()) - } - - if bp.GetOut() != expected.out { - t.Errorf("Expected BlastPropagation.Out=%v for %s, got %v", expected.out, linkedType, bp.GetOut()) - } - } - } - - log.Printf("Verified blast propagation for all child resources") - }) + log.Printf("Verified %d linked item queries for PostgreSQL Flexible Server %s (hasSubnet: %v, hasVNet: %v, hasDNS: %v)", + len(linkedQueries), postgreSQLServerName, hasSubnetLink, hasVirtualNetworkLink, hasDNSLink) }) +}) t.Run("Teardown", func(t *testing.T) { ctx := t.Context() diff --git a/sources/azure/integration-tests/keyvault-managed-hsm_test.go b/sources/azure/integration-tests/keyvault-managed-hsm_test.go index f9a0e8b0..beeb2e54 100644 --- a/sources/azure/integration-tests/keyvault-managed-hsm_test.go +++ b/sources/azure/integration-tests/keyvault-managed-hsm_test.go @@ -286,16 +286,6 @@ func TestKeyVaultManagedHSMIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } } log.Printf("Verified %d linked item queries for Managed HSM %s", len(linkedQueries), integrationTestManagedHSMName) diff --git a/sources/azure/integration-tests/keyvault-secret_test.go b/sources/azure/integration-tests/keyvault-secret_test.go index c74ad5a7..bfca030b 100644 --- a/sources/azure/integration-tests/keyvault-secret_test.go +++ b/sources/azure/integration-tests/keyvault-secret_test.go @@ -275,12 +275,6 @@ func TestKeyVaultSecretIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != vaultName { t.Errorf("Expected linked query to Key Vault %s, got %s", vaultName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go index 70374b50..a622263a 100644 --- a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go @@ -212,13 +212,6 @@ func TestManagedIdentityUserAssignedIdentityIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected federated credential link scope to be %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation (In: true, Out: true) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected federated credential blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected federated credential blast propagation Out=true, got false") - } default: t.Errorf("Unexpected linked item type: %s", liq.GetQuery().GetType()) } diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index fe729639..7525eee2 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -318,16 +318,6 @@ func TestNetworkApplicationGatewayIntegration(t *testing.T) { if query.GetScope() == "" { t.Error("Linked item query has empty Scope") } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } } // Verify critical linked types were found diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index aa11334f..b6cd87ba 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -280,27 +280,6 @@ func TestNetworkLoadBalancerIntegration(t *testing.T) { linkedType := liq.GetQuery().GetType() if _, exists := expectedLinkedTypes[linkedType]; exists { expectedLinkedTypes[linkedType] = true - - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - switch linkedType { - case azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation Out=true, got false") - } - case azureshared.NetworkPublicIPAddress.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected PublicIPAddress blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected PublicIPAddress blast propagation Out=false, got true") - } - } - } } } @@ -336,27 +315,6 @@ func TestNetworkLoadBalancerIntegration(t *testing.T) { linkedType := liq.GetQuery().GetType() if _, exists := expectedLinkedTypes[linkedType]; exists { expectedLinkedTypes[linkedType] = true - - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - switch linkedType { - case azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected FrontendIPConfiguration blast propagation Out=true, got false") - } - case azureshared.NetworkSubnet.String(): - if liq.GetBlastPropagation().GetIn() != true { - t.Errorf("Expected Subnet blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Errorf("Expected Subnet blast propagation Out=false, got true") - } - } - } } } diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index ca4c5d2f..a496409b 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -209,31 +209,10 @@ func TestNetworkNetworkInterfaceIntegration(t *testing.T) { switch liq.GetQuery().GetType() { case azureshared.NetworkNetworkInterfaceIPConfiguration.String(): hasIPConfigLink = true - // Verify blast propagation (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected IP config blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected IP config blast propagation Out=true, got false") - } case azureshared.ComputeVirtualMachine.String(): // VM link may or may not be present depending on whether NIC is attached - // Verify blast propagation if present (In: false, Out: true) - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected VM blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected VM blast propagation Out=true, got false") - } case azureshared.NetworkNetworkSecurityGroup.String(): // NSG link may or may not be present - // Verify blast propagation if present (In: true, Out: false) - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected NSG blast propagation In=true, got false") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected NSG blast propagation Out=false, got true") - } } } diff --git a/sources/azure/integration-tests/network-network-security-group_test.go b/sources/azure/integration-tests/network-network-security-group_test.go index 17039a7c..4618167b 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -242,19 +242,8 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify that default security rules are linked (they should always exist) @@ -262,14 +251,6 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkDefaultSecurityRule.String() { hasDefaultSecurityRuleLink = true - // Verify blast propagation for default security rules - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected default security rule blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected default security rule blast propagation Out=false, got true") - } break } } @@ -282,14 +263,6 @@ func TestNetworkNetworkSecurityGroupIntegration(t *testing.T) { for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkSecurityRule.String() { hasSecurityRuleLink = true - // Verify blast propagation for security rules - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected security rule blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected security rule blast propagation Out=false, got true") - } // Verify the query contains the NSG name and rule name query := liq.GetQuery().GetQuery() if query == "" { diff --git a/sources/azure/integration-tests/network-public-ip-address_test.go b/sources/azure/integration-tests/network-public-ip-address_test.go index 2cda00ee..9fea6d49 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -250,18 +250,6 @@ func TestNetworkPublicIPAddressIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for network interface link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for network interface link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for network interface link") - } - } break } } diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index 19634e65..1de639d7 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -259,35 +259,15 @@ func TestNetworkRouteTableIntegration(t *testing.T) { t.Error("Linked item query has empty Scope") } - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Linked item query has nil BlastPropagation") - } else { - // Blast propagation should have In and Out set (even if false) - _ = bp.GetIn() - _ = bp.GetOut() - } - - log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s, In=%v, Out=%v", - query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope(), - bp.GetIn(), bp.GetOut()) + log.Printf("Verified linked item query: Type=%s, Method=%s, Query=%s, Scope=%s", + query.GetType(), query.GetMethod(), query.GetQuery(), query.GetScope()) } // Verify that routes are linked (we created one named integrationTestRouteName) var hasRouteLink bool for _, liq := range linkedQueries { if liq.GetQuery().GetType() == azureshared.NetworkRoute.String() { - hasRouteLink = true - // Verify blast propagation for routes - bp := liq.GetBlastPropagation() - if bp.GetIn() != true { - t.Error("Expected route blast propagation In=true, got false") - } - if bp.GetOut() != false { - t.Error("Expected route blast propagation Out=false, got true") - } - // Verify the query contains the route table name and route name + hasRouteLink = true // Verify the query contains the route table name and route name query := liq.GetQuery().GetQuery() if query == "" { t.Error("Expected route query to be non-empty") diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index fe12269c..9a831b64 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -185,12 +185,6 @@ func TestNetworkVirtualNetworkIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVNetName { t.Errorf("Expected subnet link query to be %s, got %s", integrationTestVNetName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected subnet blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected subnet blast propagation Out=true, got false") - } break } } @@ -209,12 +203,6 @@ func TestNetworkVirtualNetworkIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != integrationTestVNetName { t.Errorf("Expected peering link query to be %s, got %s", integrationTestVNetName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != false { - t.Error("Expected peering blast propagation In=false, got true") - } - if liq.GetBlastPropagation().GetOut() != true { - t.Error("Expected peering blast propagation Out=true, got false") - } break } } diff --git a/sources/azure/integration-tests/network-zone_test.go b/sources/azure/integration-tests/network-zone_test.go index 70b87a76..cd2e363d 100644 --- a/sources/azure/integration-tests/network-zone_test.go +++ b/sources/azure/integration-tests/network-zone_test.go @@ -272,19 +272,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if linkedScope != scope { t.Errorf("Expected linked query scope %s, got %s", scope, linkedScope) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for %s, got false", linkedType) - } - if bp.GetOut() != true { - t.Errorf("Expected BlastPropagation.Out=true for %s, got false", linkedType) - } - } } // Verify DNS name server links (standard library) @@ -297,19 +284,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if linkedScope != "global" { t.Errorf("Expected linked query scope 'global' for DNS name server, got %s", linkedScope) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for DNS name server") - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for DNS name server, got false") - } - if bp.GetOut() != true { - t.Errorf("Expected BlastPropagation.Out=true for DNS name server, got false") - } - } } // Verify Virtual Network links (if present) @@ -317,19 +291,6 @@ func TestNetworkZoneIntegration(t *testing.T) { if method != sdp.QueryMethod_GET { t.Errorf("Expected linked query method GET for Virtual Network, got %s", method) } - - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for Virtual Network") - } else { - if bp.GetIn() != true { - t.Errorf("Expected BlastPropagation.In=true for Virtual Network, got false") - } - if bp.GetOut() != false { - t.Errorf("Expected BlastPropagation.Out=false for Virtual Network, got true") - } - } } } diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index 289ff425..be46d5a0 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -236,18 +236,6 @@ func TestSQLDatabaseIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - // Verify blast propagation - bp := liq.GetBlastPropagation() - if bp == nil { - t.Error("Expected BlastPropagation to be set for SQL server link") - } else { - if !bp.GetIn() { - t.Error("Expected BlastPropagation.In to be true for SQL server link") - } - if bp.GetOut() { - t.Error("Expected BlastPropagation.Out to be false for SQL server link") - } - } break } } diff --git a/sources/azure/integration-tests/sql-server_test.go b/sources/azure/integration-tests/sql-server_test.go index 48a965de..f670a2f1 100644 --- a/sources/azure/integration-tests/sql-server_test.go +++ b/sources/azure/integration-tests/sql-server_test.go @@ -239,12 +239,6 @@ func TestSQLServerIntegration(t *testing.T) { if liq.GetQuery().GetScope() != scope { t.Errorf("Expected linked query scope %s, got %s", scope, liq.GetQuery().GetScope()) } - - // Verify blast propagation is set - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - } } } @@ -255,85 +249,9 @@ func TestSQLServerIntegration(t *testing.T) { } } - log.Printf("Verified %d linked item queries for SQL server %s", len(linkedQueries), sqlServerName) - }) - - t.Run("VerifyChildResourceBlastPropagation", func(t *testing.T) { - ctx := t.Context() - - log.Printf("Verifying blast propagation for child resources of SQL server %s", sqlServerName) - - sqlServerWrapper := manual.NewSqlServer( - clients.NewSqlServersClient(sqlServerClient), - []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, integrationTestResourceGroup)}, - ) - scope := sqlServerWrapper.Scopes()[0] - - sqlServerAdapter := sources.WrapperToAdapter(sqlServerWrapper, sdpcache.NewNoOpCache()) - sdpItem, qErr := sqlServerAdapter.Get(ctx, scope, sqlServerName, true) - if qErr != nil { - t.Fatalf("Expected no error, got: %v", qErr) - } - - linkedQueries := sdpItem.GetLinkedItemQueries() - - // Verify specific blast propagation patterns - blastPropagationTests := map[string]struct { - in bool - out bool - }{ - // Child resources that depend on server (In: true, Out: false) - azureshared.SQLDatabase.String(): {in: true, out: false}, - azureshared.SQLElasticPool.String(): {in: true, out: false}, - azureshared.SQLServerSyncGroup.String(): {in: true, out: false}, - azureshared.SQLServerSyncAgent.String(): {in: true, out: false}, - azureshared.SQLServerUsage.String(): {in: true, out: false}, - azureshared.SQLServerOperation.String(): {in: true, out: false}, - azureshared.SQLServerAdvisor.String(): {in: true, out: false}, - azureshared.SQLServerPrivateLinkResource.String(): {in: true, out: false}, - // Child resources that affect server connectivity/security (In: true, Out: true) - azureshared.SQLServerFirewallRule.String(): {in: true, out: true}, - azureshared.SQLServerVirtualNetworkRule.String(): {in: true, out: true}, - azureshared.SQLServerKey.String(): {in: true, out: true}, - azureshared.SQLServerFailoverGroup.String(): {in: true, out: true}, - azureshared.SQLServerAdministrator.String(): {in: true, out: true}, - azureshared.SQLServerPrivateEndpointConnection.String(): {in: true, out: true}, - azureshared.SQLServerAuditingSetting.String(): {in: true, out: true}, - azureshared.SQLServerSecurityAlertPolicy.String(): {in: true, out: true}, - azureshared.SQLServerVulnerabilityAssessment.String(): {in: true, out: true}, - azureshared.SQLServerEncryptionProtector.String(): {in: true, out: true}, - azureshared.SQLServerBlobAuditingPolicy.String(): {in: true, out: true}, - azureshared.SQLServerAutomaticTuning.String(): {in: true, out: true}, - azureshared.SQLServerAdvancedThreatProtectionSetting.String(): {in: true, out: true}, - azureshared.SQLServerDnsAlias.String(): {in: true, out: true}, - azureshared.SQLServerBackupLongTermRetentionPolicy.String(): {in: true, out: true}, - azureshared.SQLServerDevOpsAuditSetting.String(): {in: true, out: true}, - azureshared.SQLServerTrustGroup.String(): {in: true, out: true}, - azureshared.SQLServerOutboundFirewallRule.String(): {in: true, out: true}, - } - - for _, liq := range linkedQueries { - linkedType := liq.GetQuery().GetType() - if expected, ok := blastPropagationTests[linkedType]; ok { - bp := liq.GetBlastPropagation() - if bp == nil { - t.Errorf("Expected BlastPropagation to be set for %s", linkedType) - continue - } - - if bp.GetIn() != expected.in { - t.Errorf("Expected BlastPropagation.In=%v for %s, got %v", expected.in, linkedType, bp.GetIn()) - } - - if bp.GetOut() != expected.out { - t.Errorf("Expected BlastPropagation.Out=%v for %s, got %v", expected.out, linkedType, bp.GetOut()) - } - } - } - - log.Printf("Verified blast propagation for all child resources") - }) + log.Printf("Verified %d linked item queries for SQL server %s", len(linkedQueries), sqlServerName) }) +}) t.Run("Teardown", func(t *testing.T) { ctx := t.Context() diff --git a/sources/azure/integration-tests/storage-account_test.go b/sources/azure/integration-tests/storage-account_test.go index d9501f1c..be3e69c9 100644 --- a/sources/azure/integration-tests/storage-account_test.go +++ b/sources/azure/integration-tests/storage-account_test.go @@ -200,18 +200,6 @@ func TestStorageAccountIntegration(t *testing.T) { if liq.GetQuery().GetMethod() != sdp.QueryMethod_SEARCH { t.Errorf("Expected linked query method to be SEARCH, got %s", liq.GetQuery().GetMethod()) } - - // Verify blast propagation (parent to child: In=false, Out=true) - if liq.GetBlastPropagation() == nil { - t.Errorf("Expected blast propagation to be set for linked type %s", linkedType) - } else { - if liq.GetBlastPropagation().GetIn() != false { - t.Errorf("Expected blast propagation In=false for linked type %s, got true", linkedType) - } - if liq.GetBlastPropagation().GetOut() != true { - t.Errorf("Expected blast propagation Out=true for linked type %s, got false", linkedType) - } - } } } diff --git a/sources/azure/integration-tests/storage-queues_test.go b/sources/azure/integration-tests/storage-queues_test.go index 6a97d2a6..24dc4d14 100644 --- a/sources/azure/integration-tests/storage-queues_test.go +++ b/sources/azure/integration-tests/storage-queues_test.go @@ -242,12 +242,6 @@ func TestStorageQueuesIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query to storage account %s, got %s", storageAccountName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/azure/integration-tests/storage-table_test.go b/sources/azure/integration-tests/storage-table_test.go index 52a6b3a1..2a0bae7e 100644 --- a/sources/azure/integration-tests/storage-table_test.go +++ b/sources/azure/integration-tests/storage-table_test.go @@ -242,12 +242,6 @@ func TestStorageTableIntegration(t *testing.T) { if liq.GetQuery().GetQuery() != storageAccountName { t.Errorf("Expected linked query to storage account %s, got %s", storageAccountName, liq.GetQuery().GetQuery()) } - if liq.GetBlastPropagation().GetIn() != true { - t.Error("Expected BlastPropagation.In to be true") - } - if liq.GetBlastPropagation().GetOut() != false { - t.Error("Expected BlastPropagation.Out to be false") - } break } } diff --git a/sources/snapshot/adapters/index.go b/sources/snapshot/adapters/index.go index e05ec840..fcbd92d0 100644 --- a/sources/snapshot/adapters/index.go +++ b/sources/snapshot/adapters/index.go @@ -111,8 +111,7 @@ func (idx *SnapshotIndex) hydrateLinkedItems() { } item.LinkedItems = append(item.LinkedItems, &sdp.LinkedItem{ - Item: to, - BlastPropagation: edge.GetBlastPropagation(), + Item: to, }) existingLinks[fromKey][toKey] = true } From dd2b4756f8012fd0525cdc80fe499f58e73696c7 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:59:21 +0000 Subject: [PATCH 49/51] Implement Azure Compute Galleries Client and Adapter (#3941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image --- > [!NOTE] > **Medium Risk** > Touches core Azure discovery execution paths by adding streaming query methods and new linked-item edges, which could affect discovery completeness/performance and error propagation across several adapters. > > **Overview** > Adds first-class discovery for Azure **Compute Galleries** by introducing a `GalleriesClient`, a new `ComputeGallery` listable adapter (Get/List/ListStream), unit tests, and wiring it into `manual/adapters.go`. > > Extends multiple existing Azure adapters to support streaming discovery via new `SearchStream`/`ListStream` methods (including gallery images/application versions, shared gallery images, VM extensions/run commands, SQL databases, PostgreSQL databases, and several storage child resources), and updates linked-item discovery guidance plus runtime behavior by adding **mandatory parent→child SEARCH `LinkedItemQuery` links** (e.g., Key Vault vault → secrets; gallery → gallery images). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 49b55cda6eb063f2a218885a808e6cceccd65c3f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f1b1fef0c94a59a0ae8d9a72af9d96a4eb77e156 --- sources/azure/clients/galleries-client.go | 35 +++ sources/azure/manual/adapters.go | 10 + .../compute-gallery-application-version.go | 46 ++- sources/azure/manual/compute-gallery-image.go | 42 +++ sources/azure/manual/compute-gallery.go | 211 +++++++++++++ sources/azure/manual/compute-gallery_test.go | 287 ++++++++++++++++++ .../manual/compute-shared-gallery-image.go | 42 ++- .../compute-virtual-machine-extension.go | 37 +++ .../compute-virtual-machine-run-command.go | 40 +++ .../azure/manual/dbforpostgresql-database.go | 36 +++ .../manual/dbforpostgresql-flexible-server.go | 30 ++ .../manual/documentdb-database-accounts.go | 28 ++ sources/azure/manual/keyvault-secret.go | 50 +++ sources/azure/manual/keyvault-vault.go | 12 + sources/azure/manual/keyvault-vault_test.go | 12 +- sources/azure/manual/sql-database.go | 37 +++ .../azure/manual/storage-blob-container.go | 45 +++ sources/azure/manual/storage-fileshare.go | 43 +++ sources/azure/manual/storage-queues.go | 44 +++ sources/azure/manual/storage-table.go | 45 +++ .../shared/mocks/mock_galleries_client.go | 72 +++++ 21 files changed, 1199 insertions(+), 5 deletions(-) create mode 100644 sources/azure/clients/galleries-client.go create mode 100644 sources/azure/manual/compute-gallery.go create mode 100644 sources/azure/manual/compute-gallery_test.go create mode 100644 sources/azure/shared/mocks/mock_galleries_client.go diff --git a/sources/azure/clients/galleries-client.go b/sources/azure/clients/galleries-client.go new file mode 100644 index 00000000..30640c8e --- /dev/null +++ b/sources/azure/clients/galleries-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" +) + +//go:generate mockgen -destination=../shared/mocks/mock_galleries_client.go -package=mocks -source=galleries-client.go + +// GalleriesPager is a type alias for the generic Pager interface with gallery response type. +type GalleriesPager = Pager[armcompute.GalleriesClientListByResourceGroupResponse] + +// GalleriesClient is an interface for interacting with Azure compute galleries +type GalleriesClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) GalleriesPager + Get(ctx context.Context, resourceGroupName string, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) +} + +type galleriesClient struct { + client *armcompute.GalleriesClient +} + +func (c *galleriesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) GalleriesPager { + return c.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (c *galleriesClient) Get(ctx context.Context, resourceGroupName string, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, options) +} + +// NewGalleriesClient creates a new GalleriesClient from the Azure SDK client +func NewGalleriesClient(client *armcompute.GalleriesClient) GalleriesClient { + return &galleriesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index df82a979..ba2172fa 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -263,6 +263,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create gallery images client: %w", err) } + galleriesClient, err := armcompute.NewGalleriesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create galleries client: %w", err) + } + snapshotsClient, err := armcompute.NewSnapshotsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create snapshots client: %w", err) @@ -424,6 +429,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeGallery( + clients.NewGalleriesClient(galleriesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeGalleryImage( clients.NewGalleryImagesClient(galleryImagesClient), resourceGroupScopes, @@ -492,6 +501,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeGallery(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryImage(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeSharedGalleryImage(nil, subscriptionID), noOpCache), diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index ebf07824..e5773d47 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -6,7 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -16,7 +18,6 @@ import ( var ( ComputeGalleryApplicationVersionLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplicationVersion) - ComputeGalleryLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGallery) //todo: move to its adapter file when created, this is just a placeholder ComputeGalleryApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplication) //todo: move to its adapter file when created, this is just a placeholder ) @@ -103,6 +104,49 @@ func (c computeGalleryApplicationVersionWrapper) Search(ctx context.Context, sco return items, nil } +func (c computeGalleryApplicationVersionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 2 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 2 and be the gallery name and gallery application name"), scope, c.Type())) + return + } + galleryName := queryParts[0] + if galleryName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type())) + return + } + galleryApplicationName := queryParts[1] + if galleryApplicationName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByGalleryApplicationPager(rgScope.ResourceGroup, galleryName, galleryApplicationName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, galleryApplicationVersion := range page.Value { + if galleryApplicationVersion == nil || galleryApplicationVersion.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationVersionToSDPItem(galleryApplicationVersion, galleryName, galleryApplicationName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionToSDPItem( galleryApplicationVersion *armcompute.GalleryApplicationVersion, galleryName, diff --git a/sources/azure/manual/compute-gallery-image.go b/sources/azure/manual/compute-gallery-image.go index b903588e..8fdfd581 100644 --- a/sources/azure/manual/compute-gallery-image.go +++ b/sources/azure/manual/compute-gallery-image.go @@ -5,6 +5,8 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" @@ -92,6 +94,46 @@ func (c computeGalleryImageWrapper) Search(ctx context.Context, scope string, qu return items, nil } +func (c computeGalleryImageWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type())) + return + } + galleryName := queryParts[0] + if galleryName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + + pager := c.client.NewListByGalleryPager(rgScope.ResourceGroup, galleryName, nil) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, galleryImage := range page.Value { + if galleryImage == nil || galleryImage.Name == nil { + continue + } + item, sdpErr := c.azureGalleryImageToSDPItem(galleryImage, galleryName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (c computeGalleryImageWrapper) azureGalleryImageToSDPItem( galleryImage *armcompute.GalleryImage, galleryName, diff --git a/sources/azure/manual/compute-gallery.go b/sources/azure/manual/compute-gallery.go new file mode 100644 index 00000000..48f3fc40 --- /dev/null +++ b/sources/azure/manual/compute-gallery.go @@ -0,0 +1,211 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var ComputeGalleryLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGallery) + +type computeGalleryWrapper struct { + client clients.GalleriesClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGallery(client clients.GalleriesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &computeGalleryWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGallery, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/galleries/list-by-resource-group +func (c computeGalleryWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + for _, gallery := range page.Value { + if gallery == nil || gallery.Name == nil { + continue + } + item, sdpErr := c.azureGalleryToSDPItem(gallery, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + pager := c.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, gallery := range page.Value { + if gallery == nil || gallery.Name == nil { + continue + } + item, sdpErr := c.azureGalleryToSDPItem(gallery, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/galleries/get +func (c computeGalleryWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("queryParts must be exactly 1 and be the gallery name"), scope, c.Type()) + } + galleryName := queryParts[0] + if galleryName == "" { + return nil, azureshared.QueryError(errors.New("gallery name cannot be empty"), scope, c.Type()) + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryToSDPItem(&resp.Gallery, scope) +} + +func (c computeGalleryWrapper) azureGalleryToSDPItem(gallery *armcompute.Gallery, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(gallery, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if gallery.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery name is nil"), scope, c.Type()) + } + galleryName := *gallery.Name + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Child resources: list gallery images under this gallery (Search by gallery name) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryImage.String(), + Method: sdp.QueryMethod_SEARCH, + Query: galleryName, + Scope: scope, + }, + }) + + // URI-based links from community gallery info: PublisherURI, Eula + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if gallery.Properties != nil && gallery.Properties.SharingProfile != nil && gallery.Properties.SharingProfile.CommunityGalleryInfo != nil { + info := gallery.Properties.SharingProfile.CommunityGalleryInfo + if info.PublisherURI != nil { + AppendURILinks(&linkedItemQueries, *info.PublisherURI, linkedDNSHostnames, seenIPs, true, false) + } + if info.Eula != nil { + AppendURILinks(&linkedItemQueries, *info.Eula, linkedDNSHostnames, seenIPs, true, false) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGallery.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(gallery.Tags), + LinkedItemQueries: linkedItemQueries, + } + + // Health status from ProvisioningState + if gallery.Properties != nil && gallery.Properties.ProvisioningState != nil { + switch *gallery.Properties.ProvisioningState { + case armcompute.GalleryProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armcompute.GalleryProvisioningStateCreating, armcompute.GalleryProvisioningStateUpdating, armcompute.GalleryProvisioningStateDeleting, armcompute.GalleryProvisioningStateMigrating: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armcompute.GalleryProvisioningStateFailed: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +func (c computeGalleryWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + } +} + +func (c computeGalleryWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGalleryImage, + azureshared.ComputeGalleryApplication, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/shared_image_gallery +func (c computeGalleryWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_shared_image_gallery.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/read", + } +} + +func (c computeGalleryWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery_test.go b/sources/azure/manual/compute-gallery_test.go new file mode 100644 index 00000000..a6c2fde0 --- /dev/null +++ b/sources/azure/manual/compute-gallery_test.go @@ -0,0 +1,287 @@ +package manual_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + "github.com/overmindtech/cli/sources/azure/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestComputeGallery(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + + t.Run("Get", func(t *testing.T) { + galleryName := "test-gallery" + gallery := createAzureGallery(galleryName) + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, nil).Return( + armcompute.GalleriesClientGetResponse{ + Gallery: *gallery, + }, nil) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, galleryName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGallery.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGallery.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != galleryName { + t.Errorf("Expected unique attribute value %s, got %s", galleryName, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag 'env=test', got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ComputeGalleryImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: galleryName, + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("List", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + gallery2 := createAzureGallery("test-gallery-2") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, gallery2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(sdpItems)) + } + + for _, item := range sdpItems { + if item.Validate() != nil { + t.Fatalf("Expected no validation error, got: %v", item.Validate()) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + gallery2 := createAzureGallery("test-gallery-2") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, gallery2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + wg := &sync.WaitGroup{} + wg.Add(2) + + var items []*sdp.Item + mockItemHandler := func(item *sdp.Item) { + items = append(items, item) + wg.Done() + } + + var errs []error + mockErrorHandler := func(err error) { + errs = append(errs, err) + } + + stream := discovery.NewQueryResultStream(mockItemHandler, mockErrorHandler) + + listStreamable, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + listStreamable.ListStream(ctx, scope, true, stream) + wg.Wait() + + if len(errs) != 0 { + t.Fatalf("Expected no errors, got: %v", errs) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got: %d", len(items)) + } + }) + + t.Run("ListWithNilName", func(t *testing.T) { + gallery1 := createAzureGallery("test-gallery-1") + galleryNilName := &armcompute.Gallery{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + } + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockPager := newMockGalleriesPager(ctrl, []*armcompute.Gallery{gallery1, galleryNilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listable, ok := adapter.(discovery.ListableAdapter) + if !ok { + t.Fatalf("Adapter does not support List operation") + } + + sdpItems, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got: %d", len(sdpItems)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("gallery not found") + + mockClient := mocks.NewMockGalleriesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-gallery", nil).Return( + armcompute.GalleriesClientGetResponse{}, expectedErr) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "nonexistent-gallery", true) + if qErr == nil { + t.Error("Expected error when getting non-existent gallery, but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockGalleriesClient(ctrl) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, "", true) + if qErr == nil { + t.Error("Expected error when getting gallery with empty name, but got nil") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockGalleriesClient(ctrl) + + wrapper := manual.NewComputeGallery(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Get(ctx, scope) + if qErr == nil { + t.Error("Expected error when getting gallery with insufficient query parts, but got nil") + } + }) +} + +func createAzureGallery(galleryName string) *armcompute.Gallery { + return &armcompute.Gallery{ + Name: to.Ptr(galleryName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armcompute.GalleryProperties{ + Description: to.Ptr("Test shared image gallery"), + Identifier: &armcompute.GalleryIdentifier{ + UniqueName: to.Ptr("unique-" + galleryName), + }, + ProvisioningState: to.Ptr(armcompute.GalleryProvisioningStateSucceeded), + }, + } +} + +type mockGalleriesPager struct { + ctrl *gomock.Controller + items []*armcompute.Gallery + index int + more bool +} + +func newMockGalleriesPager(ctrl *gomock.Controller, items []*armcompute.Gallery) clients.GalleriesPager { + return &mockGalleriesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockGalleriesPager) More() bool { + return m.more +} + +func (m *mockGalleriesPager) NextPage(ctx context.Context) (armcompute.GalleriesClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armcompute.GalleriesClientListByResourceGroupResponse{ + GalleryList: armcompute.GalleryList{ + Value: []*armcompute.Gallery{}, + }, + }, nil + } + + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + + return armcompute.GalleriesClientListByResourceGroupResponse{ + GalleryList: armcompute.GalleryList{ + Value: []*armcompute.Gallery{item}, + }, + }, nil +} diff --git a/sources/azure/manual/compute-shared-gallery-image.go b/sources/azure/manual/compute-shared-gallery-image.go index 97d09b6c..37ba6c70 100644 --- a/sources/azure/manual/compute-shared-gallery-image.go +++ b/sources/azure/manual/compute-shared-gallery-image.go @@ -5,7 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -14,7 +16,7 @@ import ( ) var ( - ComputeSharedGalleryImageLookupByLocation = shared.NewItemTypeLookup("location", azureshared.ComputeSharedGalleryImage) + ComputeSharedGalleryImageLookupByLocation = shared.NewItemTypeLookup("location", azureshared.ComputeSharedGalleryImage) ComputeSharedGalleryImageLookupByGalleryUniqueName = shared.NewItemTypeLookup("galleryUniqueName", azureshared.ComputeSharedGalleryImage) ComputeSharedGalleryImageLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeSharedGalleryImage) ) @@ -96,6 +98,44 @@ func (c computeSharedGalleryImageWrapper) Search(ctx context.Context, scope stri return items, nil } +func (c computeSharedGalleryImageWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 2 { + stream.SendError(azureshared.QueryError(errors.New("queryParts must be exactly 2: location and gallery unique name"), scope, c.Type())) + return + } + location := queryParts[0] + if location == "" { + stream.SendError(azureshared.QueryError(errors.New("location cannot be empty"), scope, c.Type())) + return + } + galleryUniqueName := queryParts[1] + if galleryUniqueName == "" { + stream.SendError(azureshared.QueryError(errors.New("gallery unique name cannot be empty"), scope, c.Type())) + return + } + + pager := c.client.NewListPager(location, galleryUniqueName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, image := range page.Value { + if image == nil || image.Name == nil { + continue + } + item, sdpErr := c.azureSharedGalleryImageToSDPItem(image, location, galleryUniqueName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (c computeSharedGalleryImageWrapper) azureSharedGalleryImageToSDPItem( image *armcompute.SharedGalleryImage, location, diff --git a/sources/azure/manual/compute-virtual-machine-extension.go b/sources/azure/manual/compute-virtual-machine-extension.go index 468c6115..af59349a 100644 --- a/sources/azure/manual/compute-virtual-machine-extension.go +++ b/sources/azure/manual/compute-virtual-machine-extension.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -228,6 +230,41 @@ func (c computeVirtualMachineExtensionWrapper) Search(ctx context.Context, scope return items, nil } +func (c computeVirtualMachineExtensionWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: virtualMachineName, got %d", len(queryParts)), scope, c.Type())) + return + } + virtualMachineName := queryParts[0] + if virtualMachineName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("virtualMachineName cannot be empty"), scope, c.Type())) + return + } + + rgScope, err := c.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + resp, err := c.client.List(ctx, rgScope.ResourceGroup, virtualMachineName, nil) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, extension := range resp.Value { + if extension.Name == nil { + continue + } + item, sdpErr := c.azureVirtualMachineExtensionToSDPItem(extension, virtualMachineName, *extension.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } +} + func (c computeVirtualMachineExtensionWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/compute-virtual-machine-run-command.go b/sources/azure/manual/compute-virtual-machine-run-command.go index 904d62a8..2ce81acf 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command.go +++ b/sources/azure/manual/compute-virtual-machine-run-command.go @@ -6,7 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -281,6 +283,44 @@ func (s computeVirtualMachineRunCommandWrapper) Search(ctx context.Context, scop return items, nil } +func (s computeVirtualMachineRunCommandWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(errors.New("search requires exactly 1 query part: virtualMachineName"), scope, s.Type())) + return + } + virtualMachineName := queryParts[0] + if virtualMachineName == "" { + stream.SendError(azureshared.QueryError(errors.New("virtualMachineName cannot be empty"), scope, s.Type())) + return + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.NewListByVirtualMachinePager(rgScope.ResourceGroup, virtualMachineName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, runCommand := range page.Value { + if runCommand.Name == nil { + continue + } + item, sdpErr := s.azureVirtualMachineRunCommandToSDPItem(runCommand, virtualMachineName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s computeVirtualMachineRunCommandWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/dbforpostgresql-database.go b/sources/azure/manual/dbforpostgresql-database.go index 384de872..34f182b6 100644 --- a/sources/azure/manual/dbforpostgresql-database.go +++ b/sources/azure/manual/dbforpostgresql-database.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -136,6 +138,40 @@ func (s dbforPostgreSQLDatabaseWrapper) Search(ctx context.Context, scope string return items, nil } +func (s dbforPostgreSQLDatabaseWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("Search requires 1 query part: serverName"), scope, s.Type())) + return + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, database := range page.Value { + if database.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLDatabaseToSDPItem(database, serverName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + // reference: GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DBforPostgreSQL/flexibleServers/{serverName}/databases/{databaseName}?api-version=2025-08-01 func (s dbforPostgreSQLDatabaseWrapper) GetLookups() sources.ItemTypeLookups { return sources.ItemTypeLookups{ diff --git a/sources/azure/manual/dbforpostgresql-flexible-server.go b/sources/azure/manual/dbforpostgresql-flexible-server.go index 51867295..b9e25f5c 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server.go @@ -6,7 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -82,6 +84,34 @@ func (s dbforPostgreSQLFlexibleServerWrapper) List(ctx context.Context, scope st return items, nil } +func (s dbforPostgreSQLFlexibleServerWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByResourceGroup(ctx, rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, server := range page.Value { + if server.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLFlexibleServerToSDPItem(server, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s dbforPostgreSQLFlexibleServerWrapper) azureDBforPostgreSQLFlexibleServerToSDPItem(server *armpostgresqlflexibleservers.Server, scope string) (*sdp.Item, *sdp.QueryError) { attributes, err := shared.ToAttributesWithExclude(server, "tags") if err != nil { diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 950f5822..28b12c20 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -6,7 +6,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -58,6 +60,32 @@ func (s documentDBDatabaseAccountsWrapper) List(ctx context.Context, scope strin return items, nil } +func (s documentDBDatabaseAccountsWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByResourceGroup(rgScope.ResourceGroup) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, account := range page.Value { + item, sdpErr := s.azureDocumentDBDatabaseAccountToSDPItem(account, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s documentDBDatabaseAccountsWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { if len(queryParts) < 1 { return nil, &sdp.QueryError{ diff --git a/sources/azure/manual/keyvault-secret.go b/sources/azure/manual/keyvault-secret.go index fa25686d..801e150e 100644 --- a/sources/azure/manual/keyvault-secret.go +++ b/sources/azure/manual/keyvault-secret.go @@ -5,7 +5,9 @@ import ( "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -110,6 +112,54 @@ func (k keyvaultSecretWrapper) Search(ctx context.Context, scope string, queryPa return items, nil } +func (k keyvaultSecretWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: vaultName"), scope, k.Type())) + return + } + vaultName := queryParts[0] + if vaultName == "" { + stream.SendError(azureshared.QueryError(errors.New("vaultName cannot be empty"), scope, k.Type())) + return + } + + rgScope, err := k.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, k.Type())) + return + } + pager := k.client.NewListPager(rgScope.ResourceGroup, vaultName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, k.Type())) + return + } + for _, secret := range page.Value { + if secret.Name == nil { + continue + } + var secretVaultName string + if secret.ID != nil && *secret.ID != "" { + vaultParams := azureshared.ExtractPathParamsFromResourceID(*secret.ID, []string{"vaults"}) + if len(vaultParams) > 0 { + secretVaultName = vaultParams[0] + } + } + if secretVaultName == "" { + secretVaultName = vaultName + } + item, sdpErr := k.azureSecretToSDPItem(secret, secretVaultName, *secret.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (k keyvaultSecretWrapper) azureSecretToSDPItem(secret *armkeyvault.Secret, vaultName, secretName, scope string) (*sdp.Item, *sdp.QueryError) { attributes, err := shared.ToAttributesWithExclude(secret, "tags") if err != nil { diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index cbb8f037..de66dca1 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -135,6 +135,17 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Tags: azureshared.ConvertAzureTags(vault.Tags), } + // Child resources: list secrets in this vault (Search by vault name) + vaultName := *vault.Name + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultSecret.String(), + Method: sdp.QueryMethod_SEARCH, + Query: vaultName, + Scope: scope, + }, + }) + // Link to Private Endpoints from Private Endpoint Connections // Reference: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/private-endpoints/get // GET /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/privateEndpoints/{privateEndpointName} @@ -290,6 +301,7 @@ func (k keyvaultVaultWrapper) TerraformMappings() []*sdp.TerraformMapping { func (k keyvaultVaultWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( + azureshared.KeyVaultSecret, azureshared.NetworkPrivateEndpoint, azureshared.NetworkSubnet, azureshared.KeyVaultManagedHSM, diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index c4676778..267d6955 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -108,6 +108,12 @@ func TestKeyVaultVault(t *testing.T) { t.Run("StaticTests", func(t *testing.T) { queryTests := shared.QueryTests{ { + // Child resources: secrets in this vault (SEARCH by vault name) + ExpectedType: azureshared.KeyVaultSecret.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: vaultName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), ExpectedMethod: sdp.QueryMethod_GET, @@ -214,9 +220,9 @@ func TestKeyVaultVault(t *testing.T) { t.Fatalf("Expected no error, got: %v", qErr) } - // Should have no linked item queries - if len(sdpItem.GetLinkedItemQueries()) != 0 { - t.Errorf("Expected no linked item queries, got %d", len(sdpItem.GetLinkedItemQueries())) + // Should only have the child SEARCH link (secrets in vault); no private endpoints, subnets, etc. + if len(sdpItem.GetLinkedItemQueries()) != 1 { + t.Errorf("Expected 1 linked item query (KeyVaultSecret SEARCH), got %d", len(sdpItem.GetLinkedItemQueries())) } }) diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index f22feca3..1de540a9 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -2,10 +2,13 @@ package manual import ( "context" + "errors" "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -375,6 +378,40 @@ func (s sqlDatabaseWrapper) Search(ctx context.Context, scope string, queryParts return items, nil } +func (s sqlDatabaseWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: serverName"), scope, s.Type())) + return + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, database := range page.Value { + if database.Name == nil { + continue + } + item, sdpErr := s.azureSqlDatabaseToSDPItem(database, serverName, *database.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s sqlDatabaseWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/storage-blob-container.go b/sources/azure/manual/storage-blob-container.go index 532c32f5..d123ad3b 100644 --- a/sources/azure/manual/storage-blob-container.go +++ b/sources/azure/manual/storage-blob-container.go @@ -6,7 +6,9 @@ import ( "strings" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -107,6 +109,49 @@ func (s storageBlobContainerWrapper) Search(ctx context.Context, scope string, q return items, nil } +func (s storageBlobContainerWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: storageAccountName, got %d", len(queryParts)), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + if storageAccountName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("storageAccountName cannot be empty"), scope, s.Type())) + return + } + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, container := range page.Value { + if container.Name == nil { + continue + } + item, sdpErr := s.azureBlobContainerToSDPItem(&armstorage.BlobContainer{ + ID: container.ID, + Name: container.Name, + Type: container.Type, + ContainerProperties: container.Properties, + Etag: container.Etag, + }, storageAccountName, *container.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageBlobContainerWrapper) GetLookups() sources.ItemTypeLookups { return sources.ItemTypeLookups{ StorageAccountLookupByName, diff --git a/sources/azure/manual/storage-fileshare.go b/sources/azure/manual/storage-fileshare.go index 354702db..21c8d3cb 100644 --- a/sources/azure/manual/storage-fileshare.go +++ b/sources/azure/manual/storage-fileshare.go @@ -2,9 +2,12 @@ package manual import ( "context" + "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -115,6 +118,46 @@ func (s storageFileShareWrapper) Search(ctx context.Context, scope string, query return items, nil } +func (s storageFileShareWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: storageAccountName"), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, fileShare := range page.Value { + if fileShare.Name == nil { + continue + } + item, sdpErr := s.azureFileShareToSDPItem(&armstorage.FileShare{ + ID: fileShare.ID, + Name: fileShare.Name, + Type: fileShare.Type, + FileShareProperties: fileShare.Properties, + Etag: fileShare.Etag, + }, storageAccountName, *fileShare.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageFileShareWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/storage-queues.go b/sources/azure/manual/storage-queues.go index 3af9df7f..0d72934b 100644 --- a/sources/azure/manual/storage-queues.go +++ b/sources/azure/manual/storage-queues.go @@ -2,9 +2,12 @@ package manual import ( "context" + "errors" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -150,6 +153,47 @@ func (s storageQueuesWrapper) Search(ctx context.Context, scope string, queryPar return items, nil } +func (s storageQueuesWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) < 1 { + stream.SendError(azureshared.QueryError(errors.New("Search requires 1 query part: storageAccountName"), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, queue := range page.Value { + if queue.Name == nil || queue.QueueProperties == nil { + continue + } + item, sdpErr := s.azureQueueToSDPItem(&armstorage.Queue{ + ID: queue.ID, + Name: queue.Name, + Type: queue.Type, + QueueProperties: &armstorage.QueueProperties{ + Metadata: queue.QueueProperties.Metadata, + }, + }, storageAccountName, *queue.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageQueuesWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/manual/storage-table.go b/sources/azure/manual/storage-table.go index 7ee50f70..01defcb4 100644 --- a/sources/azure/manual/storage-table.go +++ b/sources/azure/manual/storage-table.go @@ -5,7 +5,9 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" @@ -153,6 +155,49 @@ func (s storageTablesWrapper) Search(ctx context.Context, scope string, queryPar return items, nil } +func (s storageTablesWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) { + if len(queryParts) != 1 { + stream.SendError(azureshared.QueryError(fmt.Errorf("queryParts must be 1 query part: storageAccountName, got %d", len(queryParts)), scope, s.Type())) + return + } + storageAccountName := queryParts[0] + if storageAccountName == "" { + stream.SendError(azureshared.QueryError(fmt.Errorf("storageAccountName cannot be empty"), scope, s.Type())) + return + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, table := range page.Value { + if table.Name == nil { + continue + } + item, sdpErr := s.azureTableToSDPItem(&armstorage.Table{ + ID: table.ID, + Name: table.Name, + Type: table.Type, + TableProperties: table.TableProperties, + }, storageAccountName, *table.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + func (s storageTablesWrapper) SearchLookups() []sources.ItemTypeLookups { return []sources.ItemTypeLookups{ { diff --git a/sources/azure/shared/mocks/mock_galleries_client.go b/sources/azure/shared/mocks/mock_galleries_client.go new file mode 100644 index 00000000..fc58e848 --- /dev/null +++ b/sources/azure/shared/mocks/mock_galleries_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: galleries-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_galleries_client.go -package=mocks -source=galleries-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockGalleriesClient is a mock of GalleriesClient interface. +type MockGalleriesClient struct { + ctrl *gomock.Controller + recorder *MockGalleriesClientMockRecorder + isgomock struct{} +} + +// MockGalleriesClientMockRecorder is the mock recorder for MockGalleriesClient. +type MockGalleriesClientMockRecorder struct { + mock *MockGalleriesClient +} + +// NewMockGalleriesClient creates a new mock instance. +func NewMockGalleriesClient(ctrl *gomock.Controller) *MockGalleriesClient { + mock := &MockGalleriesClient{ctrl: ctrl} + mock.recorder = &MockGalleriesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleriesClient) EXPECT() *MockGalleriesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleriesClient) Get(ctx context.Context, resourceGroupName, galleryName string, options *armcompute.GalleriesClientGetOptions) (armcompute.GalleriesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, options) + ret0, _ := ret[0].(armcompute.GalleriesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleriesClientMockRecorder) Get(ctx, resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleriesClient)(nil).Get), ctx, resourceGroupName, galleryName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockGalleriesClient) NewListByResourceGroupPager(resourceGroupName string, options *armcompute.GalleriesClientListByResourceGroupOptions) clients.GalleriesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.GalleriesPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockGalleriesClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockGalleriesClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} From 6097ec3155b3abf1314947e712388ad5f55dc8ad Mon Sep 17 00:00:00 2001 From: jameslaneovermind <122231433+jameslaneovermind@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:59:42 +0000 Subject: [PATCH 50/51] =?UTF-8?q?Add=20BigQuery=20and=20Bigtable=20IAM=20T?= =?UTF-8?q?erraform=20mappings=20for=20blast=20radius=20ana=E2=80=A6=20(#3?= =?UTF-8?q?976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add IAM Terraform mappings (`_iam_binding`, `_iam_member`, `_iam_policy`) for BigQuery Dataset, BigQuery Table, Bigtable Instance, and Bigtable Table so that IAM changes in Terraform plans resolve to the parent resource for blast radius analysis - Register the BigQuery Table adapter in `manual/adapters.go` (was previously missing from the adapter list despite the adapter code existing) - Add all 12 new mappings to `TestCriticalTerraformMappingsRegistered` to prevent future regressions ## Context This was prompted by feedback from the Box PoC Data Platform team (ENG-2644). Their Terraform modules for BigQuery and Bigtable include IAM binding resources (`google_bigquery_dataset_iam_member`, `google_bigtable_instance_iam_binding`, etc.) which were previously showing as "Unsupported" in change analysis. Since IAM bindings are Terraform-only constructs with no standalone GCP API resource, the correct approach is to map them back to their parent resource -- the same pattern we already use for Pub/Sub IAM mappings. **Resources covered (12 new mappings across 4 adapters):** | Parent Resource | IAM Terraform Types | |---|---| | `gcp-big-query-dataset` | `google_bigquery_dataset_iam_{binding,member,policy}` | | `gcp-big-query-table` | `google_bigquery_table_iam_{binding,member,policy}` | | `gcp-big-table-admin-instance` | `google_bigtable_instance_iam_{binding,member,policy}` | | `gcp-big-table-admin-table` | `google_bigtable_table_iam_{binding,member,policy}` | ## Bug fix The BigQuery Table adapter was defined in `big-query-table.go` but never registered in `manual/adapters.go`, meaning its Terraform mappings (including the original `google_bigquery_table` mapping) were not included in adapter metadata. This PR fixes that by adding the registration line. ## Test plan - [x] `go build ./sources/gcp/...` passes - [x] `go test ./sources/gcp/proc/` passes (including `TestCriticalTerraformMappingsRegistered` with all 12 new entries) - [x] `go test ./sources/gcp/manual/` passes - [x] `go test ./sources/gcp/dynamic/...` passes Ticket: https://linear.app/overmind/issue/ENG-2696/bigquery-and-bigtable-iam-binding-terraform-mappings-missing-core --- > [!NOTE] > **Low Risk** > Low-risk metadata/mapping changes plus test coverage; main impact is on how Terraform IAM resources are resolved in change analysis (GET/SEARCH field selection). > > **Overview** > Improves Terraform plan change analysis by mapping BigQuery and Bigtable IAM-only Terraform resources (dataset/table and instance/table `_iam_{binding,member,policy}`) back to their parent GCP resources so they no longer appear as **Unsupported** and can participate in blast radius analysis. > > Also registers the previously unregistered manual `BigQueryTable` adapter so its Terraform mappings are included in metadata, and extends `TestCriticalTerraformMappingsRegistered` to assert all 12 new IAM mappings (plus the table adapter mapping) stay wired up. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f69895985a530c640bf60c67f1cb5ea599f0448d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 520c709437b4045cb7e48b91e6be43d47a92ed45 --- .../adapters/big-table-admin-instance.go | 17 ++++ .../dynamic/adapters/big-table-admin-table.go | 20 +++++ sources/gcp/manual/adapters.go | 1 + sources/gcp/manual/big-query-dataset.go | 17 ++++ sources/gcp/manual/big-query-table.go | 21 ++++- sources/gcp/proc/proc_test.go | 90 +++++++++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) diff --git a/sources/gcp/dynamic/adapters/big-table-admin-instance.go b/sources/gcp/dynamic/adapters/big-table-admin-instance.go index 10657461..b1f52589 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance.go @@ -37,6 +37,23 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_bigtable_instance.name", }, + // IAM resources for Bigtable Instances. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent instance for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigtable_instance_iam + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_binding.instance", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_member.instance", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigtable_instance_iam_policy.instance", + }, }, }, }.Register() diff --git a/sources/gcp/dynamic/adapters/big-table-admin-table.go b/sources/gcp/dynamic/adapters/big-table-admin-table.go index 8194fe7e..499b3643 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table.go @@ -46,6 +46,26 @@ var _ = registerableAdapter{ TerraformMethod: sdp.QueryMethod_SEARCH, TerraformQueryMap: "google_bigtable_table.id", }, + // IAM resources for Bigtable Tables. These are Terraform-only constructs + // (no standalone GCP API resource exists). We use the instance_name + // attribute because the table attribute is a bare name that the SEARCH + // handler would misinterpret as an instance name. Using instance_name + // lists all tables in the affected instance, providing instance-level + // blast radius for table IAM changes. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigtable_table_iam + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_binding.instance_name", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_member.instance_name", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigtable_table_iam_policy.instance_name", + }, }, }, }.Register() diff --git a/sources/gcp/manual/adapters.go b/sources/gcp/manual/adapters.go index a28cfb49..9af03d1b 100644 --- a/sources/gcp/manual/adapters.go +++ b/sources/gcp/manual/adapters.go @@ -295,6 +295,7 @@ func Adapters(ctx context.Context, projectLocations, regionLocations, zoneLocati sources.WrapperToAdapter(NewCloudKMSCryptoKey(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewCloudKMSCryptoKeyVersion(kmsLoader, projectLocations), cache), sources.WrapperToAdapter(NewBigQueryDataset(shared.NewBigQueryDatasetClient(bigQueryDatasetCli), projectLocations), cache), + sources.WrapperToAdapter(NewBigQueryTable(shared.NewBigQueryTableClient(bigQueryDatasetCli), projectLocations), cache), sources.WrapperToAdapter(NewLoggingSink(shared.NewLoggingConfigClient(loggingConfigCli), projectLocations), cache), sources.WrapperToAdapter(NewBigQueryRoutine(shared.NewBigQueryRoutineClient(bigQueryDatasetCli), projectLocations), cache), sources.WrapperToAdapter(NewStorageBucketIAMPolicy(shared.NewStorageBucketIAMPolicyGetter(storageCli), projectLocations), cache), diff --git a/sources/gcp/manual/big-query-dataset.go b/sources/gcp/manual/big-query-dataset.go index 79343481..0b9175b5 100644 --- a/sources/gcp/manual/big-query-dataset.go +++ b/sources/gcp/manual/big-query-dataset.go @@ -62,6 +62,23 @@ func (b BigQueryDatasetWrapper) TerraformMappings() []*sdp.TerraformMapping { TerraformMethod: sdp.QueryMethod_GET, TerraformQueryMap: "google_bigquery_dataset.dataset_id", }, + // IAM resources for BigQuery Datasets. These are Terraform-only constructs + // (no standalone GCP API resource exists). When an IAM binding/member/policy + // changes, we resolve it to the parent dataset for blast radius analysis. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset_iam + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_binding.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_member.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "google_bigquery_dataset_iam_policy.dataset_id", + }, } } diff --git a/sources/gcp/manual/big-query-table.go b/sources/gcp/manual/big-query-table.go index f0ac0121..7f11a2e3 100644 --- a/sources/gcp/manual/big-query-table.go +++ b/sources/gcp/manual/big-query-table.go @@ -57,7 +57,7 @@ func (b BigQueryTableWrapper) PotentialLinks() map[shared.ItemType]bool { ) } -// TerraformMappings returns the Terraform mappings for the BigQuery dataset wrapper +// TerraformMappings returns the Terraform mappings for the BigQuery table wrapper func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { return []*sdp.TerraformMapping{ { @@ -68,6 +68,25 @@ func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping { // them to GET operations by extracting the last N path parameters (based on GetLookups count). TerraformQueryMap: "google_bigquery_table.id", }, + // IAM resources for BigQuery Tables. These are Terraform-only constructs + // (no standalone GCP API resource exists). We use the dataset_id attribute + // because table_id is a bare name that the SEARCH handler would misinterpret + // as a dataset ID. Using dataset_id lists all tables in the affected dataset, + // providing dataset-level blast radius for table IAM changes. + // + // Reference: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table_iam + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_binding.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_member.dataset_id", + }, + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "google_bigquery_table_iam_policy.dataset_id", + }, } } diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index d244fcf7..5089058b 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -829,6 +829,96 @@ func TestCriticalTerraformMappingsRegistered(t *testing.T) { expectedMethod: sdp.QueryMethod_GET, reason: "IAM policy on topic — resolves to parent topic for blast radius", }, + // BigQuery Dataset IAM + { + terraformType: "google_bigquery_dataset_iam_binding", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on dataset — resolves to parent dataset for blast radius", + }, + { + terraformType: "google_bigquery_dataset_iam_member", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on dataset — resolves to parent dataset for blast radius", + }, + { + terraformType: "google_bigquery_dataset_iam_policy", + expectedType: gcpshared.BigQueryDataset.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on dataset — resolves to parent dataset for blast radius", + }, + // BigQuery Table IAM — resolves via dataset_id (bare table_id would be + // misinterpreted as a dataset ID by the SEARCH handler) + { + terraformType: "google_bigquery_table_iam_binding", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM binding on table — resolves via dataset_id to list tables in affected dataset", + }, + { + terraformType: "google_bigquery_table_iam_member", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM member on table — resolves via dataset_id to list tables in affected dataset", + }, + { + terraformType: "google_bigquery_table_iam_policy", + expectedType: gcpshared.BigQueryTable.String(), + expectedField: "dataset_id", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM policy on table — resolves via dataset_id to list tables in affected dataset", + }, + // Bigtable Instance IAM + { + terraformType: "google_bigtable_instance_iam_binding", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM binding on instance — resolves to parent instance for blast radius", + }, + { + terraformType: "google_bigtable_instance_iam_member", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM member on instance — resolves to parent instance for blast radius", + }, + { + terraformType: "google_bigtable_instance_iam_policy", + expectedType: gcpshared.BigTableAdminInstance.String(), + expectedField: "instance", + expectedMethod: sdp.QueryMethod_GET, + reason: "IAM policy on instance — resolves to parent instance for blast radius", + }, + // Bigtable Table IAM — resolves via instance_name (the table attribute is + // a bare name that the SEARCH handler would misinterpret as an instance name) + { + terraformType: "google_bigtable_table_iam_binding", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM binding on table — resolves via instance_name to list tables in affected instance", + }, + { + terraformType: "google_bigtable_table_iam_member", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM member on table — resolves via instance_name to list tables in affected instance", + }, + { + terraformType: "google_bigtable_table_iam_policy", + expectedType: gcpshared.BigTableAdminTable.String(), + expectedField: "instance_name", + expectedMethod: sdp.QueryMethod_SEARCH, + reason: "IAM policy on table — resolves via instance_name to list tables in affected instance", + }, } for _, tc := range criticalMappings { From 3f411bcad0ff2252cdf23d08d73a49acc408270a Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Mon, 23 Feb 2026 22:01:34 +0000 Subject: [PATCH 51/51] Run go mod tidy --- go.mod | 193 +-------------- go.sum | 725 --------------------------------------------------------- 2 files changed, 6 insertions(+), 912 deletions(-) diff --git a/go.mod b/go.mod index 120b4e1b..94be0a7f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( cloud.google.com/go/bigtable v1.42.0 cloud.google.com/go/certificatemanager v1.9.6 cloud.google.com/go/compute v1.54.0 - cloud.google.com/go/compute/metadata v0.9.0 + cloud.google.com/go/compute/metadata v0.9.0 // indirect 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 @@ -39,8 +39,6 @@ require ( cloud.google.com/go/storage v1.60.0 cloud.google.com/go/storagetransfer v1.13.1 connectrpc.com/connect v1.18.1 // v1.19.0 was faulty, wait until it is above this version - connectrpc.com/otelconnect v0.9.0 - github.com/1password/onepassword-sdk-go v0.4.0 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 @@ -57,12 +55,6 @@ require ( 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/a-h/templ v0.3.977 - github.com/adrg/strutil v0.3.1 - github.com/akedrou/textdiff v0.1.0 - github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 - github.com/antihax/optional v1.0.0 - github.com/auth0/go-auth0/v2 v2.5.0 github.com/auth0/go-jwt-middleware/v2 v2.3.1 github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.32.7 @@ -88,84 +80,50 @@ require ( github.com/aws/aws-sdk-go-v2/service/rds v1.115.0 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 - github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1 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 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 github.com/aws/smithy-go v1.24.0 - github.com/bombsimon/logrusr/v4 v4.1.0 - github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 - github.com/brianvoe/gofakeit/v7 v7.14.0 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 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 - github.com/exaring/otelpgx v0.10.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/getsentry/sentry-go v0.42.0 github.com/go-jose/go-jose/v4 v4.1.3 - github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/google/btree v1.1.3 - github.com/google/go-github/v80 v80.0.0 github.com/google/uuid v1.6.0 github.com/googleapis/gax-go/v2 v2.17.0 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e - github.com/gorilla/mux v1.8.1 - github.com/harness/harness-go-sdk v0.7.9 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20260210152655-f4be3ba97d94 github.com/hashicorp/terraform-plugin-framework v1.17.0 github.com/hashicorp/terraform-plugin-go v0.29.0 github.com/hashicorp/terraform-plugin-testing v1.14.0 - github.com/invopop/jsonschema v0.13.0 - github.com/jackc/pgx/v5 v5.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 - github.com/jxskiss/base62 v1.1.0 - github.com/kaptinlin/jsonrepair v0.2.8 - github.com/manifoldco/promptui v0.9.0 - github.com/mavolin/go-htmx v1.0.0 - github.com/mergestat/timediff v0.0.4 github.com/micahhausler/aws-iam-policy v0.4.2 github.com/miekg/dns v1.1.72 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-ps v1.0.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.4 github.com/nats-io/nats.go v1.48.0 github.com/nats-io/nkeys v0.4.15 - github.com/neo4j/neo4j-go-driver/v6 v6.0.0 - github.com/onsi/ginkgo/v2 v2.28.1 - github.com/onsi/gomega v1.39.1 - github.com/openai/openai-go/v3 v3.21.0 + 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/pborman/ansi v1.0.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/posthog/posthog-go v1.10.0 - github.com/projectdiscovery/subfinder/v2 v2.12.0 - github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf - github.com/riverqueue/river v0.30.2 - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2 - github.com/riverqueue/river/rivertype v0.30.2 - github.com/riverqueue/rivercontrib/otelriver v0.7.0 - github.com/rs/cors v1.11.1 - github.com/samber/slog-logrus/v2 v2.5.3 - github.com/sashabaranov/go-openai v1.41.2 - github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/stripe/stripe-go/v84 v84.3.0 - github.com/tiktoken-go/tokenizer v0.7.0 github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 - github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/xiam/dig v0.0.0-20191116195832-893b5fb5093b github.com/zclconf/go-cty v1.17.0 go.etcd.io/bbolt v1.4.3 @@ -195,16 +153,11 @@ require ( k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 - k8s.io/component-base v0.35.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 - modernc.org/sqlite v1.45.0 - riverqueue.com/riverui v0.14.0 - sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/kind v0.31.0 ) require ( - aead.dev/minisign v0.2.0 // indirect al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/schedule v0.1.0 // indirect @@ -218,23 +171,15 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect - github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect - github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/PuerkitoBio/rehttp v1.4.0 // indirect - github.com/STARRY-S/zip v0.2.1 // indirect - github.com/VividCortex/ewma v1.2.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/akrylysov/pogreb v0.10.1 // indirect github.com/alecthomas/chroma/v2 v2.16.0 // indirect github.com/alecthomas/kingpin/v2 v2.4.0 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect - github.com/andybalholm/brotli v1.1.1 // indirect github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect @@ -250,14 +195,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect - github.com/bodgit/windows v1.0.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect; being pulled by glamour, this will be resolved in https://github.com/charmbracelet/glamour/pull/408 @@ -265,65 +202,37 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/cheggaaa/pb/v3 v3.1.4 // indirect - github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect - github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/containerd/console v1.0.4 // indirect - github.com/corpix/uarand v0.2.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/extism/go-sdk v1.7.1 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gaissmai/bart v0.20.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.1 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.26.1 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-github/v30 v30.1.0 // indirect - github.com/google/go-github/v75 v75.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.8 // indirect - github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect - github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -333,7 +242,6 @@ require ( github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect @@ -344,38 +252,20 @@ require ( github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/klauspost/pgzip v1.2.6 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/lestrrat-go/blackmagic v1.0.3 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/lithammer/fuzzysearch v1.1.8 - github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mholt/archives v0.1.0 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect - github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -387,104 +277,41 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkoukk/tiktoken-go v0.1.7 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/projectdiscovery/blackrock v0.0.1 // indirect - github.com/projectdiscovery/cdncheck v1.1.24 // indirect - github.com/projectdiscovery/chaos-client v0.5.2 // indirect - github.com/projectdiscovery/dnsx v1.2.2 // indirect - github.com/projectdiscovery/fastdialer v0.4.1 // indirect - github.com/projectdiscovery/goflags v0.1.74 // indirect - github.com/projectdiscovery/gologger v1.1.54 // indirect - github.com/projectdiscovery/hmap v0.0.90 // indirect - github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect - github.com/projectdiscovery/networkpolicy v0.1.16 // indirect - github.com/projectdiscovery/ratelimit v0.0.81 // indirect - github.com/projectdiscovery/retryabledns v1.0.102 // indirect - github.com/projectdiscovery/retryablehttp-go v1.0.115 // indirect - github.com/projectdiscovery/utils v0.4.21 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/refraction-networking/utls v1.7.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e // indirect - github.com/riverqueue/river/riverdriver v0.30.2 // indirect - github.com/riverqueue/river/rivershared v0.30.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/xid v1.5.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect - github.com/samber/lo v1.52.0 // indirect - github.com/samber/slog-common v0.20.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.7 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/syndtr/goleveldb v1.0.0 // indirect - github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 // indirect - github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect - github.com/tetratelabs/wazero v1.11.0 // indirect - github.com/therootcompany/xz v1.0.1 // indirect - github.com/tidwall/btree v1.6.0 // indirect - github.com/tidwall/buntdb v1.3.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/grect v0.1.4 // indirect - github.com/tidwall/match v1.2.0 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/rtred v0.1.2 // indirect - github.com/tidwall/sjson v1.2.5 // indirect - github.com/tidwall/tinyqueue v0.1.1 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect - github.com/ulikunitz/xz v0.5.15 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/weppos/publicsuffix-go v0.30.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.10 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - github.com/zcalusic/sysinfo v1.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect - github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect - go.devnw.com/structs v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.32.0 // indirect @@ -494,23 +321,15 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect - gopkg.in/djherbis/times.v1 v1.3.0 // 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 - k8s.io/apiextensions-apiserver v0.35.0 // indirect - k8s.io/apiserver v0.35.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - modernc.org/libc v1.67.6 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 1b0cecf6..15504935 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= -aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= @@ -16,15 +14,6 @@ buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0 buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/aiplatform v1.116.0 h1:Qc8tv4DD6IbQfDKDd1Hu2qeGeYxTKTeZ7GH0vQrLAm8= @@ -33,8 +22,6 @@ cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.73.1 h1:v//GZwdhtmCbZ87rOnxz7pectOGFS1GNRvrGTvLzka4= cloud.google.com/go/bigquery v1.73.1/go.mod h1:KSLx1mKP/yGiA8U+ohSrqZM1WknUnjZAxHAQZ51/b1k= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= @@ -43,7 +30,6 @@ cloud.google.com/go/certificatemanager v1.9.6 h1:v5X8X+THKrS9OFZb6k0GRDP1WQxLXTd cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w= cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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.46.0 h1:xX94Lo3xrS5OkdMWKvpEVAbBwjN9uleVv6vOi02fL4s= @@ -54,7 +40,6 @@ cloud.google.com/go/dataplex v1.28.0 h1:rROI3iqMVI9nXT701ULoFRETQVAOAPC3mPSWFDxX cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= cloud.google.com/go/dataproc/v2 v2.15.0 h1:I/Yux/d8uaxf3W+d59kolGTOc52+VZaL6RzJw7oDOeg= cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/filestore v1.10.3 h1:3KZifUVTqGhNNv6MLeONYth1HjlVM4vDhaH+xrdPljU= cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= cloud.google.com/go/functions v1.19.7 h1:7LcOD18euIVGRUPaeCmgO6vfWSLNIsi6STWRQcdANG8= @@ -73,8 +58,6 @@ cloud.google.com/go/networksecurity v0.11.0 h1:+ahtCqEqwHw3a3UIeG21vT817xt9kkDDA cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM= cloud.google.com/go/orgpolicy v1.15.1 h1:0hq12wxNwcfUMojr5j3EjWECSInIuyYDhkAWXTomRhc= cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/redis v1.18.3 h1:6LI8zSt+vmE3WQ7hE5GsJ13CbJBLV1qUw6B7CY31Wcw= cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= cloud.google.com/go/resourcemanager v1.10.7 h1:oPZKIdjyVTuag+D4HF7HO0mnSqcqgjcuA18xblwA0V0= @@ -87,8 +70,6 @@ cloud.google.com/go/securitycentermanagement v1.1.6 h1:XFqjKq4ZpKTj8xCXWs/mTmh/U cloud.google.com/go/securitycentermanagement v1.1.6/go.mod h1:nt5Z6rU4s2/j8R/EQxG5K7OfVAfAfwo89j0Nx2Srzaw= cloud.google.com/go/spanner v1.88.0 h1:HS+5TuEYZOVOXj9K+0EtrbTw7bKBLrMe3vgGsbnehmU= cloud.google.com/go/spanner v1.88.0/go.mod h1:MzulBwuuYwQUVdkZXBBFapmXee3N+sQrj2T/yup6uEE= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= cloud.google.com/go/storagetransfer v1.13.1 h1:Sjukr1LtUt7vLTHNvGc2gaAqlXNFeDFRIRmWGrFaJlY= @@ -97,13 +78,8 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= -connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= -connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/1password/onepassword-sdk-go v0.4.0 h1:Nou39yuC6Q0om03irkh5UurfPdX3wx26qZZhQeC9TBU= -github.com/1password/onepassword-sdk-go v0.4.0/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -146,10 +122,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ 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= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= @@ -173,29 +147,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/MrAlias/otel-schema-utils v0.4.0-alpha h1:6ZG9rw4NvxKwRp2Bmnfr8WJZVWLhK4e5n3+ezXE6Z2g= github.com/MrAlias/otel-schema-utils v0.4.0-alpha/go.mod h1:baehOhES9qiLv9xMcsY6ZQlKLBRR89XVJEvU7Yz3qJk= -github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= -github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4= -github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= -github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/PuerkitoBio/rehttp v1.4.0 h1:rIN7A2s+O9fmHUM1vUcInvlHj9Ysql4hE+Y0wcl/xk8= -github.com/PuerkitoBio/rehttp v1.4.0/go.mod h1:LUwKPoDbDIA2RL5wYZCNsQ90cx4OJ4AWBmq6KzWZL1s= -github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= -github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= -github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= -github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= -github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/akedrou/textdiff v0.1.0 h1:K7nbOVQju7/coCXnJRJ2fsltTwbSvC+M4hKBUJRBRGY= -github.com/akedrou/textdiff v0.1.0/go.mod h1:a9CCC49AKtFTmVDNFHDlCg7V/M7C7QExDAhb2SkL6DQ= -github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= -github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= @@ -206,12 +161,6 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 h1:TdGQS+RoR4AUO6gqUL74yK1dz/Arrt/WG+dxOj6Yo6A= -github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -221,11 +170,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/auth0/go-auth0/v2 v2.5.0 h1:IBfiYGsqFwOu4hsxV1JDtB6+ayRinybUIUCU/fRBE8Y= -github.com/auth0/go-auth0/v2 v2.5.0/go.mod h1:XVRck9fw1EIw1z4guYcbKFGmElnexb+xOvQ/0U1hHd0= github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw= github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= @@ -296,8 +241,6 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0D github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1 h1:0Pitfk3kTCUeJp+7xvTYhdgwVQhszqw1i4s8U93Z/ds= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.1/go.mod h1:lm1VCfakGKIqjexled4IMNMxgOQpDk7buAFd+7lr9pA= 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= @@ -314,50 +257,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= -github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= -github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= -github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= -github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= -github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= -github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= -github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= -github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= -github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= -github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= -github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= -github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk= -github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= @@ -378,32 +289,15 @@ github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 h1:8fUBS github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= -github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= -github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= -github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE= -github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -411,44 +305,22 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= -github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= -github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= -github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/exaring/otelpgx v0.10.0 h1:NGGegdoBQM3jNZDKG8ENhigUcgBN7d7943L0YlcIpZc= -github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U= -github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= -github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -456,23 +328,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gaissmai/bart v0.20.4 h1:Ik47r1fy3jRVU+1eYzKSW3ho2UgBVTVnUS8O993584U= -github.com/gaissmai/bart v0.20.4/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk= github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8= github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= -github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= -github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= -github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -481,8 +342,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -490,66 +349,29 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= -github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= @@ -558,43 +380,17 @@ github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8i github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= -github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= -github.com/google/go-github/v50 v50.1.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= -github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= -github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= -github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= -github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs= -github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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= @@ -603,8 +399,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -615,14 +409,8 @@ github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IP github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= -github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd h1:FsX+T6wA8spPe4c1K9vi7T0LvNCO1TTqiL8u7Wok2hw= -github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= -github.com/harness/harness-go-sdk v0.7.9 h1:4l1t+7MovJVTyU2rTWUcI8tSsCSsMsMQC6U2Fculj7g= -github.com/harness/harness-go-sdk v0.7.9/go.mod h1:iEAGFfIm0MOFJxN6tqMQSPZiEO/Dz1joLDHrkEU3lps= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -646,10 +434,6 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -682,24 +466,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f h1:Fnl4pzx8SR7k7JuzyW8lEtSFH6EQ8xgcypgIn8pcGIE= -github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= -github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -708,39 +476,21 @@ github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= -github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= -github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= -github.com/kaptinlin/jsonrepair v0.2.8 h1:BjiyVcJDwGrz01/9cvtX1ArNVvtybydGFDxoaU/6lsU= -github.com/kaptinlin/jsonrepair v0.2.8/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 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 v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= -github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -750,36 +500,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= -github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= -github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -793,14 +519,6 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mavolin/go-htmx v1.0.0 h1:43rZuemWd23zrMcTU939EsflXjOPxtHy9VraT1CZ6qQ= -github.com/mavolin/go-htmx v1.0.0/go.mod h1:r6O09gzKou9kutq3UiDPZ//Q7IeBCMcs8US5/sHFbvg= -github.com/mergestat/timediff v0.0.4 h1:NZ3sqG/6K9flhTubdltmRx3RBfIiYv6LsGP+4FlXMM8= -github.com/mergestat/timediff v0.0.4/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= -github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= -github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= -github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= github.com/micahhausler/aws-iam-policy v0.4.2 h1:HF7bERLnpqEmffV9/wTT4jZ7TbSNVk0JbpXo1Cj3up0= github.com/micahhausler/aws-iam-policy v0.4.2/go.mod h1:Ojgst9ZFn+VEEJpqtuw/LxVGqEf2+hwWBlkYWvF/XWM= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -809,14 +527,10 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= -github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -831,8 +545,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= -github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -851,34 +563,16 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/neo4j/neo4j-go-driver/v6 v6.0.0 h1:xVAi6YLOfzXUx+1Lc/F2dUhpbN76BfKleZbAlnDFRiA= -github.com/neo4j/neo4j-go-driver/v6 v6.0.0/go.mod h1:hzSTfNfM31p1uRSzL1F/BAYOgaiTarE6OAQBajfsm+I= -github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= -github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 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.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/openai/openai-go/v3 v3.21.0 h1:3GpIR/W4q/v1uUOVuK3zYtQiF3DnRrZag/sxbtvEdtc= -github.com/openai/openai-go/v3 v3.21.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 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= github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297/go.mod h1:bRQZYnvLrW1S5wYT6tbQnun8NpO5X6zP5cY3VKuDc4U= -github.com/pborman/ansi v1.0.0 h1:OqjHMhvlSuCCV5JT07yqPuJPQzQl+WXsiZ14gZsqOrQ= -github.com/pborman/ansi v1.0.0/go.mod h1:SgWzwMAx1X/Ez7i90VqF8LRiQtx52pWDiQP+x3iGnzw= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -893,58 +587,13 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= -github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posthog/posthog-go v1.10.0 h1:wfoy7Jfb4LigCoHYyMZoiJmmEoCLOkSaYfDxM/NtCqY= -github.com/posthog/posthog-go v1.10.0/go.mod h1:wB3/9Q7d9gGb1P/yf/Wri9VBlbP8oA8z++prRzL5OcY= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= -github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= -github.com/projectdiscovery/cdncheck v1.1.24 h1:6pJ4XnovIrTWzlCJs5/QD1tv6wvK0wiICmmdY0/8WAs= -github.com/projectdiscovery/cdncheck v1.1.24/go.mod h1:dFEGsG0qAJY0AaRr2N1BY0OtZiTxS4kYeT5+OkF8t1U= -github.com/projectdiscovery/chaos-client v0.5.2 h1:dN+7GXEypsJAbCD//dBcUxzAEAEH1fjc/7Rf4F/RiNU= -github.com/projectdiscovery/chaos-client v0.5.2/go.mod h1:KnoJ/NJPhll42uaqlDga6oafFfNw5l2XI2ajRijtDuU= -github.com/projectdiscovery/dnsx v1.2.2 h1:ZjUov0GOyrS8ERlKAAhk+AOkqzaYHBzCP0qZfO+6Ihg= -github.com/projectdiscovery/dnsx v1.2.2/go.mod h1:3iYm86OEqo0WxeGDkVl5WZNmG0qYE5TYNx8fBg6wX1I= -github.com/projectdiscovery/fastdialer v0.4.1 h1:kp6Q0odo0VZ0vZIGOn+q9aLgBSk6uYoD1MsjCAH8+h4= -github.com/projectdiscovery/fastdialer v0.4.1/go.mod h1:875Wlggf0JAz+fDIPwUQeeBqEF6nJA71XVrjuTZCV7I= -github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= -github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= -github.com/projectdiscovery/gologger v1.1.54 h1:WMzvJ8j/4gGfPKpCttSTaYCVDU1MWQSJnk3wU8/U6Ws= -github.com/projectdiscovery/gologger v1.1.54/go.mod h1:vza/8pe2OKOt+ujFWncngknad1XWr8EnLKlbcejOyUE= -github.com/projectdiscovery/hmap v0.0.90 h1:p8HWGvPI88hgJoAb4ayR1Oo5VzqPrOCdFG7mASUhQI4= -github.com/projectdiscovery/hmap v0.0.90/go.mod h1:dcjd9P82mkBpFGEy0wBU/3qql5Bx14kmJZvVg7o7vXY= -github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 h1:ZScLodGSezQVwsQDtBSMFp72WDq0nNN+KE/5DHKY5QE= -github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI= -github.com/projectdiscovery/networkpolicy v0.1.16 h1:H2VnLmMD7SvxF+rao+639nn8KX/kbPFY+mc8FxeltsI= -github.com/projectdiscovery/networkpolicy v0.1.16/go.mod h1:Vs/IRcJq4QUicjd/tl9gkhQWy7d/LssOwWbaz4buJ0U= -github.com/projectdiscovery/ratelimit v0.0.81 h1:u6lW+rAhS/UO0amHTYmYLipPK8NEotA9521hdojBtgI= -github.com/projectdiscovery/ratelimit v0.0.81/go.mod h1:tK04WXHuC4i6AsFkByInODSNf45gd9sfaMHzmy2bAsA= -github.com/projectdiscovery/retryabledns v1.0.102 h1:R8PzFCVofqLX3Bn4kdjOsE9wZ83FQjXZMDNs4/bHxzI= -github.com/projectdiscovery/retryabledns v1.0.102/go.mod h1:3+GL+YuHpV0Fp6UG7MbIG8mVxXHjfPO5ioQdwlnV08E= -github.com/projectdiscovery/retryablehttp-go v1.0.115 h1:ubIaVyHNj0/qxNv4gar+8/+L3G2Fhpfk54iMDctC7+E= -github.com/projectdiscovery/retryablehttp-go v1.0.115/go.mod h1:XlxLSMBVM7fTXeLVOLjVn1FLuRgQtD49NMFs9sQygfA= -github.com/projectdiscovery/subfinder/v2 v2.12.0 h1:MgEYn0F2qLvr63BWpV9jNjFiD8i9oXI3dp02tAGRft0= -github.com/projectdiscovery/subfinder/v2 v2.12.0/go.mod h1:FNy+bkJwZjUUWLte6T91IRBISqWDZ/q+ygUmoe8eb/w= -github.com/projectdiscovery/utils v0.4.21 h1:yAothTUSF6NwZ9yoC4iGe5gSBrovqKR9JwwW3msxk3Q= -github.com/projectdiscovery/utils v0.4.21/go.mod h1:HJuJFqjB6EmVaDl0ilFPKvLoMaX2GyE6Il2TqKXNs8I= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -954,78 +603,24 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.53 h1:8ERV5eXyvXlAIY8LRrhapPS34j7IKKDAnb7o1Ih3T0w= github.com/pterm/pterm v0.12.53/go.mod h1:BY2H3GtX2BX0ULqLY11C2CusIqnxsYerbkil3XvXIBg= -github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf h1:NxGxgo0KmC8w9fdn8jLCyG1SDrR/Vxbfa1nWErS3pmw= -github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf/go.mod h1:q6RK8Iv6obzk6i0rnLyYPtppwZ5uXJLloL3oxmfrwm8= -github.com/refraction-networking/utls v1.7.0 h1:9JTnze/Md74uS3ZWiRAabityY0un69rOLXsBf8LGgTs= -github.com/refraction-networking/utls v1.7.0/go.mod h1:lV0Gwc1/Fi+HYH8hOtgFRdHfKo4FKSn6+FdyOz9hRms= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e h1:OwOgxT3MRpOj5Mp6DhFdZP43FOQOf2hhywAuT5XZCR4= -github.com/riverqueue/apiframe v0.0.0-20251229202423-2b52ce1c482e/go.mod h1:O7UmsAMjpMYuToN4au5GNXdmN1gli+5FTldgXqAfaD0= -github.com/riverqueue/river v0.30.2 h1:RtJ3/CBat00Jjtllvy2P7A/QxSH3PRR0ri/B8PxWm1w= -github.com/riverqueue/river v0.30.2/go.mod h1:iPpsnw82MCcwAVhLo42g7eNdb5apT8VZ37Bel2x/Gws= -github.com/riverqueue/river/riverdriver v0.30.2 h1:JUmzh0iGPVpK4H7hugpgmQm2crOI9X4iKsd/9wz3IJk= -github.com/riverqueue/river/riverdriver v0.30.2/go.mod h1:w8DiNtR5uUfpIoNZVq1K7Xv0ER+1GrBK8nIxRFugiqI= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2 h1:nrz1NOLm9BXzTK96ANYmkiOXgjfD3+nLUbP7CrdSzY0= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.30.2/go.mod h1:KmZHJvXC1eOXSHxJa3V0JKBI+sSYhhAxkAl7AKRQPXk= -github.com/riverqueue/river/rivershared v0.30.2 h1:LFGWnhFZIXNgooXVRY/+Of6bc9Z6ndZ8kf0A6hUO+8c= -github.com/riverqueue/river/rivershared v0.30.2/go.mod h1:K/DCaSKzbmVcOLC2PmaPycHdc56MMTZjU3LWiNh3yqQ= -github.com/riverqueue/river/rivertype v0.30.2 h1:9VVcrsXEPDFnl6qyOS0PxEoUSo9P5yD1E1HwyTpbXS8= -github.com/riverqueue/river/rivertype v0.30.2/go.mod h1:rWpgI59doOWS6zlVocROcwc00fZ1RbzRwsRTU8CDguw= -github.com/riverqueue/rivercontrib/otelriver v0.7.0 h1:zLjPf674dcGrz7OPG2JF5xea0fyitFax6Cc6q370Xzo= -github.com/riverqueue/rivercontrib/otelriver v0.7.0/go.mod h1:MuyMZmYBz3JXC8ZLP0dH9IqXK95qRY6gCQSoJGh9h7E= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= -github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= -github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= -github.com/samber/slog-logrus/v2 v2.5.3 h1:N6YGgQ9CQjUQXe75/iWKtE55EENjG67HYUsJQbPn/dE= -github.com/samber/slog-logrus/v2 v2.5.3/go.mod h1:W3njRsspuMRCd33S0ibPyK1ohRaMhuXKZ1BK8pNiM+c= -github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= -github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda h1:w3JksZEWJDI7x+No5yh2/8S86hq1dmJy7n5btakG30U= -github.com/serpapi/serpapi-golang v0.0.0-20260126142127-0e41c7993cda/go.mod h1:xVL4PnCuCPwkXhVVQysVrX3hEv7nWnIbfnDj2B+hsPw= -github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= -github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= -github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -1044,12 +639,10 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -1062,61 +655,10 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/stripe/stripe-go/v84 v84.3.0 h1:77HH+ro7yzmyyF7Xkbkj6y5QtnU1WWHC6t2y4mq0Wvk= -github.com/stripe/stripe-go/v84 v84.3.0/go.mod h1:Z4gcKw1zl4geDG2+cjpSaJES9jaohGX6n7FP8/kHIqw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= -github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= -github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= -github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= -github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= -github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= -github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= -github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= -github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= -github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= -github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= -github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= -github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= -github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= -github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= -github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= -github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= -github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= -github.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw= -github.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 h1:H8wwQwTe5sL6x30z71lUgNiwBdeCHQjrphCfLwqIHGo= github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2/go.mod h1:/kR4beFhlz2g+V5ik8jW+3PMiMQAPt29y6K64NNY53c= github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= @@ -1128,12 +670,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db/go.mod h1:aiQaH1XpzIfgrJq3S1iw7w+3EDbRP7mF5fmwUhWyRUs= -github.com/weppos/publicsuffix-go v0.30.1 h1:8q+QwBS1MY56Zjfk/50ycu33NN8aa1iCCEQwo/71Oos= -github.com/weppos/publicsuffix-go v0.30.1/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -1147,21 +683,12 @@ github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= -github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc= -github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -1170,24 +697,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30= -github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= -github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= -github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= -github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 h1:YOQ1vXEwE4Rnj+uQ/3oCuJk5wgVsvUyW+glsndwYuyA= -github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968/go.mod h1:xIuOvYCZX21S5Z9bK1BMrertTGX/F8hgAPw7ERJRNS0= -github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= -go.devnw.com/structs v1.0.0 h1:FFkBoBOkapCdxFEIkpOZRmMOMr9b9hxjKTD3bJYl9lk= -go.devnw.com/structs v1.0.0/go.mod h1:wHBkdQpNeazdQHszJ2sxwVEpd8zGTEsKkeywDLGbrmg= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c h1:YSqSR1Fil5Ip0N6AlNBFbNv7cvIHZ2j4+wqbVafAGmQ= @@ -1202,8 +713,6 @@ go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= @@ -1230,279 +739,102 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= -go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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.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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= -gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 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-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1510,91 +842,34 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= -gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= -gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= -gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= -k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= -k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= -k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= -k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= -k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs= -modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -riverqueue.com/riverui v0.14.0 h1:nDHvKywBSzgvnARjvberwGc5CgBMdIdQM4Mcci3+flU= -riverqueue.com/riverui v0.14.0/go.mod h1:uUwoeQGDO4+o4ofqenWL2UNuCED5/1/lwnkFKYR9vZw= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 h1:XotDXzqvJ8Nx5eiZZueLpTuafJz8SiodgOemI+w87QU= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= -sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g=

nG-!0W@p-gQ8~zde=mBPv}U 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